diff --git a/category/execution/ethereum/db/db_snapshot.cpp b/category/execution/ethereum/db/db_snapshot.cpp index 64298765c9..47b4c00ea1 100644 --- a/category/execution/ethereum/db/db_snapshot.cpp +++ b/category/execution/ethereum/db/db_snapshot.cpp @@ -243,6 +243,42 @@ uint64_t monad_db_snapshot_loader_read_account( return bytes_consumed; } +// Consume the stream header if there is one, leaving `stream` at its first +// record. +void read_stream_header( + monad::byte_string_view &stream, monad_snapshot_type const kind) +{ + using namespace monad; + if (stream.size() < sizeof(monad_snapshot_stream_header)) { + return; + } + auto const header = + unaligned_load(stream.data()); + if (header.magic != MONAD_SNAPSHOT_STREAM_MAGIC) { + return; + } + // A legacy stream can hold the guard byte by chance, but not the magic, so + // past the magic a bad guard is corruption rather than an older layout and + // must not silently bypass the version and kind checks below. + MONAD_ASSERT_PRINTF( + header.guard == MONAD_SNAPSHOT_STREAM_GUARD, + "snapshot stream opens with the header magic but guard 0x%02x " + "(expected 0x%02x)", + header.guard, + MONAD_SNAPSHOT_STREAM_GUARD); + MONAD_ASSERT_PRINTF( + header.version == MONAD_SNAPSHOT_STREAM_VERSION, + "snapshot stream version %u is not supported (expected %u)", + header.version, + MONAD_SNAPSHOT_STREAM_VERSION); + MONAD_ASSERT_PRINTF( + header.kind == kind, + "snapshot stream holds kind %u where kind %u was expected", + header.kind, + static_cast(kind)); + stream.remove_prefix(sizeof(header)); +} + class NibblePath { private: @@ -291,16 +327,86 @@ class NibblePath } }; +using SnapshotWriteFn = uint64_t (*)( + uint64_t shard, monad_snapshot_type, unsigned char const *bytes, size_t len, + void *user); + +// Writes the records of every stream of one dump, and is shared by every clone +// of the traverse machine as well as by the eth-header writes outside it. +// +// Every record goes through here so that no stream can be opened without its +// header: a stream missing one is indistinguishable from a stream written +// before headers existed, so it would load without complaint. +class SnapshotStreamWriter +{ + SnapshotWriteFn const write_; + void *const user_; + std::array< + std::array, MONAD_SNAPSHOT_SHARDS> + header_written_{}; + // Length of each shard's account stream counted from its first record, so + // that the offsets it hands out do not shift when a header is present. + std::array account_bytes_written_{}; + + // Written lazily so that a kind a shard has no records for leaves a + // zero-length stream rather than a header-only one. + void write_stream_header_once( + uint64_t const shard, monad_snapshot_type const kind) + { + auto &written = header_written_.at(shard).at(kind); + if (written) { + return; + } + monad_snapshot_stream_header const header{ + .magic = MONAD_SNAPSHOT_STREAM_MAGIC, + .version = MONAD_SNAPSHOT_STREAM_VERSION, + .kind = static_cast(kind), + .reserved = 0, + .guard = MONAD_SNAPSHOT_STREAM_GUARD}; + std::array bytes; + monad::unaligned_store(bytes.data(), header); + MONAD_ASSERT( + write_(shard, kind, bytes.data(), bytes.size(), user_) == + bytes.size()); + written = true; + } + +public: + SnapshotStreamWriter(SnapshotWriteFn const write, void *const user) + : write_{write} + , user_{user} + { + } + + SnapshotStreamWriter(SnapshotStreamWriter const &) = delete; + + void write_record( + uint64_t const shard, monad_snapshot_type const kind, + unsigned char const *const bytes, size_t const len) + { + write_stream_header_once(shard, kind); + MONAD_ASSERT(write_(shard, kind, bytes, len, user_) == len); + } + + // Appends one account record, returning the offset it occupies in the + // shard's account stream, which is how a storage record names its account. + uint64_t write_account_record( + uint64_t const shard, unsigned char const *const bytes, + size_t const len) + { + uint64_t const offset = account_bytes_written_.at(shard); + account_bytes_written_.at(shard) += len; + write_record(shard, MONAD_SNAPSHOT_ACCOUNT, bytes, len); + return offset; + } +}; + struct MonadSnapshotTraverseMachine : public monad::mpt::TraverseMachine { unsigned char nibble; NibblePath path; - std::array &account_bytes_written; + SnapshotStreamWriter &writer; uint64_t account_offset; - uint64_t (*write)( - uint64_t shard, monad_snapshot_type, unsigned char const *bytes, - size_t len, void *user); - void *user; uint64_t total_shards; uint64_t shard_number; // Source db is page-encoded: storage leaves hold encoded pages rather than @@ -308,18 +414,12 @@ struct MonadSnapshotTraverseMachine : public monad::mpt::TraverseMachine bool page_encoded; MonadSnapshotTraverseMachine( - std::array &account_bytes_written, - uint64_t (*write)( - uint64_t shard, monad_snapshot_type, unsigned char const *bytes, - size_t len, void *user), - void *const user, uint64_t const total_shards, + SnapshotStreamWriter &writer, uint64_t const total_shards, uint64_t const shard_number, bool const page_encoded) : nibble{monad::mpt::INVALID_BRANCH} , path{} - , account_bytes_written{account_bytes_written} + , writer{writer} , account_offset{std::numeric_limits::max()} - , write(write) - , user{user} , total_shards{total_shards} , shard_number{shard_number} , page_encoded{page_encoded} @@ -367,50 +467,36 @@ struct MonadSnapshotTraverseMachine : public monad::mpt::TraverseMachine if (nibble == CODE_NIBBLE) { MONAD_ASSERT(path.length() == HASH_SIZE); uint64_t const len = val.size(); - MONAD_ASSERT( - write( - shard, - MONAD_SNAPSHOT_CODE, - reinterpret_cast(&len), - sizeof(len), - user) == sizeof(len)); - MONAD_ASSERT( - write(shard, MONAD_SNAPSHOT_CODE, val.data(), len, user) == - len); + writer.write_record( + shard, + MONAD_SNAPSHOT_CODE, + reinterpret_cast(&len), + sizeof(len)); + writer.write_record( + shard, MONAD_SNAPSHOT_CODE, val.data(), val.size()); } else { MONAD_ASSERT(nibble == STATE_NIBBLE); if (path.length() == HASH_SIZE) { - account_offset = account_bytes_written.at(shard); - account_bytes_written.at(shard) += val.size(); - MONAD_ASSERT( - write( - shard, - MONAD_SNAPSHOT_ACCOUNT, - val.data(), - val.size(), - user) == val.size()); + account_offset = + writer.write_account_record(shard, val.data(), val.size()); } else { MONAD_ASSERT(path.length() == (HASH_SIZE * 2)); // Emit one slot-format storage entry, prefixed with the owning // account's offset so the loader can re-link it. auto const emit_slot = [&](byte_string_view const entry) { - MONAD_ASSERT( - write( - shard, - MONAD_SNAPSHOT_STORAGE, - reinterpret_cast( - &account_offset), - sizeof(account_offset), - user) == sizeof(account_offset)); - MONAD_ASSERT( - write( - shard, - MONAD_SNAPSHOT_STORAGE, - entry.data(), - entry.size(), - user) == entry.size()); + writer.write_record( + shard, + MONAD_SNAPSHOT_STORAGE, + reinterpret_cast( + &account_offset), + sizeof(account_offset)); + writer.write_record( + shard, + MONAD_SNAPSHOT_STORAGE, + entry.data(), + entry.size()); }; if (page_encoded) { // Source db is page-encoded: expand the storage leaf into @@ -466,10 +552,11 @@ MONAD_ANONYMOUS_NAMESPACE_END // Directory Format // block number // shard -// account -> empty | leaf.value(), ... -// storage -> empty | [account_offset, leaf.value()], ... -// code -> empty | [size, code], ... -// eth header -> empty | rlp(header) +// account +// storage +// code +// eth_header +// Each file holds one stream, empty or in the layout db_snapshot.h describes. bool monad_db_dump_snapshot( char const *const *const dbname_paths, size_t const len, unsigned const sq_thread_cpu, uint64_t const block, @@ -505,6 +592,7 @@ bool monad_db_dump_snapshot( io_context, dump_from_secondary ? timeline_id::secondary : timeline_id::primary}; + SnapshotStreamWriter writer{write, user}; for (uint64_t b = block < 256 ? 0 : block - 255; b <= block; ++b) { uint64_t const header_shard = block - b; if (header_shard % total_shards != shard_number) { @@ -521,13 +609,11 @@ bool monad_db_dump_snapshot( return false; } auto const header_view = header_cursor_res.value().node->value(); - MONAD_ASSERT( - write( - header_shard, - MONAD_SNAPSHOT_ETH_HEADER, - header_view.data(), - header_view.size(), - user) == header_view.size()); + writer.write_record( + header_shard, + MONAD_SNAPSHOT_ETH_HEADER, + header_view.data(), + header_view.size()); } auto const root = db.load_root_for_version(block); @@ -548,11 +634,8 @@ bool monad_db_dump_snapshot( return false; } - std::array account_bytes_written{}; MonadSnapshotTraverseMachine machine{ - account_bytes_written, - write, - user, + writer, total_shards, shard_number, db.state_machine_type() == state_machine_kind::monad}; @@ -599,28 +682,36 @@ void monad_db_snapshot_loader_load( using namespace monad::mpt; constexpr size_t BYTES_READ_BEFORE_FLUSH = 10ull * 1024 * 1024 * 1024; MONAD_ASSERT(loader); + // Account offsets index from the first account record, so the storage loop + // below must resolve them against this header-stripped view rather than the + // raw buffer. + byte_string_view accounts{}; if (account) { - for (uint64_t account_offset = 0; account_offset != account_len;) { + accounts = byte_string_view{account, account_len}; + read_stream_header(accounts, MONAD_SNAPSHOT_ACCOUNT); + for (uint64_t account_offset = 0; account_offset != accounts.size();) { account_offset += monad_db_snapshot_loader_read_account( - loader, shard, account_offset, {account, account_len}); + loader, shard, account_offset, accounts); if (loader->bytes_read >= BYTES_READ_BEFORE_FLUSH) { monad_db_snapshot_loader_flush(loader); } - MONAD_ASSERT(account_offset <= account_len); + MONAD_ASSERT(account_offset <= accounts.size()); } } if (storage) { MONAD_ASSERT(account); byte_string_view storage_view{storage, storage_len}; + read_stream_header(storage_view, MONAD_SNAPSHOT_STORAGE); auto &account_offset_to_update = loader->account_offset_to_update.at(shard); while (!storage_view.empty()) { + MONAD_ASSERT(storage_view.size() >= sizeof(uint64_t)); uint64_t const account_offset = unaligned_load(storage_view.data()); if (!account_offset_to_update.contains(account_offset)) { monad_db_snapshot_loader_read_account( - loader, shard, account_offset, {account, account_len}); + loader, shard, account_offset, accounts); } storage_view.remove_prefix(sizeof(account_offset)); byte_string_view const before{storage_view}; @@ -675,6 +766,7 @@ void monad_db_snapshot_loader_load( if (code) { byte_string_view code_view{code, code_len}; + read_stream_header(code_view, MONAD_SNAPSHOT_CODE); while (!code_view.empty()) { MONAD_ASSERT(code_view.size() >= sizeof(uint64_t)); uint64_t const size = unaligned_load(code_view.data()); @@ -698,11 +790,13 @@ void monad_db_snapshot_loader_load( if (eth_header) { byte_string_view enc{eth_header, eth_header_len}; + read_stream_header(enc, MONAD_SNAPSHOT_ETH_HEADER); + byte_string_view const rlp_header{enc}; auto const header = rlp::decode_block_header(enc); MONAD_ASSERT(header.has_value()); MONAD_ASSERT(header.value().number == (loader->block - shard)); // stash to upsert versions last - loader->eth_headers.at(shard).assign(eth_header, eth_header_len); + loader->eth_headers.at(shard).assign(rlp_header); } monad_db_snapshot_loader_flush(loader); } diff --git a/category/execution/ethereum/db/db_snapshot.h b/category/execution/ethereum/db/db_snapshot.h index 09cc42c412..9472b0337d 100644 --- a/category/execution/ethereum/db/db_snapshot.h +++ b/category/execution/ethereum/db/db_snapshot.h @@ -21,6 +21,8 @@ #ifdef __cplusplus + #include + inline constexpr unsigned MONAD_SNAPSHOT_SHARD_NIBBLES = 2; inline constexpr unsigned MONAD_SNAPSHOT_SHARDS = 1 << (MONAD_SNAPSHOT_SHARD_NIBBLES * 4); @@ -32,6 +34,57 @@ static_assert(MONAD_SNAPSHOT_SHARDS == 256); // (active shards) * MONAD_SNAPSHOT_FILES_PER_SHARD descriptors at its peak. inline constexpr unsigned MONAD_SNAPSHOT_FILES_PER_SHARD = 4; +// Every non-empty stream of a shard opens with monad_snapshot_stream_header, +// followed by that stream's records: +// +// eth_header := rlp(header) +// account := encode_account_db(address, account) ... +// storage := [account_offset: uint64][encode_storage_db(key, value)] ... +// code := [size: uint64][code] ... +// +// A storage record is prefixed by the offset of the owning account within the +// shard's account stream, which is how the loader relinks it whatever order the +// records arrive in. Those offsets count from the first account record rather +// than from the stream header, so they do not depend on whether the header is +// present. +// +// A dump always writes the header, but a reader must also accept a stream that +// lacks one and parse its records from byte 0: that is how a snapshot written +// before the header existed is recognised. +// +// Scalars are native-endian, which the format takes to be little-endian. +inline constexpr uint32_t MONAD_SNAPSHOT_STREAM_MAGIC = 0x5347534d; // "MSGS" +inline constexpr uint8_t MONAD_SNAPSHOT_STREAM_VERSION = 1; +inline constexpr uint8_t MONAD_SNAPSHOT_STREAM_GUARD = 0xff; + +struct monad_snapshot_stream_header +{ + uint32_t magic; + uint8_t version; + // The monad_snapshot_type this stream holds, so a stream file of the wrong + // kind is rejected rather than misparsed. Nothing here identifies the + // shard, so files swapped between shards still load. + uint8_t kind; + // Zero. Readers ignore it, so a later revision may give it a meaning + // without a version bump only if an unaware reader can correctly skip it. + uint8_t reserved; + // MONAD_SNAPSHOT_STREAM_GUARD. The magic's first byte on disk is below + // 0xc0, so a header can never be mistaken for the RLP list that opens an + // eth_header or account stream; the guard is the most significant byte when + // the eight are read as the leading uint64 of a storage or code stream, + // putting them above 2^56 where an account offset or a code length never + // reaches. A binary predating the header therefore aborts on the bogus + // value rather than misreading. + uint8_t guard; +}; + +static_assert(sizeof(struct monad_snapshot_stream_header) == 8); +// Both properties the guard comment relies on are positional, and hold only +// where the magic's low byte and the guard are respectively the first and last +// of the eight bytes on disk. +static_assert(std::endian::native == std::endian::little); +static_assert((MONAD_SNAPSHOT_STREAM_MAGIC & 0xff) < 0xc0); + extern "C" { #endif @@ -69,4 +122,8 @@ void monad_db_snapshot_loader_destroy(struct monad_db_snapshot_loader *); #ifdef __cplusplus } + +// The dumper indexes per-kind state by monad_snapshot_type, so a new kind needs +// a wider array rather than a runtime out_of_range mid-dump. +static_assert(MONAD_SNAPSHOT_CODE + 1 == MONAD_SNAPSHOT_FILES_PER_SHARD); #endif diff --git a/category/execution/ethereum/test/test_db_snapshot.cpp b/category/execution/ethereum/test/test_db_snapshot.cpp index 08675abcc7..1a1ae5a533 100644 --- a/category/execution/ethereum/test/test_db_snapshot.cpp +++ b/category/execution/ethereum/test/test_db_snapshot.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -33,13 +34,21 @@ #include #include #include +#include #include #include #include +#include +#include #include +#include +#include +#include +#include +#include namespace monad::mpt::test { @@ -104,6 +113,110 @@ namespace std::filesystem::remove_all(path, ec); } }; + + constexpr std::array STREAM_FILES{ + std::pair{"eth_header", MONAD_SNAPSHOT_ETH_HEADER}, + std::pair{"account", MONAD_SNAPSHOT_ACCOUNT}, + std::pair{"storage", MONAD_SNAPSHOT_STORAGE}, + std::pair{"code", MONAD_SNAPSHOT_CODE}}; + static_assert(STREAM_FILES.size() == MONAD_SNAPSHOT_FILES_PER_SHARD); + + monad::byte_string read_file(std::filesystem::path const &path) + { + std::ifstream in{path, std::ios::binary}; + MONAD_ASSERT(in.is_open()); + std::stringstream buffer; + buffer << in.rdbuf(); + auto const bytes = buffer.str(); + return monad::byte_string{ + reinterpret_cast(bytes.data()), + bytes.size()}; + } + + void write_file( + std::filesystem::path const &path, monad::byte_string_view const bytes) + { + std::ofstream out{path, std::ios::binary | std::ios::trunc}; + MONAD_ASSERT(out.is_open()); + out.write( + reinterpret_cast(bytes.data()), + static_cast(bytes.size())); + out.close(); + MONAD_ASSERT(out.good()); + } + + // Assert the header is well formed and of `kind`, then remove it, leaving + // the stream in the layout one predating headers would have. A missing or + // malformed header fails here rather than corrupting the stripped stream. + monad::byte_string_view strip_stream_header( + monad::byte_string_view view, monad_snapshot_type const kind) + { + using namespace monad; + MONAD_ASSERT(view.size() >= sizeof(monad_snapshot_stream_header)); + auto const header = + unaligned_load(view.data()); + MONAD_ASSERT(header.magic == MONAD_SNAPSHOT_STREAM_MAGIC); + MONAD_ASSERT(header.version == MONAD_SNAPSHOT_STREAM_VERSION); + MONAD_ASSERT(header.kind == kind); + MONAD_ASSERT(header.reserved == 0); + MONAD_ASSERT(header.guard == MONAD_SNAPSHOT_STREAM_GUARD); + view.remove_prefix(sizeof(header)); + return view; + } + + // Rewrite a snapshot in the layout that predates stream headers. + void strip_stream_headers(std::filesystem::path const &version_dir) + { + for (auto const &dir : + std::filesystem::directory_iterator{version_dir}) { + for (auto const &[name, kind] : STREAM_FILES) { + auto const path = dir.path() / name; + auto const framed = read_file(path); + if (framed.empty()) { + continue; + } + write_file(path, strip_stream_header(framed, kind)); + } + } + } + + // Load a snapshot directory the way monad_db_snapshot_load_filesystem does, + // but without verifying checksums, so a test can rewrite the streams first. + void load_snapshot( + std::string const &dbname, std::filesystem::path const &root, + uint64_t const block, bool const load_to_secondary) + { + char const *dbname_paths[] = {dbname.c_str()}; + auto *const loader = monad_db_snapshot_loader_create( + block, + dbname_paths, + 1, + static_cast(-1), + load_to_secondary); + for (auto const &dir : std::filesystem::directory_iterator{ + root / std::to_string(block)}) { + uint64_t const shard = std::stoull(dir.path().stem()); + auto const eth_header = read_file(dir.path() / "eth_header"); + auto const account = read_file(dir.path() / "account"); + auto const storage = read_file(dir.path() / "storage"); + auto const code = read_file(dir.path() / "code"); + auto const ptr = [](monad::byte_string const &b) { + return b.empty() ? nullptr : b.data(); + }; + monad_db_snapshot_loader_load( + loader, + shard, + ptr(eth_header), + eth_header.size(), + ptr(account), + account.size(), + ptr(storage), + storage.size(), + ptr(code), + code.size()); + } + monad_db_snapshot_loader_destroy(loader); + } } TEST(DbBinarySnapshot, Basic) @@ -619,6 +732,262 @@ TEST(DbBinarySnapshot, LoadPageModeOnSecondaryDb) } } +namespace +{ + constexpr uint64_t PAGE_BLOCK = 1; + // Raw slot keys 0x00-0x7f share page 0, 0x80-0xff page 1, and so on, so + // every account spans four pages, two of which hold more than one slot. + constexpr std::array PAGE_SLOTS{ + 0x0000, 0x0001, 0x0002, 0x007f, 0x0080, 0x0081, 0x0100, 0x01ff}; + constexpr size_t PAGES_PER_ACCOUNT = 4; + std::array const PAGE_ADDRS{ + monad::Address{1}, + monad::Address{2}, + monad::Address{3}, + monad::Address{4}}; + + monad::bytes32_t page_slot_key(uint16_t const raw) + { + monad::bytes32_t key{}; + key.bytes[30] = static_cast(raw >> 8); + key.bytes[31] = static_cast(raw & 0xff); + return key; + } + + monad::bytes32_t + page_slot_value(monad::Address const &addr, uint16_t const raw) + { + monad::bytes32_t value{}; + value.bytes[29] = addr.bytes[19]; + value.bytes[30] = static_cast(raw >> 8); + value.bytes[31] = static_cast((raw & 0xff) ^ 0xa5); + return value; + } + + monad::byte_string page_code(monad::Address const &addr) + { + return monad::byte_string(64, addr.bytes[19]); + } + + // Populate the page-encoded secondary timeline of `dbname` and return its + // state root. + monad::bytes32_t build_page_source(std::string const &dbname) + { + using namespace monad; + using namespace monad::mpt; + + mpt::Db db1{ + std::make_unique(), + OnDiskDbConfig{.dbname_paths = {dbname}}}; + // Activate before any TrieDb exists (requires worker_thread_use_count + // == 1). + mpt::Db db2 = db1.activate_secondary_timeline( + std::make_unique()); + load_header({}, db2, BlockHeader{.number = 0}); + db2.update_finalized_version(0); + + StateDeltas deltas; + Code code_delta; + for (auto const &addr : PAGE_ADDRS) { + StorageDeltas storage; + for (auto const raw : PAGE_SLOTS) { + storage.emplace( + page_slot_key(raw), + StorageDelta{bytes32_t{}, page_slot_value(addr, raw)}); + } + auto const code = page_code(addr); + bytes32_t const code_hash = to_bytes(keccak256(code)); + code_delta.emplace(code_hash, vm::make_shared_intercode(code)); + deltas.emplace( + addr, + StateDelta{ + .account = + {std::nullopt, + Account{.balance = 1, .code_hash = code_hash}}, + .storage = storage}); + } + TrieDb tdb2{db2}; + MONAD_ASSERT(tdb2.is_page_encoded()); + PageCommitBuilder builder(PAGE_BLOCK, tdb2); + builder.add_state_deltas(deltas).add_code(code_delta); + BlockHeader const header{.number = PAGE_BLOCK}; + tdb2.commit( + bytes32_t{PAGE_BLOCK}, + builder, + header, + deltas, + [&](BlockHeader &h) { + h.receipts_root = tdb2.receipts_root(); + h.state_root = tdb2.state_root(); + h.withdrawals_root = tdb2.withdrawals_root(); + h.transactions_root = tdb2.transactions_root(); + }); + tdb2.finalize(PAGE_BLOCK, bytes32_t{PAGE_BLOCK}); + return tdb2.state_root(); + } + + void dump_page_source( + std::string const &dbname, std::filesystem::path const &root) + { + auto *const context = + monad_db_snapshot_filesystem_write_user_context_create( + root.c_str(), PAGE_BLOCK); + char const *paths[] = {dbname.c_str()}; + EXPECT_TRUE(monad_db_dump_snapshot( + paths, + 1, + static_cast(-1), + PAGE_BLOCK, + monad_db_snapshot_write_filesystem, + context, + 2048, + 1, + 0, + /*dump_from_secondary=*/true)); + monad_db_snapshot_filesystem_write_user_context_destroy(context); + } + + void activate_page_secondary(std::string const &dbname) + { + using namespace monad; + using namespace monad::mpt; + mpt::Db primary{ + std::make_unique(), + OnDiskDbConfig{.dbname_paths = {dbname}}}; + [[maybe_unused]] auto const secondary = + primary.activate_secondary_timeline( + std::make_unique()); + MONAD_ASSERT(primary.timeline_active(timeline_id::secondary)); + } + + void verify_page_restore( + std::string const &dbname, monad::bytes32_t const &expected_root) + { + using namespace monad; + using namespace monad::mpt; + + mpt::Db db{ + std::make_unique(), + OnDiskDbConfig{.append = true, .dbname_paths = {dbname}}}; + { + auto db2 = db.open_secondary_timeline( + std::make_unique()); + ASSERT_TRUE(db2.has_value()); + db = std::move(db2.value()); + } + TrieDb tdb{db}; + ASSERT_TRUE(tdb.is_page_encoded()); + tdb.set_block_and_prefix(PAGE_BLOCK); + EXPECT_EQ(tdb.state_root(), expected_root); + + Incarnation const inc{0, 0}; + for (auto const &addr : PAGE_ADDRS) { + ASSERT_TRUE(tdb.read_account(addr).has_value()); + for (auto const raw : PAGE_SLOTS) { + EXPECT_EQ( + tdb.read_storage(addr, inc, page_slot_key(raw)), + page_slot_value(addr, raw)) + << "addr=" << static_cast(addr.bytes[19]) << " slot=0x" + << std::hex << raw; + } + } + + auto const state_cursor = + db.find(concat(finalized_nibbles, STATE_NIBBLE), PAGE_BLOCK); + ASSERT_TRUE(state_cursor.has_value()); + LeafCounter counter; + ASSERT_TRUE( + db.traverse_blocking(state_cursor.value(), counter, PAGE_BLOCK)); + EXPECT_EQ(counter.count, PAGE_ADDRS.size() * PAGES_PER_ACCOUNT); + } +} + +// Every stream a shard writes opens with a header naming its version and kind. +TEST(DbBinarySnapshot, SnapshotStreamHeaders) +{ + TempDb const src_db; + TempDir const snapshot_dir; + + build_page_source(src_db.path); + dump_page_source(src_db.path, snapshot_dir.path); + + std::array headers_checked{}; + for (auto const &dir : std::filesystem::directory_iterator{ + snapshot_dir.path / std::to_string(PAGE_BLOCK)}) { + for (auto const &[name, kind] : STREAM_FILES) { + auto const stream = read_file(dir.path() / name); + if (stream.empty()) { + continue; + } + // Asserts the header is well formed and of this stream's kind. + EXPECT_LT(strip_stream_header(stream, kind).size(), stream.size()); + ++headers_checked.at(kind); + } + } + for (auto const &[name, kind] : STREAM_FILES) { + EXPECT_GT(headers_checked.at(kind), 0u) << name; + } +} + +// A snapshot with no stream headers at all — the layout dumped before they +// existed — still restores into either encoding. +TEST(DbBinarySnapshot, HeaderlessSnapshotRestores) +{ + using namespace monad; + using namespace monad::mpt; + + TempDb const src_db; + TempDb const page_db; + TempDb const slot_db; + TempDir const snapshot_dir; + + bytes32_t const source_root = build_page_source(src_db.path); + dump_page_source(src_db.path, snapshot_dir.path); + strip_stream_headers(snapshot_dir.path / std::to_string(PAGE_BLOCK)); + + activate_page_secondary(page_db.path); + load_snapshot( + page_db.path, + snapshot_dir.path, + PAGE_BLOCK, + /*load_to_secondary=*/true); + verify_page_restore(page_db.path, source_root); + + { + mpt::Db dest_init{ + std::make_unique(), + OnDiskDbConfig{.dbname_paths = {slot_db.path}}}; + monad::mpt::test::DbAccessor::aux(dest_init) + .metadata_ctx() + .set_state_machine_kind( + timeline_id::primary, state_machine_kind::ethereum); + } + load_snapshot( + slot_db.path, + snapshot_dir.path, + PAGE_BLOCK, + /*load_to_secondary=*/false); + { + AsyncIOContext io_context{ + ReadOnlyOnDiskDbConfig{.dbname_paths = {slot_db.path}}}; + mpt::Db db{io_context}; + TrieDb tdb{db}; + ASSERT_FALSE(tdb.is_page_encoded()); + tdb.set_block_and_prefix(PAGE_BLOCK); + Incarnation const inc{0, 0}; + for (auto const &addr : PAGE_ADDRS) { + ASSERT_TRUE(tdb.read_account(addr).has_value()); + for (auto const raw : PAGE_SLOTS) { + EXPECT_EQ( + tdb.read_storage(addr, inc, page_slot_key(raw)), + page_slot_value(addr, raw)) + << "addr=" << static_cast(addr.bytes[19]) << " slot=0x" + << std::hex << raw; + } + } + } +} + // Dump from a page-encoded secondary timeline, then load into a fresh // slot-encoded primary db. This is the dual-db migration path: the secondary // holds page-encoded state, monad_db_dump_snapshot(dump_from_secondary=true)