From 739e608d298dc2696db432e263e5652033d58a10 Mon Sep 17 00:00:00 2001 From: Vicky Chen Date: Thu, 10 Sep 2026 12:12:13 -0400 Subject: [PATCH 1/2] Cleanup MIP8 migration code: assume single page-encoded timeline in runloop_monad and statesync runloop_monad and the statesync client and server now require a single page-encoded db and assert it on entry, instead of carrying an optional secondary timeline and branching on encoding. - runloop_monad: drop the secondary_db parameter and the for_each_db dual writes; assert db.is_page_encoded() and no active secondary. - statesync client: drop the secondary db and slot storage builder; commit() is page-only, finalize() rolls forward one db. Drop the chain_config parameter from the C ABI and the Rust StateSyncCtx, since the target revision no longer selects an encoding. - statesync server: always expand page leaves into slot upserts; the server context asserts a page-encoded TrieDb. - runloop C library: open a page-encoded primary only; replace the primary/secondary state root getters with monad_runloop_get_state_root. - tests: collapse the pre/post-mip8 typed fixture into one page-encoded fixture, drop chain and timestamp plumbing, update hardcoded state roots. runloop_monad_ethblocks keeps its secondary_db for pre-mip8 replay. Co-Authored-By: Claude Fable 5.1 --- .../runloop/runloop_interface_monad.cpp | 38 +- .../runloop/runloop_interface_monad.h | 8 +- category/execution/runloop/runloop_monad.cpp | 53 +- category/execution/runloop/runloop_monad.hpp | 8 +- category/statesync/statesync_client.cpp | 123 ++-- category/statesync/statesync_client.h | 7 +- .../statesync/statesync_client_context.cpp | 366 ++++-------- .../statesync/statesync_client_context.hpp | 11 +- category/statesync/statesync_server.cpp | 64 +- .../statesync/statesync_server_context.cpp | 3 + category/statesync/test/fuzz_statesync.cpp | 33 +- .../statesync/test/test_network_shutdown.cpp | 3 +- category/statesync/test/test_statesync.cpp | 557 ++++-------------- cmd/monad/main.cpp | 32 +- rust/crates/monad-statesync/src/ffi.rs | 5 - rust/crates/monad-statesync/wrapper.h | 5 +- 16 files changed, 369 insertions(+), 947 deletions(-) diff --git a/category/execution/runloop/runloop_interface_monad.cpp b/category/execution/runloop/runloop_interface_monad.cpp index e0df868372..677bce3056 100644 --- a/category/execution/runloop/runloop_interface_monad.cpp +++ b/category/execution/runloop/runloop_interface_monad.cpp @@ -211,11 +211,8 @@ struct MonadRunloopImpl fs::path ledger_dir; AccountOverrideMap account_override; mpt::Db raw_db; - mpt::Db secondary_raw_db; TrieDb triedb; - TrieDb secondary_triedb; MonadRunloopTrieDb runloop_db; - MonadRunloopTrieDb secondary_runloop_db; vm::VM vm; BlockHashBufferFinalized block_hash_buffer; fiber::PriorityPool priority_pool; @@ -226,40 +223,26 @@ struct MonadRunloopImpl uint64_t chain_id, char const *ledger_path, char const *db_path); }; -mpt::Db get_secondary_raw_db(mpt::Db &db) -{ - if (db.timeline_active(mpt::timeline_id::secondary)) { - auto db2 = - db.open_secondary_timeline(std::make_unique()); - MONAD_ASSERT(db2.has_value()); - return std::move(*db2); - } - return db.activate_secondary_timeline( - std::make_unique()); -} - MonadRunloopImpl::MonadRunloopImpl( uint64_t const chain_id, char const *const ledger_path, char const *const db_path) : chain{monad_chain_from_chain_id(chain_id)} , ledger_dir{ledger_path} - , raw_db{std::make_unique(), mpt::OnDiskDbConfig{.append = true, .compaction = true, .rewind_to_latest_finalized = true, .rd_buffers = 8192, .wr_buffers = 32, .uring_entries = 128, .sq_thread_cpu = sq_thread_cpu, .dbname_paths = {fs::path{db_path}}}} - , secondary_raw_db{get_secondary_raw_db(raw_db)} + , raw_db{std::make_unique(), mpt::OnDiskDbConfig{.append = true, .compaction = true, .rewind_to_latest_finalized = true, .rd_buffers = 8192, .wr_buffers = 32, .uring_entries = 128, .sq_thread_cpu = sq_thread_cpu, .dbname_paths = {fs::path{db_path}}}} , triedb{raw_db, /*enable_multiblock_cache=*/true} - , secondary_triedb{secondary_raw_db} , runloop_db{triedb, account_override} - , secondary_runloop_db{secondary_triedb, account_override} , vm{} , block_hash_buffer{} , priority_pool{nthreads, nfibers} { - MONAD_ASSERT(triedb.is_page_encoded() == false); - MONAD_ASSERT(secondary_triedb.is_page_encoded() == true); + // runloop_monad requires a single page-encoded timeline; the machine + // passed above stamps the primary as page-encoded. + MONAD_ASSERT(triedb.is_page_encoded()); + MONAD_ASSERT(!raw_db.timeline_active(mpt::timeline_id::secondary)); if (triedb.get_root() == nullptr) { LOG_INFO("loading from genesis"); GenesisState const genesis_state = chain->get_genesis_state(); load_genesis_state(genesis_state, triedb); - load_genesis_state(genesis_state, secondary_triedb); } else { LOG_INFO("loading from previous DB state"); @@ -406,7 +389,6 @@ try { runloop->ledger_dir, runloop->raw_db, runloop->runloop_db, - &runloop->secondary_runloop_db, runloop->vm, runloop->block_hash_buffer, runloop->priority_pool, @@ -463,7 +445,7 @@ extern "C" void monad_runloop_get_balance( store_be(result_balance->bytes, bal); } -extern "C" void monad_runloop_get_primary_state_root( +extern "C" void monad_runloop_get_state_root( MonadRunloop *const pre_runloop, MonadRunloopWord *const result_state_root) { MonadRunloopImpl *const runloop = to_impl(pre_runloop); @@ -471,14 +453,6 @@ extern "C" void monad_runloop_get_primary_state_root( std::bit_cast(runloop->triedb.state_root()); } -extern "C" void monad_runloop_get_secondary_state_root( - MonadRunloop *const pre_runloop, MonadRunloopWord *const result_state_root) -{ - MonadRunloopImpl *const runloop = to_impl(pre_runloop); - *result_state_root = std::bit_cast( - runloop->secondary_runloop_db.state_root()); -} - extern "C" void monad_runloop_dump(MonadRunloop *const pre_runloop) { MonadRunloopImpl *const runloop = to_impl(pre_runloop); diff --git a/category/execution/runloop/runloop_interface_monad.h b/category/execution/runloop/runloop_interface_monad.h index 485922e62c..785bad1383 100644 --- a/category/execution/runloop/runloop_interface_monad.h +++ b/category/execution/runloop/runloop_interface_monad.h @@ -55,12 +55,8 @@ void monad_runloop_get_balance( MonadRunloop *, MonadRunloopAddress const *, MonadRunloopWord *result_balance); -// Store current primary state root in `result_state_root`. -void monad_runloop_get_primary_state_root( - MonadRunloop *, MonadRunloopWord *result_state_root); - -// Store current secondary state root in `result_state_root`. -void monad_runloop_get_secondary_state_root( +// Store current state root in `result_state_root`. +void monad_runloop_get_state_root( MonadRunloop *, MonadRunloopWord *result_state_root); // Dump the current state of the database to stdout diff --git a/category/execution/runloop/runloop_monad.cpp b/category/execution/runloop/runloop_monad.cpp index f0fb23b3cc..06c7d387bb 100644 --- a/category/execution/runloop/runloop_monad.cpp +++ b/category/execution/runloop/runloop_monad.cpp @@ -183,7 +183,7 @@ Result propose_block( BlockHashChain &block_hash_chain, MonadChain const &chain, Db &db, vm::VM &vm, fiber::PriorityPool &priority_pool, bool const is_first_block, bool const enable_tracing, BlockCache &block_cache, - ExecutionEventRecorder *const exec_recorder, Db *secondary_db, + ExecutionEventRecorder *const exec_recorder, RunloopMonadOverride const runloop_override) { [[maybe_unused]] auto const block_start = std::chrono::system_clock::now(); @@ -282,11 +282,9 @@ Result propose_block( // Core execution: transaction-level EVM execution that tracks state // changes but does not commit them - for_each_db(db, secondary_db, [&](Db &d) { - d.set_block_and_prefix( - block.header.number - 1, - is_first_block ? bytes32_t{} : consensus_header.parent_id()); - }); + db.set_block_and_prefix( + block.header.number - 1, + is_first_block ? bytes32_t{} : consensus_header.parent_id()); block.header.parent_hash = to_bytes(keccak256(rlp::encode_block_header(db.read_eth_header()))); @@ -300,7 +298,7 @@ Result propose_block( BlockExecOutput exec_output; BlockMetrics block_metrics; - BlockState block_state(db, vm, secondary_db); + BlockState block_state(db, vm); record_block_marker_event(exec_recorder, MONAD_EXEC_BLOCK_PERF_EVM_ENTER); BOOST_OUTCOME_TRY( auto const results, @@ -337,7 +335,8 @@ Result propose_block( .call_frames = call_frames, .ommers = block.ommers, .withdrawals = block.withdrawals}; - commit_block(db, secondary_db, block_id, block.header, *state, anc); + commit_block( + db, /*secondary_db=*/nullptr, block_id, block.header, *state, anc); [[maybe_unused]] auto const commit_time = std::chrono::duration_cast( std::chrono::steady_clock::now() - commit_begin); @@ -363,16 +362,11 @@ Result propose_block( block_id, consensus_header.parent_id()); - // Dual-db migration: log both timelines' state roots (slot primary vs - // page secondary) so a divergence is visible per block. LOG_INFO( - "block={}, block_id={} state_root primary={}{}", + "block={}, block_id={} state_root={}", block.header.number, block_id, - db.state_root(), - secondary_db != nullptr - ? fmt::format(" secondary={}", secondary_db->state_root()) - : std::string{}); + db.state_root()); // Emit the block metrics log line [[maybe_unused]] auto const block_time = @@ -493,13 +487,23 @@ MONAD_NAMESPACE_BEGIN Result> runloop_monad( MonadChain const &chain, std::filesystem::path const &ledger_dir, - mpt::Db &raw_db, Db &db, Db *secondary_db, vm::VM &vm, + mpt::Db &raw_db, Db &db, vm::VM &vm, BlockHashBufferFinalized &block_hash_buffer, fiber::PriorityPool &priority_pool, uint64_t &block_num, uint64_t const end_block_num, sig_atomic_t const volatile &stop, bool const enable_tracing, ExecutionEventRecorder *const exec_recorder, RunloopMonadOverride const runloop_override) { + // Live execution runs on a single page-encoded timeline. A db that is + // still slot-encoded, or still carries a secondary timeline from the + // dual-db migration, is rejected here instead of being executed against. + MONAD_ASSERT( + db.is_page_encoded(), "runloop_monad requires a page-encoded db"); + MONAD_ASSERT( + !raw_db.is_on_disk() || + !raw_db.timeline_active(mpt::timeline_id::secondary), + "runloop_monad requires a single timeline; deactivate the secondary"); + constexpr auto SLEEP_TIME = std::chrono::microseconds(100); uint64_t const start_block_num = runloop_override.start_block_num(block_num); @@ -652,15 +656,12 @@ Result> runloop_monad( enable_tracing, &block_cache, exec_recorder, - secondary_db, runloop_override]( bytes32_t const &block_id, auto const &header) -> Result> { auto const block_time_start = std::chrono::steady_clock::now(); - for_each_db(db, secondary_db, [&](Db &d) { - d.update_voted_metadata(header.seqno - 1, header.parent_id()); - }); + db.update_voted_metadata(header.seqno - 1, header.parent_id()); record_block_qc(exec_recorder, header, last_finalized_block_number); uint64_t const block_number = header.execution_inputs.number; @@ -713,7 +714,6 @@ Result> runloop_monad( enable_tracing, block_cache, exec_recorder, - secondary_db, runloop_override); MONAD_ABORT_PRINTF("handled rev value %d", rev); }; @@ -721,9 +721,7 @@ Result> runloop_monad( BlockExecOutput const exec_output, record_block_result(exec_recorder, propose_dispatch())); - for_each_db(db, secondary_db, [&](Db &d) { - d.update_proposed_metadata(header.seqno, block_id); - }); + db.update_proposed_metadata(header.seqno, block_id); log_tps( block_number, @@ -748,17 +746,14 @@ Result> runloop_monad( "Processing finalization for block {} with block_id {}", block, block_id); - for_each_db( - db, secondary_db, [&](Db &d) { d.finalize(block, block_id); }); + db.finalize(block, block_id); block_hash_chain.finalize(block_id); record_block_finalized(exec_recorder, block_id, block); finalized_block_num = block; if (!verified_blocks.empty() && verified_blocks.back() != mpt::INVALID_BLOCK_NUM) { - for_each_db(db, secondary_db, [&](Db &d) { - d.update_verified_block(verified_blocks.back()); - }); + db.update_verified_block(verified_blocks.back()); } record_block_verified(exec_recorder, verified_blocks); } diff --git a/category/execution/runloop/runloop_monad.hpp b/category/execution/runloop/runloop_monad.hpp index 3cbfe1e413..5e37124d3a 100644 --- a/category/execution/runloop/runloop_monad.hpp +++ b/category/execution/runloop/runloop_monad.hpp @@ -43,11 +43,11 @@ namespace fiber class PriorityPool; } +// `db` must be a single page-encoded timeline; both are asserted on entry. Result> runloop_monad( MonadChain const &, std::filesystem::path const &, mpt::Db &, Db &, - Db *secondary_db, vm::VM &, BlockHashBufferFinalized &, - fiber::PriorityPool &, uint64_t &, uint64_t, sig_atomic_t const volatile &, - bool enable_tracing, ExecutionEventRecorder *, - RunloopMonadOverride runloop_override = {}); + vm::VM &, BlockHashBufferFinalized &, fiber::PriorityPool &, uint64_t &, + uint64_t, sig_atomic_t const volatile &, bool enable_tracing, + ExecutionEventRecorder *, RunloopMonadOverride runloop_override = {}); MONAD_NAMESPACE_END diff --git a/category/statesync/statesync_client.cpp b/category/statesync/statesync_client.cpp index 3ed1920e7c..92ee189fc4 100644 --- a/category/statesync/statesync_client.cpp +++ b/category/statesync/statesync_client.cpp @@ -28,7 +28,6 @@ #include #include #include -#include #include #include @@ -40,7 +39,6 @@ using namespace monad::mpt; unsigned const MONAD_SQPOLL_DISABLED = unsigned(-1); monad_statesync_client_context *monad_statesync_client_context_create( - monad_chain_config const chain_config, char const *const *const dbname_paths, size_t const len, unsigned const sq_thread_cpu, monad_statesync_client *const sync, void (*statesync_send_request)( @@ -49,13 +47,12 @@ monad_statesync_client_context *monad_statesync_client_context_create( std::vector const paths{ dbname_paths, dbname_paths + len}; MONAD_ASSERT(!paths.empty()); - // C ABI entry — runs in a foreign process; register the state machine - // factories so the kind-driven Db ctor can resolve `ethereum`. + // C ABI entry, runs in a foreign process; register the state machine + // factories so the kind-driven Db ctor can resolve the persisted kind. // Idempotent: re-registration overwrites the prior factory. register_ethereum_state_machines(); register_monad_state_machines(); return new monad_statesync_client_context{ - chain_config, paths, sq_thread_cpu == MONAD_SQPOLL_DISABLED ? std::nullopt @@ -171,13 +168,6 @@ bool monad_statesync_client_finalize(monad_statesync_client_context *const ctx) } auto const latest_version = ctx->db.get_latest_version(); - // The dual-write keeps both timelines in lockstep, so the secondary must be - // at the same version as the primary at finalize. - if (ctx->secondary_db) { - MONAD_ASSERT( - latest_version == ctx->secondary_db->get_latest_version(), - "dual-db versions should always be in sync"); - } MONAD_ASSERT(for_each_code( ctx->db, latest_version, [&](bytes32_t const &hash, byte_string_view) { @@ -188,77 +178,52 @@ bool monad_statesync_client_finalize(monad_statesync_client_context *const ctx) return false; } - // Roll one db forward from its synced version to the target, replaying the - // trailing block headers, then mark the target finalized. Applied to the - // primary and, in dual-db mode, the page-encoded secondary, so both - // timelines are version- and finalize-consistent at the target. - auto const roll_forward_and_finalize = - [ctx, &tgrt, latest_version](mpt::Db &db) -> bool { - if (latest_version != tgrt.number) { - db.move_trie_version_forward(latest_version, tgrt.number); - bytes32_t expected = tgrt.parent_hash; - for (size_t i = 0; i < std::min(tgrt.number, 256ul); ++i) { - auto const v = tgrt.number - i - 1; - auto const &hdr = ctx->hdrs[v % ctx->hdrs.size()]; - auto const rlp = rlp::encode_block_header(hdr); - auto const hash = to_bytes(keccak256(rlp)); - if (hash != expected) { - return false; - } - expected = hdr.parent_hash; - - Update block_header_update{ - .key = block_header_nibbles, - .value = rlp, - .incarnation = true, - .next = UpdateList{}, - .version = static_cast(v)}; - UpdateList updates; - updates.push_front(block_header_update); - Update finalized{ - .key = finalized_nibbles, - .value = byte_string_view{}, - .incarnation = false, - .next = std::move(updates), - .version = static_cast(v)}; - UpdateList finalized_updates; - finalized_updates.push_front(finalized); - db.upsert( - db.load_root_for_version(v), - std::move(finalized_updates), - v, - false, - false); + // Roll the db forward from its synced version to the target, replaying the + // trailing block headers, then mark the target finalized. + if (latest_version != tgrt.number) { + ctx->db.move_trie_version_forward(latest_version, tgrt.number); + bytes32_t expected = tgrt.parent_hash; + for (size_t i = 0; i < std::min(tgrt.number, 256ul); ++i) { + auto const v = tgrt.number - i - 1; + auto const &hdr = ctx->hdrs[v % ctx->hdrs.size()]; + auto const rlp = rlp::encode_block_header(hdr); + auto const hash = to_bytes(keccak256(rlp)); + if (hash != expected) { + return false; } + expected = hdr.parent_hash; + + Update block_header_update{ + .key = block_header_nibbles, + .value = rlp, + .incarnation = true, + .next = UpdateList{}, + .version = static_cast(v)}; + UpdateList updates; + updates.push_front(block_header_update); + Update finalized{ + .key = finalized_nibbles, + .value = byte_string_view{}, + .incarnation = false, + .next = std::move(updates), + .version = static_cast(v)}; + UpdateList finalized_updates; + finalized_updates.push_front(finalized); + ctx->db.upsert( + ctx->db.load_root_for_version(v), + std::move(finalized_updates), + v, + false, + false); } - db.update_finalized_version(tgrt.number); - return true; - }; - - if (!roll_forward_and_finalize(ctx->db)) { - return false; - } - if (ctx->secondary_db && !roll_forward_and_finalize(*ctx->secondary_db)) { - return false; } + ctx->db.update_finalized_version(tgrt.number); - // Pick the right TrieDb to read state_root from based on the target's - // revision. - auto const monad_rev = ctx->chain->get_monad_revision(tgrt.timestamp); - bool const page_encoded = mip_8_active(monad_rev); - TrieDb *db = &ctx->tdb; - if (page_encoded != db->is_page_encoded()) { - MONAD_ASSERT_PRINTF( - ctx->secondary_tdb && - ctx->secondary_tdb->is_page_encoded() == page_encoded, - "No client db timeline is %s-encoded as the target revision " - "requires", - page_encoded ? "page" : "slot"); - db = ctx->secondary_tdb.get(); - } - db->set_block_and_prefix(ctx->db.get_latest_finalized_version()); - MONAD_ASSERT(db->get_block_number() == tgrt.number); - return db->state_root() == tgrt.state_root; + // The client db is page-encoded, so a target from before MIP-8 fails + // this root comparison rather than being detected up front. + ctx->tdb.set_block_and_prefix(ctx->db.get_latest_finalized_version()); + MONAD_ASSERT(ctx->tdb.get_block_number() == tgrt.number); + return ctx->tdb.state_root() == tgrt.state_root; } void monad_statesync_client_context_destroy( diff --git a/category/statesync/statesync_client.h b/category/statesync/statesync_client.h index 0762946f8d..18bc7ad895 100644 --- a/category/statesync/statesync_client.h +++ b/category/statesync/statesync_client.h @@ -15,7 +15,6 @@ #pragma once -#include #include #ifdef __cplusplus @@ -28,10 +27,10 @@ extern unsigned const MONAD_SQPOLL_DISABLED; struct monad_statesync_client; struct monad_statesync_client_context; -// chain_config must be a Monad chain +// The db at dbname_paths must be a single page-encoded timeline. struct monad_statesync_client_context *monad_statesync_client_context_create( - enum monad_chain_config chain_config, char const *const *dbname_paths, - size_t len, unsigned sq_thread_cpu, struct monad_statesync_client *, + char const *const *dbname_paths, size_t len, unsigned sq_thread_cpu, + struct monad_statesync_client *, void (*statesync_send_request)( struct monad_statesync_client *, struct monad_sync_request)); diff --git a/category/statesync/statesync_client_context.cpp b/category/statesync/statesync_client_context.cpp index 37b7c4d487..4c5f86d710 100644 --- a/category/statesync/statesync_client_context.cpp +++ b/category/statesync/statesync_client_context.cpp @@ -19,7 +19,6 @@ #include #include #include -#include #include #include #include @@ -36,14 +35,12 @@ using namespace monad; using namespace monad::mpt; monad_statesync_client_context::monad_statesync_client_context( - monad_chain_config const chain_config, std::vector const dbname_paths, std::optional const sq_thread_cpu, unsigned const wr_buffers, monad_statesync_client *const sync, void (*statesync_send_request)( struct monad_statesync_client *, struct monad_sync_request)) - : chain{make_monad_chain(chain_config)} - , db{mpt::OnDiskDbConfig{ + : db{mpt::OnDiskDbConfig{ .append = true, .compaction = false, .rewind_to_latest_finalized = true, @@ -53,19 +50,6 @@ monad_statesync_client_context::monad_statesync_client_context( .sq_thread_cpu = sq_thread_cpu, .dbname_paths = dbname_paths}} , tdb{db} // open with latest finalized if valid, otherwise init as block 0 - , secondary_db{[this] { - if (db.timeline_active(timeline_id::secondary)) { - MONAD_ASSERT( - db.state_machine_type() == state_machine_kind::ethereum); - auto ret = db.open_secondary_timeline(); - MONAD_ASSERT(ret.has_value()); - MONAD_ASSERT( - ret->state_machine_type() == state_machine_kind::monad); - return std::make_unique(std::move(ret.value())); - } - return std::unique_ptr{}; - }()} - , secondary_tdb{secondary_db ? std::make_unique(*secondary_db) : nullptr} , progress( monad_statesync_client_prefixes(), {db.get_latest_version(), db.get_latest_version()}) @@ -77,69 +61,62 @@ monad_statesync_client_context::monad_statesync_client_context( , statesync_send_request{statesync_send_request} { MONAD_ASSERT(db.get_latest_version() == db.get_latest_finalized_version()); - MONAD_ASSERT((secondary_db != nullptr) == (secondary_tdb != nullptr)); - MONAD_ASSERT(secondary_tdb == nullptr || secondary_tdb->is_page_encoded()); + // Statesync writes a single page-encoded timeline. A slot-encoded db, or a + // db still carrying a secondary timeline from the dual-db migration, is + // rejected rather than synced into. + MONAD_ASSERT(tdb.is_page_encoded()); + MONAD_ASSERT(!db.timeline_active(timeline_id::secondary)); } void monad_statesync_client_context::prepare_current_state() { - // Roll forward `target_db`: upsert an empty finalized marker for the - // `current` version and carry the state + code subtries over from - // `latest_version`. - auto const roll_forward = [&](mpt::Db &target_db, auto &target_tdb) { - auto const latest_version = target_db.get_latest_version(); - UpdateList finalized_empty; - Update finalized{ - .key = finalized_nibbles, - .value = byte_string_view{}, - .incarnation = true, - .next = UpdateList{}, - .version = static_cast(current)}; - finalized_empty.push_front(finalized); - auto const src_root = target_db.load_root_for_version(latest_version); - bool write_root = false; - auto dest_root = target_db.upsert( - src_root, - std::move(finalized_empty), - current, - false, - false, - write_root); - MONAD_ASSERT( - target_db.find(dest_root, finalized_nibbles, current).has_value()); - - auto const state_key = concat(FINALIZED_NIBBLE, STATE_NIBBLE); - auto const code_key = concat(FINALIZED_NIBBLE, CODE_NIBBLE); - dest_root = target_db.copy_trie( - src_root, - state_key, - std::move(dest_root), - state_key, - current, - write_root); - write_root = true; - dest_root = target_db.copy_trie( - src_root, - code_key, - std::move(dest_root), - code_key, - current, - write_root); - auto const finalized_res = - target_db.find(dest_root, finalized_nibbles, current); - MONAD_ASSERT(finalized_res.has_value()); - MONAD_ASSERT(finalized_res.value().node->number_of_children() == 2); - MONAD_ASSERT(target_db.find(dest_root, state_key, current).has_value()); - MONAD_ASSERT(target_db.find(dest_root, code_key, current).has_value()); - MONAD_ASSERT(dest_root->value() == src_root->value()); - target_tdb.reset_root(dest_root, current); - MONAD_ASSERT(target_db.get_latest_version() == current); - }; - - roll_forward(db, tdb); - if (secondary_tdb) { - roll_forward(*secondary_db, *secondary_tdb); - } + // Roll the db forward: upsert an empty finalized marker for the `current` + // version and carry the state + code subtries over from `latest_version`. + auto const latest_version = db.get_latest_version(); + UpdateList finalized_empty; + Update finalized{ + .key = finalized_nibbles, + .value = byte_string_view{}, + .incarnation = true, + .next = UpdateList{}, + .version = static_cast(current)}; + finalized_empty.push_front(finalized); + auto const src_root = db.load_root_for_version(latest_version); + bool write_root = false; + auto dest_root = db.upsert( + src_root, + std::move(finalized_empty), + current, + false, + false, + write_root); + MONAD_ASSERT(db.find(dest_root, finalized_nibbles, current).has_value()); + + auto const state_key = concat(FINALIZED_NIBBLE, STATE_NIBBLE); + auto const code_key = concat(FINALIZED_NIBBLE, CODE_NIBBLE); + dest_root = db.copy_trie( + src_root, + state_key, + std::move(dest_root), + state_key, + current, + write_root); + write_root = true; + dest_root = db.copy_trie( + src_root, + code_key, + std::move(dest_root), + code_key, + current, + write_root); + auto const finalized_res = db.find(dest_root, finalized_nibbles, current); + MONAD_ASSERT(finalized_res.has_value()); + MONAD_ASSERT(finalized_res.value().node->number_of_children() == 2); + MONAD_ASSERT(db.find(dest_root, state_key, current).has_value()); + MONAD_ASSERT(db.find(dest_root, code_key, current).has_value()); + MONAD_ASSERT(dest_root->value() == src_root->value()); + tdb.reset_root(dest_root, current); + MONAD_ASSERT(db.get_latest_version() == current); } void monad_statesync_client_context::commit() @@ -149,47 +126,21 @@ void monad_statesync_client_context::commit() prepare_current_state(); } - // Build the encoded block header once; it's identical for both dbs. - auto const header_rlp = rlp::encode_block_header(tgrt); - - // Build a slot-encoded storage UpdateList for an account's deltas. - auto build_slot_storage = [this]( - StorageDeltas const &slot_deltas, - std::deque &alloc, - std::deque &bytes_alloc, - std::deque &hash_alloc) { - UpdateList storage; - for (auto const &[key, val] : slot_deltas) { - storage.push_front(alloc.emplace_back(Update{ - .key = hash_alloc.emplace_back(keccak256(key.bytes)), - .value = val == bytes32_t{} - ? std::nullopt - : std::make_optional( - bytes_alloc.emplace_back( - encode_storage_db(key, val))), - .incarnation = false, - .next = UpdateList{}, - .version = static_cast(current)})); - } - return storage; - }; - - // Build a page-encoded storage UpdateList for Db2. Slots are grouped by - // page_key and merged onto any existing page contents read from the - // secondary trie. A page that ends up empty becomes a delete on the - // page entry. - auto build_page_storage = [this]( - TrieDb &paged_db, - Address const &addr, - StorageDeltas const &slot_deltas, - std::deque &alloc, - std::deque &bytes_alloc, - std::deque &hash_alloc) { + // UpdateList is intrusive: every Update node and the bytes it points at + // must outlive the upsert below, so they are allocated in these deques. + std::deque alloc; + std::deque bytes_alloc; + std::deque hash_alloc; + + // Build the page-encoded storage UpdateList for one account's deltas. + // Slots are grouped by page_key and merged onto the page's current + // contents, read from the trie on first touch, so several slot writes at + // the same page_key compose into one update. A page that ends up empty + // becomes a delete on the page entry. Pages from one account never + // collide with another's, so no cross-account cache is needed. + auto const build_page_storage = [&](Address const &addr, + StorageDeltas const &slot_deltas) { UpdateList storage; - // Per-account page granularity: keyed by page_key, value is the - // mutable storage_page_t being merged. Each first-touch of a page - // reads the current page from the secondary trie so subsequent slot - // writes at the same page_key compose into one update. ankerl::unordered_dense::segmented_map< bytes32_t, storage_page_t, @@ -203,7 +154,7 @@ void monad_statesync_client_context::commit() // Incarnation isn't tracked in statesync deltas; TrieDb // ignores it for storage reads, so a fixed value is fine. it->second = - paged_db.read_storage_page(addr, Incarnation{0, 0}, pg_key); + tdb.read_storage_page(addr, Incarnation{0, 0}, pg_key); } it->second.set(slot_off, slot_val); } @@ -224,132 +175,73 @@ void monad_statesync_client_context::commit() return storage; }; - auto build_and_upsert = [this, &header_rlp]( - mpt::Db &target_db, - auto &target_tdb, - auto build_storage) { - std::deque alloc; - std::deque bytes_alloc; - std::deque hash_alloc; - - UpdateList accounts; - for (auto const &[addr, delta] : deltas) { - UpdateList storage; - std::optional value; - if (delta.has_value()) { - auto const &[acct, slot_deltas] = delta.value(); - value = bytes_alloc.emplace_back(encode_account_db(addr, acct)); - storage = build_storage( - addr, slot_deltas, alloc, bytes_alloc, hash_alloc); - } - accounts.push_front(alloc.emplace_back(Update{ - .key = hash_alloc.emplace_back(keccak256(addr.bytes)), - .value = value, - .incarnation = false, - .next = std::move(storage), - .version = static_cast(current)})); - } - UpdateList code_updates; - for (auto const &[hash, bytes] : code) { - code_updates.push_front(alloc.emplace_back(Update{ - .key = NibblesView{hash}, - .value = bytes, - .incarnation = false, - .next = UpdateList{}, - .version = static_cast(current)})); + UpdateList accounts; + for (auto const &[addr, delta] : deltas) { + UpdateList storage; + std::optional value; + if (delta.has_value()) { + auto const &[acct, slot_deltas] = delta.value(); + value = bytes_alloc.emplace_back(encode_account_db(addr, acct)); + storage = build_page_storage(addr, slot_deltas); } - - auto state_update = Update{ - .key = state_nibbles, - .value = byte_string_view{}, + accounts.push_front(alloc.emplace_back(Update{ + .key = hash_alloc.emplace_back(keccak256(addr.bytes)), + .value = value, .incarnation = false, - .next = std::move(accounts), - .version = static_cast(current)}; - auto code_update = Update{ - .key = code_nibbles, - .value = byte_string_view{}, + .next = std::move(storage), + .version = static_cast(current)})); + } + UpdateList code_updates; + for (auto const &[hash, bytes] : code) { + code_updates.push_front(alloc.emplace_back(Update{ + .key = NibblesView{hash}, + .value = bytes, .incarnation = false, - .next = std::move(code_updates), - .version = static_cast(current)}; - auto block_header_update = Update{ - .key = block_header_nibbles, - .value = header_rlp, - .incarnation = true, .next = UpdateList{}, - .version = static_cast(current)}; - UpdateList updates; - updates.push_front(state_update); - updates.push_front(code_update); - updates.push_front(block_header_update); - - UpdateList finalized_updates; - Update finalized{ - .key = finalized_nibbles, - .value = byte_string_view{}, - .incarnation = false, - .next = std::move(updates), - .version = static_cast(current)}; - finalized_updates.push_front(finalized); - - target_tdb.reset_root( - target_db.upsert( - target_tdb.get_root(), - std::move(finalized_updates), - current, - false, - false), - current); - }; - - // Primary Db - if (!tdb.is_page_encoded()) { - build_and_upsert( - db, - tdb, - [&](Address const &, - StorageDeltas const &slot_deltas, - std::deque &alloc, - std::deque &bytes_alloc, - std::deque &hash_alloc) { - return build_slot_storage( - slot_deltas, alloc, bytes_alloc, hash_alloc); - }); - } - else { - build_and_upsert( - db, - tdb, - [&](Address const &addr, - StorageDeltas const &slot_deltas, - std::deque &alloc, - std::deque &bytes_alloc, - std::deque &hash_alloc) { - return build_page_storage( - tdb, addr, slot_deltas, alloc, bytes_alloc, hash_alloc); - }); + .version = static_cast(current)})); } - // Secondary: page-encoded storage. Each per-account call builds its - // own page map; pages from one account never collide with another's, - // so no cross-account cache is needed. - if (secondary_tdb) { - build_and_upsert( - *secondary_db, - *secondary_tdb, - [&](Address const &addr, - StorageDeltas const &slot_deltas, - std::deque &alloc, - std::deque &bytes_alloc, - std::deque &hash_alloc) { - return build_page_storage( - *secondary_tdb, - addr, - slot_deltas, - alloc, - bytes_alloc, - hash_alloc); - }); - } + auto const header_rlp = rlp::encode_block_header(tgrt); + auto state_update = Update{ + .key = state_nibbles, + .value = byte_string_view{}, + .incarnation = false, + .next = std::move(accounts), + .version = static_cast(current)}; + auto code_update = Update{ + .key = code_nibbles, + .value = byte_string_view{}, + .incarnation = false, + .next = std::move(code_updates), + .version = static_cast(current)}; + auto block_header_update = Update{ + .key = block_header_nibbles, + .value = header_rlp, + .incarnation = true, + .next = UpdateList{}, + .version = static_cast(current)}; + UpdateList updates; + updates.push_front(state_update); + updates.push_front(code_update); + updates.push_front(block_header_update); + + UpdateList finalized_updates; + Update finalized{ + .key = finalized_nibbles, + .value = byte_string_view{}, + .incarnation = false, + .next = std::move(updates), + .version = static_cast(current)}; + finalized_updates.push_front(finalized); + + tdb.reset_root( + db.upsert( + tdb.get_root(), + std::move(finalized_updates), + current, + false, + false), + current); code.clear(); deltas.clear(); diff --git a/category/statesync/statesync_client_context.hpp b/category/statesync/statesync_client_context.hpp index f0a0b55ed1..4b400f5c82 100644 --- a/category/statesync/statesync_client_context.hpp +++ b/category/statesync/statesync_client_context.hpp @@ -18,11 +18,9 @@ #include #include #include -#include #include #include #include -#include #include #include @@ -47,16 +45,10 @@ struct monad_statesync_client_context using StateDelta = std::pair; - // Chain instance for revision lookups and determine whether the slot or - // page encoded db are canonical. - std::unique_ptr chain; - + // Single page-encoded timeline; asserted at construction. monad::mpt::Db db; monad::TrieDb tdb; - std::unique_ptr secondary_db; - std::unique_ptr secondary_tdb; - std::vector> progress; std::vector> protocol; std::array hdrs; @@ -72,7 +64,6 @@ struct monad_statesync_client_context struct monad_statesync_client *, struct monad_sync_request); monad_statesync_client_context( - monad_chain_config chain_config, std::vector dbname_paths, std::optional sq_thread_cpu, unsigned wr_buffers, monad_statesync_client *, diff --git a/category/statesync/statesync_server.cpp b/category/statesync/statesync_server.cpp index c2e031da37..af8708a42c 100644 --- a/category/statesync/statesync_server.cpp +++ b/category/statesync/statesync_server.cpp @@ -218,14 +218,6 @@ bool statesync_server_handle_request( { } - // When the server's primary is page-encoded, storage leaves hold - // encoded pages rather than single slots; we expand each page into - // slot-format upserts so v1 clients sync unchanged. - bool server_is_page_encoded() const - { - return sync->context->is_page_encoded(); - } - virtual bool down(unsigned char const branch, Node const &node) override { if (branch == INVALID_BRANCH) { @@ -270,15 +262,12 @@ bool statesync_server_handle_request( } if (node.has_value() && v <= until) { - auto const send_upsert = [&](monad_sync_type const type, - unsigned char const *const v1 = - nullptr, - uint64_t const size1 = 0) { - uint64_t const size2 = node.value().size(); + auto const send_upsert = [&](monad_sync_type const type) { + uint64_t const size = node.value().size(); sync->statesync_server_send_upsert( - sync->net, type, v1, size1, node.value().data(), size2); + sync->net, type, nullptr, 0, node.value().data(), size); ++(*num_upserts); - *upsert_bytes += size1 + size2; + *upsert_bytes += size; }; if (nibble == CODE_NIBBLE) { @@ -292,34 +281,25 @@ bool statesync_server_handle_request( } else { MONAD_ASSERT(depth == (HASH_SIZE * 2)); - if (server_is_page_encoded()) { - // Expand the page-encoded leaf into one slot-format - // upsert per non-zero slot, so the wire stays - // identical to a slot-encoded server. - auto const decoded = - decode_storage_page_leaf(node.value()); - MONAD_ASSERT(decoded.has_value()); - for (auto const [slot_key, slot_val] : - decoded.value().slots()) { - auto const entry = - encode_storage_db(slot_key, slot_val); - sync->statesync_server_send_upsert( - sync->net, - SYNC_TYPE_UPSERT_STORAGE, - reinterpret_cast( - &addr), - sizeof(addr), - entry.data(), - entry.size()); - ++(*num_upserts); - *upsert_bytes += sizeof(addr) + entry.size(); - } - } - else { - send_upsert( + // Storage leaves hold page-encoded pages. Expand each + // into one slot-format upsert per non-zero slot so + // the wire protocol stays slot-based. + auto const decoded = + decode_storage_page_leaf(node.value()); + MONAD_ASSERT(decoded.has_value()); + for (auto const [slot_key, slot_val] : + decoded.value().slots()) { + auto const entry = + encode_storage_db(slot_key, slot_val); + sync->statesync_server_send_upsert( + sync->net, SYNC_TYPE_UPSERT_STORAGE, - reinterpret_cast(&addr), - sizeof(addr)); + reinterpret_cast(&addr), + sizeof(addr), + entry.data(), + entry.size()); + ++(*num_upserts); + *upsert_bytes += sizeof(addr) + entry.size(); } } } diff --git a/category/statesync/statesync_server_context.cpp b/category/statesync/statesync_server_context.cpp index 6675a5a131..8609d95d0a 100644 --- a/category/statesync/statesync_server_context.cpp +++ b/category/statesync/statesync_server_context.cpp @@ -217,6 +217,9 @@ monad_statesync_server_context::monad_statesync_server_context(TrieDb &rw) : rw{rw} , ro{nullptr} { + // The server traversal expands page leaves into slot-format upserts and + // has no slot-encoded path. + MONAD_ASSERT(rw.is_page_encoded()); } bool monad_statesync_server_context::is_page_encoded() const diff --git a/category/statesync/test/fuzz_statesync.cpp b/category/statesync/test/fuzz_statesync.cpp index 6eac17121c..c27c4dbc99 100644 --- a/category/statesync/test/fuzz_statesync.cpp +++ b/category/statesync/test/fuzz_statesync.cpp @@ -255,15 +255,7 @@ namespace update_storage(deltas, state, db, n, true); } - std::unique_ptr make_on_disk_machine(bool const page_encoded) - { - if (page_encoded) { - return std::make_unique(); - } - return std::make_unique(); - } - - std::filesystem::path tmp_dbname(bool const page_encoded) + std::filesystem::path tmp_dbname() { std::filesystem::path dbname( MONAD_ASYNC_NAMESPACE::working_temporary_directory() / @@ -276,35 +268,30 @@ namespace ::close(fd); char const *const path = dbname.c_str(); mpt::Db db{ - make_on_disk_machine(page_encoded), + std::make_unique(), mpt::OnDiskDbConfig{.append = false, .dbname_paths = {path}}}; monad::mpt::test::DbAccessor::aux(db) .metadata_ctx() .set_state_machine_kind( - timeline_id::primary, - page_encoded ? state_machine_kind::monad - : state_machine_kind::ethereum); + timeline_id::primary, state_machine_kind::monad); return dbname; } - void run_fuzz( - monad_chain_config const chain, bool const page_encoded, - std::span raw) + void run_fuzz(std::span raw) { - std::filesystem::path const cdbname{tmp_dbname(page_encoded)}; + std::filesystem::path const cdbname{tmp_dbname()}; char const *const cdbname_str = cdbname.c_str(); monad_statesync_client client; monad_statesync_client_context *const cctx = monad_statesync_client_context_create( - chain, &cdbname_str, 1, static_cast(get_nprocs() - 1), &client, &statesync_send_request); - std::filesystem::path sdbname{tmp_dbname(page_encoded)}; + std::filesystem::path sdbname{tmp_dbname()}; mpt::Db sdb{ - make_on_disk_machine(page_encoded), + std::make_unique(), OnDiskDbConfig{.append = true, .dbname_paths = {sdbname}}}; TrieDb stdb{sdb}; std::unique_ptr sctx = @@ -411,11 +398,7 @@ LLVMFuzzerTestOneInput(uint8_t const *const data, size_t const size) init_root_logger(quill::LogLevel::Error); - // Fuzz both encodings each input, until slot encoding is retired: - // MONAD_TESTNET is pre-mip_8 (slot), MONAD_DEVNET is mip_8-active (page). - std::span const raw{data, size}; - run_fuzz(CHAIN_CONFIG_MONAD_TESTNET, false, raw); - run_fuzz(CHAIN_CONFIG_MONAD_DEVNET, true, raw); + run_fuzz(std::span{data, size}); return 0; } diff --git a/category/statesync/test/test_network_shutdown.cpp b/category/statesync/test/test_network_shutdown.cpp index f92a0ff4b2..70b455db67 100644 --- a/category/statesync/test/test_network_shutdown.cpp +++ b/category/statesync/test/test_network_shutdown.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -69,7 +70,7 @@ namespace // Initialize the on-disk DB format; the Db object is not needed // after this point. (void)monad::mpt::Db{ - std::make_unique(), + std::make_unique(), monad::mpt::OnDiskDbConfig{ .append = false, .dbname_paths = {path}}}; } diff --git a/category/statesync/test/test_statesync.cpp b/category/statesync/test/test_statesync.cpp index 7735f9097c..6f70309f52 100644 --- a/category/statesync/test/test_statesync.cpp +++ b/category/statesync/test/test_statesync.cpp @@ -33,8 +33,6 @@ #include #include #include -#include -#include #include #include #include @@ -47,9 +45,6 @@ #include #include #include -#include -#include -#include #include #include @@ -67,7 +62,7 @@ namespace monad::mpt::test { // Friend-of-Db accessor: lets the temp-db helper stamp the persisted // state_machine_kind on a fresh pool, simulating what monad-mpt --create - // --state-machine ethereum does in production. + // --state-machine monad does in production. struct DbAccessor { static UpdateAux &aux(Db &db) @@ -106,16 +101,16 @@ namespace ::ftruncate(fd, static_cast(8ULL * 1024 * 1024 * 1024))); ::close(fd); char const *const path = dbname.c_str(); - // Stamp the kind so a later open via Db(OnDiskDbConfig const&) — which - // monad_statesync_client_context uses internally — finds a valid kind. + // Stamp the kind so a later open via Db(OnDiskDbConfig const&), which + // monad_statesync_client_context uses internally, finds a valid kind. mpt::Db db{ - std::make_unique(), + std::make_unique(), mpt::OnDiskDbConfig{ .append = false, .dbname_paths = {path}, .chunk_capacity = 24}}; monad::mpt::test::DbAccessor::aux(db) .metadata_ctx() .set_state_machine_kind( - timeline_id::primary, state_machine_kind::ethereum); + timeline_id::primary, state_machine_kind::monad); return dbname; } @@ -175,34 +170,9 @@ namespace } } - // single-timeline server, dual-timeline client - template - struct StateSyncFixtureT : public ::testing::Test + // Page-encoded server and client, each a single timeline. + struct StateSyncFixture : public ::testing::Test { - static std::unique_ptr make_server_machine() - { - if constexpr (MIP_8_ACTIVE) { // if mip_8_is_active, we make server - // primary db page encoded - return std::make_unique(); - } - else { - return std::make_unique(); - } - } - - struct RevisionConfig - { - monad_chain_config chain_config; - uint64_t timestamp; - }; - - // This is THE single place to update when a chain's mip8 activation - // timestamp changes or MONAD_NEXT is promoted to a concrete revision. - // Pre-mip8: testnet at timestamp 0 maps to MONAD_ZERO (slot canonical). - static constexpr RevisionConfig PRE_MIP8{CHAIN_CONFIG_MONAD_TESTNET, 0}; - // Post-mip8: devnet currently maps to MONAD_NEXT for any timestamp - static constexpr RevisionConfig POST_MIP8{CHAIN_CONFIG_MONAD_DEVNET, 0}; - std::filesystem::path cdbname; monad_statesync_client client; monad_statesync_client_context *cctx; @@ -212,17 +182,14 @@ namespace monad_statesync_server_context sctx; mpt::AsyncIOContext io_ctx; mpt::Db ro; - RevisionConfig const revision_config; monad_statesync_server_network net; monad_statesync_server *server{}; - static constexpr bool mip_8_is_active = MIP_8_ACTIVE; - - StateSyncFixtureT() + StateSyncFixture() : cdbname{tmp_dbname()} , cctx{nullptr} , sdbname{tmp_dbname()} - , sdb{make_server_machine(), + , sdb{std::make_unique(), OnDiskDbConfig{ .append = true, .dbname_paths = {sdbname}, @@ -231,22 +198,9 @@ namespace , sctx{stdb} , io_ctx{mpt::ReadOnlyOnDiskDbConfig{.dbname_paths = {sdbname}}} , ro{io_ctx} - , revision_config{MIP_8_ACTIVE ? POST_MIP8 : PRE_MIP8} { - MONAD_ASSERT(MIP_8_ACTIVE == stdb.is_page_encoded()); + MONAD_ASSERT(stdb.is_page_encoded()); sctx.ro = &ro; - // The client context now requires a secondary timeline to - // already be active on the client db. - mpt::Db primary{ - std::make_unique(), - OnDiskDbConfig{ - .append = true, - .dbname_paths = {cdbname}, - .chunk_capacity = 24}}; - [[maybe_unused]] mpt::Db secondary = - primary.activate_secondary_timeline( - std::make_unique()); - MONAD_ASSERT(primary.timeline_active(mpt::timeline_id::secondary)); } void init() @@ -257,7 +211,6 @@ namespace monad::register_ethereum_state_machines(); monad::register_monad_state_machines(); cctx = new monad_statesync_client_context{ - revision_config.chain_config, {cdbname}, std::make_optional(static_cast(get_nprocs() - 1)), 4, @@ -283,13 +236,7 @@ namespace } } - monad_revision get_monad_revision() const - { - return make_monad_chain(revision_config.chain_config) - ->get_monad_revision(revision_config.timestamp); - } - - ~StateSyncFixtureT() + ~StateSyncFixture() { monad_statesync_client_context_destroy(cctx); monad_statesync_server_destroy(server); @@ -297,176 +244,51 @@ namespace std::filesystem::remove(sdbname); } }; - - // Slot-encoded server primary for pre-mip8 fork. - using StateSyncFixture = StateSyncFixtureT; - // Page-encoded server primary for post-mip8 fork - using PageServerStateSyncFixture = StateSyncFixtureT; - - template - struct StateSyncTestBothForks : public TFixture - { - }; - - using StateSyncTestTypes = - ::testing::Types; - TYPED_TEST_SUITE(StateSyncTestBothForks, StateSyncTestTypes); - - // Trait-aware dispatch wrapper for commit_block - void commit_block_dispatch( - monad::Db &primary, monad::Db *const secondary, - monad_revision const rev, bytes32_t const &block_id, - BlockHeader const &header, StateDeltas const &deltas, - BlockCommitAncillaries const &anc) - { - SWITCH_MONAD_TRAITS( - commit_block, primary, secondary, block_id, header, deltas, anc); - MONAD_ASSERT(false); - } - - // Commit one block proposal through the production commit_block path, which - // stamps the canonical state_root (slot pre-mip8, page post-mip8) into the - // header(s) - bytes32_t commit_simple_revision_aware( - monad::Db &primary, monad::Db *const secondary, - monad_revision const rev, StateDeltas const &deltas, Code const &code, - BlockHeader const &header, std::vector const &receipts = {}, - std::vector> const &call_frames = {}, - std::vector
const &senders = {}, - std::vector const &txns = {}, - std::vector const &ommers = {}, - std::optional> const &withdrawals = - std::nullopt) - { - bytes32_t const block_id = - header.number ? bytes32_t{header.number} : NULL_HASH_BLAKE3; - BlockCommitAncillaries const anc{ - .code = code, - .receipts = receipts, - .transactions = txns, - .senders = senders, - .call_frames = call_frames, - .ommers = ommers, - .withdrawals = withdrawals}; - commit_block_dispatch( - primary, secondary, rev, block_id, header, deltas, anc); - return block_id; - } - - // Like commit_simple_revision_aware, then finalize and reposition both - // timelines (the finalized-block analogue of commit_sequential). - void commit_sequential_revision_aware( - monad::Db &primary, monad::Db *const secondary, - monad_revision const rev, StateDeltas const &deltas, Code const &code, - BlockHeader const &header, std::vector const &receipts = {}, - std::vector> const &call_frames = {}, - std::vector
const &senders = {}, - std::vector const &txns = {}, - std::vector const &ommers = {}, - std::optional> const &withdrawals = - std::nullopt) - { - bytes32_t const block_id = commit_simple_revision_aware( - primary, - secondary, - rev, - deltas, - code, - header, - receipts, - call_frames, - senders, - txns, - ommers, - withdrawals); - primary.finalize(header.number, block_id); - primary.set_block_and_prefix(header.number); - if (secondary != nullptr) { - secondary->finalize(header.number, block_id); - secondary->set_block_and_prefix(header.number); - } - } } -// single timeline server -> slot and page-encoded dual db client -TYPED_TEST(StateSyncTestBothForks, sync_from_latest) +TEST_F(StateSyncFixture, sync_from_latest) { constexpr auto N = 1'000'000; bytes32_t parent_hash{NULL_HASH}; { mpt::Db db{ - std::make_unique(), + std::make_unique(), OnDiskDbConfig{ .append = true, - .dbname_paths = {this->cdbname}, + .dbname_paths = {cdbname}, .chunk_capacity = 24}}; TrieDb tdb{db}; - // In dual-db set up, both primary and secondary should stay in - // lockstep. - { - auto db2 = db.open_secondary_timeline( - std::make_unique()); - MONAD_ASSERT(db2.has_value()); - TrieDb tdb2{*db2}; - ASSERT_TRUE(tdb2.is_page_encoded()); - uint64_t const block_number = N - 257; - load_header( - db.load_root_for_version(block_number), - db, - BlockHeader{.number = block_number}); - load_header( - db2->load_root_for_version(block_number), - *db2, - BlockHeader{.number = block_number}); - for (size_t i = N - 256; i < N; ++i) { - BlockHeader const hdr{.parent_hash = parent_hash, .number = i}; - tdb.set_block_and_prefix(i - 1); - tdb2.set_block_and_prefix(i - 1); - // Pre-fork (slot canonical): dual-write so both client dbs' - // headers carry the same slot state_root. - commit_sequential_revision_aware( - tdb, - &tdb2, - this->get_monad_revision(), - StateDeltas({}), - {}, - hdr); - parent_hash = to_bytes( - keccak256(rlp::encode_block_header(tdb.read_eth_header()))); - } - // Block N: dual-write so both client dbs carry the canonical - // header - commit_sequential_revision_aware( - tdb, - &tdb2, - this->get_monad_revision(), - init_deltas(), - init_code(), - BlockHeader{.number = N}); - // a pending proposal at N+1 (not finalized) - commit_simple_revision_aware( - tdb, - &tdb2, - this->get_monad_revision(), - StateDeltas({}), - {}, - BlockHeader{.number = N + 1}); + uint64_t const block_number = N - 257; + load_header( + db.load_root_for_version(block_number), + db, + BlockHeader{.number = block_number}); + for (size_t i = N - 256; i < N; ++i) { + BlockHeader const hdr{.parent_hash = parent_hash, .number = i}; + tdb.set_block_and_prefix(i - 1); + commit_sequential(tdb, StateDeltas({}), {}, hdr); + parent_hash = to_bytes( + keccak256(rlp::encode_block_header(tdb.read_eth_header()))); } - this->init(); + load_db(tdb, N); + // a pending proposal at N+1 (not finalized) + commit_simple( + tdb, + StateDeltas({}), + {}, + bytes32_t{N + 1}, + BlockHeader{.number = N + 1}); + init(); } - // Canonical root of the load_db state: slot-encoded pre-mip8, page-encoded - // post-mip8. handle_target( - this->cctx, + cctx, BlockHeader{ .parent_hash = parent_hash, .state_root = - this->mip_8_is_active - ? 0x3438aff12a8d7d87cfae57d462e250c2dd03b5b06a5fa50a2eb3c8d397877e79_bytes32 - : 0xb9eda41f4a719d9f2ae332e3954de18bceeeba2248a44110878949384b184888_bytes32, + 0x3438aff12a8d7d87cfae57d462e250c2dd03b5b06a5fa50a2eb3c8d397877e79_bytes32, .number = N}); - EXPECT_TRUE(monad_statesync_client_has_reached_target(this->cctx)); - EXPECT_TRUE(monad_statesync_client_finalize(this->cctx)); + EXPECT_TRUE(monad_statesync_client_has_reached_target(cctx)); + EXPECT_TRUE(monad_statesync_client_finalize(cctx)); } TEST_F(StateSyncFixture, sync_from_empty) @@ -495,7 +317,7 @@ TEST_F(StateSyncFixture, sync_from_empty) BlockHeader const tgrt{ .parent_hash = parent_hash, .state_root = - 0xb9eda41f4a719d9f2ae332e3954de18bceeeba2248a44110878949384b184888_bytes32, + 0x3438aff12a8d7d87cfae57d462e250c2dd03b5b06a5fa50a2eb3c8d397877e79_bytes32, .number = N}; handle_target(cctx, tgrt); run(); @@ -503,7 +325,7 @@ TEST_F(StateSyncFixture, sync_from_empty) EXPECT_TRUE(monad_statesync_client_finalize(cctx)); mpt::Db cdb{ - std::make_unique(), + std::make_unique(), mpt::OnDiskDbConfig{ .append = true, .dbname_paths = {cdbname}, .chunk_capacity = 24}}; TrieDb ctdb{cdb}; @@ -534,24 +356,17 @@ TEST_F(StateSyncFixture, sync_from_empty) EXPECT_EQ(hdr.value(), tgrt); } -// single timeline server -> slot and page-encoded dual db client -TYPED_TEST(StateSyncTestBothForks, sync_from_some) +TEST_F(StateSyncFixture, sync_from_some) { { mpt::Db db{ - std::make_unique(), + std::make_unique(), OnDiskDbConfig{ .append = true, - .dbname_paths = {this->cdbname}, + .dbname_paths = {cdbname}, .chunk_capacity = 24}}; TrieDb tdb{db}; - auto db2_opt = - db.open_secondary_timeline(std::make_unique()); - MONAD_ASSERT(db2_opt.has_value()); - TrieDb tdb2{db2_opt.value()}; - ASSERT_TRUE(tdb2.is_page_encoded()); load_genesis_state(GENESIS_STATE, tdb); - load_genesis_state(GENESIS_STATE, tdb2); // commit some proposal to client db commit_simple( tdb, @@ -559,36 +374,26 @@ TYPED_TEST(StateSyncTestBothForks, sync_from_some) {}, NULL_HASH_BLAKE3, BlockHeader{.number = 1}); - commit_simple( - tdb2, - StateDeltas({}), - {}, - NULL_HASH_BLAKE3, - BlockHeader{.number = 1}); - EXPECT_TRUE(db2_opt->load_root_for_version(0) != nullptr); - load_genesis_state(GENESIS_STATE, this->stdb); - this->init(); + load_genesis_state(GENESIS_STATE, stdb); + init(); } - ASSERT_TRUE(this->stdb.get_root() != nullptr); - auto const res = this->sdb.find( - this->stdb.get_root(), concat(FINALIZED_NIBBLE, BLOCKHEADER_NIBBLE), 0); + ASSERT_TRUE(stdb.get_root() != nullptr); + auto const res = sdb.find( + stdb.get_root(), concat(FINALIZED_NIBBLE, BLOCKHEADER_NIBBLE), 0); ASSERT_TRUE(res.has_value() && res.value().is_valid()); - // Commit a server block, then capture its committed header. The committed - // state_root is slot pre-mip8 / page post-mip8. + // Commit a server block, then capture its committed header. bytes32_t parent_hash = to_bytes(keccak256(res.value().node->value())); auto const commit_server_block_update_parent_hash = [&](StateDeltas const &deltas, Code const &code, uint64_t const number) { - commit_sequential_revision_aware( - this->sctx, - nullptr, - this->get_monad_revision(), + commit_sequential( + sctx, deltas, code, BlockHeader{.parent_hash = parent_hash, .number = number}); - BlockHeader const committed = this->stdb.read_eth_header(); + BlockHeader const committed = stdb.read_eth_header(); parent_hash = to_bytes(keccak256(rlp::encode_block_header(committed))); return committed; @@ -601,13 +406,13 @@ TYPED_TEST(StateSyncTestBothForks, sync_from_some) constexpr auto ADDR3 = 0x5353535353535353535353535353535353535353_address; // delete existing account ADDR1 - auto const acct1 = this->stdb.read_account(ADDR1); + auto const acct1 = stdb.read_account(ADDR1); MONAD_ASSERT(acct1.has_value()); auto const hdr1 = commit_server_block_update_parent_hash( StateDeltas({{ADDR1, {.account = {acct1, std::nullopt}}}}), Code{}, 1); // new storage to existing account ADDR2 - auto acct2 = this->stdb.read_account(ADDR2); + auto acct2 = stdb.read_account(ADDR2); auto const hdr2 = commit_server_block_update_parent_hash( StateDeltas( {{ADDR2, @@ -645,7 +450,7 @@ TYPED_TEST(StateSyncTestBothForks, sync_from_some) 3); // delete storage in account ADDR2 - acct2 = this->stdb.read_account(ADDR2); + acct2 = stdb.read_account(ADDR2); auto const hdr4 = commit_server_block_update_parent_hash( StateDeltas( {{ADDR2, @@ -658,7 +463,7 @@ TYPED_TEST(StateSyncTestBothForks, sync_from_some) 4); // account incarnation for ADDR2 - auto const old = this->stdb.read_account(ADDR2); + auto const old = stdb.read_account(ADDR2); acct2 = old; acct2->incarnation = Incarnation{5, 0}; auto const hdr5 = commit_server_block_update_parent_hash( @@ -673,33 +478,33 @@ TYPED_TEST(StateSyncTestBothForks, sync_from_some) 5); // delete smart contract at ADDR3 - auto const acct3 = this->stdb.read_account(ADDR3); + auto const acct3 = stdb.read_account(ADDR3); MONAD_ASSERT(acct3.has_value()); auto const hdr6 = commit_server_block_update_parent_hash( StateDeltas({{ADDR3, {.account = {acct3, std::nullopt}}}}), Code{}, 6); - handle_target(this->cctx, hdr1); - this->run(); + handle_target(cctx, hdr1); + run(); - handle_target(this->cctx, hdr2); - this->run(); + handle_target(cctx, hdr2); + run(); - handle_target(this->cctx, hdr3); - this->run(); + handle_target(cctx, hdr3); + run(); - handle_target(this->cctx, hdr4); - this->run(); + handle_target(cctx, hdr4); + run(); - handle_target(this->cctx, hdr5); - this->run(); + handle_target(cctx, hdr5); + run(); - handle_target(this->cctx, hdr6); - this->run(); + handle_target(cctx, hdr6); + run(); - EXPECT_TRUE(monad_statesync_client_finalize(this->cctx)); + EXPECT_TRUE(monad_statesync_client_finalize(cctx)); // find transaction trie - mpt::RODb cdb{ReadOnlyOnDiskDbConfig{.dbname_paths = {this->cdbname}}}; + mpt::RODb cdb{ReadOnlyOnDiskDbConfig{.dbname_paths = {cdbname}}}; for (auto const nibble : {RECEIPT_NIBBLE, TRANSACTION_NIBBLE, @@ -713,41 +518,35 @@ TYPED_TEST(StateSyncTestBothForks, sync_from_some) } } -TYPED_TEST(StateSyncTestBothForks, deletion_proposal) +TEST_F(StateSyncFixture, deletion_proposal) { { mpt::Db db{ - std::make_unique(), + std::make_unique(), OnDiskDbConfig{ .append = true, - .dbname_paths = {this->cdbname}, + .dbname_paths = {cdbname}, .chunk_capacity = 24}}; TrieDb tdb{db}; - auto db2_opt = - db.open_secondary_timeline(std::make_unique()); - MONAD_ASSERT(db2_opt.has_value()); - TrieDb tdb2{db2_opt.value()}; - ASSERT_TRUE(tdb2.is_page_encoded()); load_genesis_state(GENESIS_STATE, tdb); - load_genesis_state(GENESIS_STATE, tdb2); - load_genesis_state(GENESIS_STATE, this->stdb); - this->init(); + load_genesis_state(GENESIS_STATE, stdb); + init(); } - ASSERT_TRUE(this->stdb.get_root() != nullptr); - auto const res = this->sdb.find( - this->stdb.get_root(), concat(FINALIZED_NIBBLE, BLOCKHEADER_NIBBLE), 0); + ASSERT_TRUE(stdb.get_root() != nullptr); + auto const res = sdb.find( + stdb.get_root(), concat(FINALIZED_NIBBLE, BLOCKHEADER_NIBBLE), 0); ASSERT_TRUE(res.has_value() && res.value().is_valid()); // delete ADDR1 on one fork { constexpr auto ADDR1 = 0x000d836201318ec6899a67540690382780743280_address; - auto const acct = this->sctx.read_account(ADDR1); + auto const acct = sctx.read_account(ADDR1); ASSERT_TRUE(acct.has_value()); StateDeltas deltas{{ADDR1, {.account = {acct, std::nullopt}}}}; - this->sctx.set_block_and_prefix(0); + sctx.set_block_and_prefix(0); commit_simple( - this->sctx, + sctx, StateDeltas(std::move(deltas)), Code{}, bytes32_t{1}, @@ -757,30 +556,30 @@ TYPED_TEST(StateSyncTestBothForks, deletion_proposal) { constexpr auto ADDR2 = 0x001762430ea9c3a26e5749afdb70da5f78ddbb8c_address; - auto const acct = this->sctx.read_account(ADDR2); + auto const acct = sctx.read_account(ADDR2); ASSERT_TRUE(acct.has_value()); StateDeltas deltas{{ADDR2, {.account = {acct, std::nullopt}}}}; - this->sctx.set_block_and_prefix(0); + sctx.set_block_and_prefix(0); commit_simple( - this->sctx, + sctx, StateDeltas(std::move(deltas)), Code{}, bytes32_t{2}, BlockHeader{.number = 1}); } - this->sctx.finalize(1, bytes32_t{2}); + sctx.finalize(1, bytes32_t{2}); - this->sctx.set_block_and_prefix(1, bytes32_t{1}); - auto const bad_header = this->sctx.read_eth_header(); + sctx.set_block_and_prefix(1, bytes32_t{1}); + auto const bad_header = sctx.read_eth_header(); - this->sctx.set_block_and_prefix(1, bytes32_t{2}); - auto const finalized_header = this->sctx.read_eth_header(); + sctx.set_block_and_prefix(1, bytes32_t{2}); + auto const finalized_header = sctx.read_eth_header(); EXPECT_NE(finalized_header.state_root, bad_header.state_root); - handle_target(this->cctx, finalized_header); - this->run(); + handle_target(cctx, finalized_header); + run(); - EXPECT_TRUE(monad_statesync_client_finalize(this->cctx)); + EXPECT_TRUE(monad_statesync_client_finalize(cctx)); } TEST_F(StateSyncFixture, sync_one_account) @@ -821,127 +620,11 @@ TEST_F(StateSyncFixture, sync_one_account) EXPECT_TRUE(monad_statesync_client_finalize(cctx)); } -// Pre-fork dual-timeline server -> dual-timeline client -TEST_F(StateSyncFixture, pre_fork_dual_timeline_server_to_dual_db_client) +// Multi-slot pages: the server expands each page leaf into one slot-format +// upsert per non-zero slot, and the client packs them back into pages. +TEST_F(StateSyncFixture, sync_multi_slot_pages) { - mpt::Db secondary_sdb = - sdb.activate_secondary_timeline(std::make_unique()); - TrieDb secondary_stdb{secondary_sdb}; - ASSERT_TRUE(secondary_stdb.is_page_encoded()); - init(); - uint64_t const timestamp = revision_config.timestamp; - monad_revision const rev = cctx->chain->get_monad_revision(timestamp); - - // ADDR_A holds five slots: three share one page_key, two share another, so - // the page secondary holds genuine multi-slot pages. The server dual-writes - // every block to both its slot primary and page secondary via commit_block; - // the client in turn receives slot-encoded upserts and dual-writes them to - // its own slot Db1 and page Db2. The test compares the two page tries at - // the end. - constexpr auto N = 1'000'000; - bytes32_t parent_hash{NULL_HASH}; - load_header( - sdb.load_root_for_version(N - 257), - sdb, - BlockHeader{.number = N - 257}); - load_header( - secondary_sdb.load_root_for_version(N - 257), - secondary_sdb, - BlockHeader{.number = N - 257}); - for (size_t i = N - 256; i < N; ++i) { - stdb.set_block_and_prefix(i - 1); - secondary_stdb.set_block_and_prefix(i - 1); - commit_sequential_revision_aware( - stdb, - &secondary_stdb, - rev, - {}, - Code{}, - BlockHeader{ - .parent_hash = parent_hash, - .number = i, - .timestamp = timestamp}); - EXPECT_EQ( - stdb.read_eth_header().state_root, - secondary_stdb.read_eth_header().state_root); - parent_hash = to_bytes( - keccak256(rlp::encode_block_header(stdb.read_eth_header()))); - } - - // Slots 0x00, 0x01, 0x7f all map to page_key 0 (low 7 bits are offset). - // Slots 0x80, 0x81 map to page_key 1. - constexpr auto slot_a = bytes32_t{uint64_t{0x00}}; - constexpr auto slot_b = bytes32_t{uint64_t{0x01}}; - constexpr auto slot_c = bytes32_t{uint64_t{0x7f}}; - constexpr auto slot_d = bytes32_t{uint64_t{0x80}}; - constexpr auto slot_e = bytes32_t{uint64_t{0x81}}; - constexpr auto val_a = - 0x00000000000000000000000000000000000000000000000000000000000000aa_bytes32; - constexpr auto val_b = - 0x00000000000000000000000000000000000000000000000000000000000000bb_bytes32; - constexpr auto val_c = - 0x00000000000000000000000000000000000000000000000000000000000000cc_bytes32; - constexpr auto val_d = - 0x00000000000000000000000000000000000000000000000000000000000000dd_bytes32; - constexpr auto val_e = - 0x00000000000000000000000000000000000000000000000000000000000000ee_bytes32; - - ASSERT_EQ(compute_page_key(slot_a), compute_page_key(slot_b)); - ASSERT_EQ(compute_page_key(slot_a), compute_page_key(slot_c)); - ASSERT_EQ(compute_page_key(slot_d), compute_page_key(slot_e)); - ASSERT_NE(compute_page_key(slot_a), compute_page_key(slot_d)); - - StateDeltas const storage_deltas{ - {ADDR_A, - StateDelta{ - .account = {std::nullopt, Account{.balance = 100}}, - .storage = { - {slot_a, {bytes32_t{}, val_a}}, - {slot_b, {bytes32_t{}, val_b}}, - {slot_c, {bytes32_t{}, val_c}}, - {slot_d, {bytes32_t{}, val_d}}, - {slot_e, {bytes32_t{}, val_e}}}}}}; - - // Dual-write the state block to both timelines - commit_sequential_revision_aware( - stdb, - &secondary_stdb, - rev, - storage_deltas, - Code{}, - BlockHeader{.number = N, .timestamp = timestamp}); - - handle_target( - cctx, - BlockHeader{ - .parent_hash = parent_hash, - .state_root = stdb.read_eth_header().state_root, - .number = N, - .timestamp = timestamp}); - run(); - - EXPECT_TRUE(monad_statesync_client_finalize(cctx)); - - // both timelines db state root should match - cctx->tdb.set_block_and_prefix(N); - stdb.set_block_and_prefix(N); - EXPECT_EQ(stdb.state_root(), cctx->tdb.state_root()); - - cctx->secondary_tdb->set_block_and_prefix(N); - secondary_stdb.set_block_and_prefix(N); - EXPECT_EQ(secondary_stdb.state_root(), cctx->secondary_tdb->state_root()); -} - -TEST_F( - PageServerStateSyncFixture, post_fork_sync_page_primary_to_dual_db_client) -{ - ASSERT_TRUE(this->stdb.is_page_encoded()); - - this->init(); - uint64_t const timestamp = this->revision_config.timestamp; - monad_revision const rev = this->cctx->chain->get_monad_revision(timestamp); - ASSERT_TRUE(mip_8_active(rev)); // ADDR_A holds five slots: 0x00/0x01/0x7f share one page, 0x80/0x81 a // second page, so the server holds genuine multi-slot pages to expand. @@ -974,16 +657,11 @@ TEST_F( BlockHeader{.number = N - 257}); for (size_t i = N - 256; i < N; ++i) { stdb.set_block_and_prefix(i - 1); - commit_sequential_revision_aware( + commit_sequential( stdb, - nullptr, - rev, - {}, + StateDeltas({}), Code{}, - BlockHeader{ - .parent_hash = parent_hash, - .number = i, - .timestamp = timestamp}); + BlockHeader{.parent_hash = parent_hash, .number = i}); parent_hash = to_bytes( keccak256(rlp::encode_block_header(stdb.read_eth_header()))); } @@ -999,30 +677,22 @@ TEST_F( {slot_d, {bytes32_t{}, val_d}}, {slot_e, {bytes32_t{}, val_e}}}}}}; - commit_sequential_revision_aware( - stdb, - nullptr, - rev, - storage_deltas, - Code{}, - BlockHeader{.number = N, .timestamp = timestamp}); + commit_sequential(stdb, storage_deltas, Code{}, BlockHeader{.number = N}); handle_target( cctx, BlockHeader{ .parent_hash = parent_hash, .state_root = stdb.state_root(), - .number = N, - .timestamp = timestamp}); + .number = N}); run(); EXPECT_TRUE(monad_statesync_client_finalize(cctx)); - // Post-fork the client's page secondary must match the page-encoded server - // primary. + // The client's page trie must match the server's. stdb.set_block_and_prefix(N); - cctx->secondary_tdb->set_block_and_prefix(N); - EXPECT_EQ(stdb.state_root(), cctx->secondary_tdb->state_root()); + cctx->tdb.set_block_and_prefix(N); + EXPECT_EQ(stdb.state_root(), cctx->tdb.state_root()); } TEST_F(StateSyncFixture, sync_empty) @@ -1056,7 +726,7 @@ TEST_F(StateSyncFixture, sync_client_has_proposals) { // init client DB mpt::Db db{ - std::make_unique(), + std::make_unique(), OnDiskDbConfig{ .append = true, .dbname_paths = {cdbname}, @@ -1094,7 +764,7 @@ TEST_F(StateSyncFixture, sync_client_has_proposals) BlockHeader const tgrt{ .parent_hash = parent_hash, .state_root = - 0xb9eda41f4a719d9f2ae332e3954de18bceeeba2248a44110878949384b184888_bytes32, + 0x3438aff12a8d7d87cfae57d462e250c2dd03b5b06a5fa50a2eb3c8d397877e79_bytes32, .number = N}; handle_target(cctx, tgrt); run(); @@ -1331,7 +1001,7 @@ TEST_F(StateSyncFixture, delete_storage_after_account_deletion) BlockHeader hdr{ .parent_hash = parent_hash, .state_root = - 0x92c33474d175fb59002e90f3625f9850b8305519318701e61f3fd8341d63983d_bytes32, + 0xe0e21f93c7b5e5f90cf3b93142153f9fdf312fd54ac8b898afc23b399f6881c0_bytes32, .number = 1'000'000}; commit_sequential( sctx, @@ -1416,7 +1086,7 @@ TEST_F(StateSyncFixture, update_contract_twice) .incarnation = Incarnation{1, 0}}; hdr.state_root = - 0x3dda8f21af5ec3d4caea2b3b2bddd988e3f1ff1fbfdbaa87a6477bbfce356d26_bytes32; + 0x9b578b1731c41625e45949119ddcd5255e588ba02e17ff3d8a5a3e1b15a84cf8_bytes32; hdr.number = 1; commit_sequential( sctx, @@ -1438,7 +1108,7 @@ TEST_F(StateSyncFixture, update_contract_twice) hdr.parent_hash = to_bytes(keccak256(rlp::encode_block_header(stdb.read_eth_header()))); hdr.state_root = - 0xca4adc8c322ed636a12f74b72d88536795f70e74c8c9b6448ad57058a57664af_bytes32; + 0x9234ef997132f63682a40f6b4c74cf5f839c8346eec2759f88220f84b1f55361_bytes32; hdr.number = 2; commit_sequential( sctx, @@ -1842,15 +1512,10 @@ TEST(ProtocolValidation, upserts_reject_trailing_bytes) auto const dbname = tmp_dbname(); { - monad::register_ethereum_state_machines(); + monad::register_monad_state_machines(); monad_statesync_client client; monad_statesync_client_context ctx{ - CHAIN_CONFIG_MONAD_TESTNET, - {dbname}, - std::nullopt, - 4, - &client, - &statesync_send_request}; + {dbname}, std::nullopt, 4, &client, &statesync_send_request}; Address a{0xdeadbeef}; Account acct{.balance = 1}; diff --git a/cmd/monad/main.cpp b/cmd/monad/main.cpp index 01bbae53b3..4eab5ba2ad 100644 --- a/cmd/monad/main.cpp +++ b/cmd/monad/main.cpp @@ -346,13 +346,13 @@ try { /*enable_multiblock_cache=*/true}; // Dual-timeline: open the secondary alongside the primary. The primary - // always owns the latest state; a secondary is optional. - // runloop_monad: writes every block to every open db. - // runloop_monad_ethblocks: + // always owns the latest state; a secondary is optional and only + // runloop_monad_ethblocks consumes it: // before mip8 fork: writes every block to every open db // after mip8 fork: asserts primary db must be page-encoded, // writes to primary db only, freeze secondary slot db if // secondary db is active. + // runloop_monad requires a single page-encoded timeline and asserts it. std::optional secondary_raw_db; std::optional secondary_db; if (!db_in_memory && @@ -396,9 +396,9 @@ try { std::unique_ptr sync_server; if (!statesync.empty()) { - // Works for either encoding: a page-encoded primary expands each - // page leaf into slot-format upserts in the server traversal, so no - // protocol changes are needed. + // Requires a page-encoded primary (asserted by the server context). + // The server traversal expands each page leaf into slot-format + // upserts, so the wire protocol is unchanged. sync_server = monad::make_statesync_server(monad::StateSyncServerConfig{ .triedb = &triedb, .network = &net.value(), @@ -518,31 +518,11 @@ try { exec_recorder); } else { - // TODO: Remove this check once dual-db is deprecated. - // Live monad requires a page-encoded timeline, either as - // primary (Phase C) or secondary (Phase A/B dual-db). - if (chain_config == CHAIN_CONFIG_MONAD_TESTNET || - chain_config == CHAIN_CONFIG_MONAD_MAINNET) { - bool const primary_is_page = db.is_page_encoded(); - bool const secondary_active = secondary_db.has_value(); - MONAD_ASSERT_PRINTF( - primary_is_page || secondary_active, - "live monad requires a page-encoded timeline " - "(as primary or secondary) on %s; " - "primary_is_page=%d secondary_active=%d", - chain_config == CHAIN_CONFIG_MONAD_TESTNET - ? "monad_testnet" - : "monad_mainnet", // TODO: remove at release2 - primary_is_page, - secondary_active); - } - return runloop_monad( dynamic_cast(*chain), block_db_path, raw_db, db, - secondary_db.has_value() ? &*secondary_db : nullptr, vm, block_hash_buffer, priority_pool, diff --git a/rust/crates/monad-statesync/src/ffi.rs b/rust/crates/monad-statesync/src/ffi.rs index 0eaf99a448..61ab20f6e1 100644 --- a/rust/crates/monad-statesync/src/ffi.rs +++ b/rust/crates/monad-statesync/src/ffi.rs @@ -60,7 +60,6 @@ fn add_client_prefixes_as_new_peers(ctx: *mut monad_statesync_client_context, cl /// Thin unsafe wrapper around statesync_client_context that handles destruction and finalization /// checking pub struct StateSyncCtx { - chain_config: monad_chain_config, dbname_paths: *const *const ::std::os::raw::c_char, len: usize, sq_thread_cpu: Option<::std::os::raw::c_uint>, @@ -76,7 +75,6 @@ pub struct StateSyncCtx { impl StateSyncCtx { /// Initialize StateSyncCtx. There should only ever be *one* StateSyncCtx at any given time. pub fn new( - chain_config: monad_chain_config, dbname_paths: *const *const ::std::os::raw::c_char, len: usize, sq_thread_cpu: Option<::std::os::raw::c_uint>, @@ -89,7 +87,6 @@ impl StateSyncCtx { assert!(unsafe { bindings::monad_statesync_client_compatible(client_version) }); Self { - chain_config, dbname_paths, len, sq_thread_cpu, @@ -108,7 +105,6 @@ impl StateSyncCtx { pub fn get_or_create_ctx(&mut self) -> *mut monad_statesync_client_context { *self.ctx.get_or_insert_with(|| unsafe { self::bindings::monad_statesync_client_context_create( - self.chain_config, self.dbname_paths, self.len, self.sq_thread_cpu @@ -124,7 +120,6 @@ impl StateSyncCtx { ) -> *mut monad_statesync_client_context { *self.ctx.get_or_insert_with(|| unsafe { let ctx = self::bindings::monad_statesync_client_context_create( - self.chain_config, self.dbname_paths, self.len, self.sq_thread_cpu diff --git a/rust/crates/monad-statesync/wrapper.h b/rust/crates/monad-statesync/wrapper.h index da5a41c1db..b902222bf7 100644 --- a/rust/crates/monad-statesync/wrapper.h +++ b/rust/crates/monad-statesync/wrapper.h @@ -13,6 +13,9 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -#include +// chain_config.h is not needed by the statesync C API; it is included so the +// bindings keep exporting monad_chain_config for Rust callers. +#include #include +#include #include From 2027be856dc794a6a464ec161f9789d06b4b0c43 Mon Sep 17 00:00:00 2001 From: Vicky Chen Date: Thu, 10 Sep 2026 15:14:29 -0400 Subject: [PATCH 2/2] runloop C library: open the db by its persisted state machine kind MonadRunloopImpl used the Db ctor that takes a StateMachine, which stamps the machine's kind into the pool metadata on every open and would relabel an existing pool of the other encoding. Use the kind-reading ctor instead and register the state machine factories in monad_runloop_new, so the encoding always comes from monad-mpt --create --state-machine. Co-Authored-By: Claude Fable 5.1 --- .../runloop/runloop_interface_monad.cpp | 22 ++++++++++++++++--- .../runloop/runloop_interface_monad.h | 3 ++- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/category/execution/runloop/runloop_interface_monad.cpp b/category/execution/runloop/runloop_interface_monad.cpp index 677bce3056..46bbe4a308 100644 --- a/category/execution/runloop/runloop_interface_monad.cpp +++ b/category/execution/runloop/runloop_interface_monad.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -27,6 +28,7 @@ #include #include #include +#include #include #include #include @@ -228,15 +230,24 @@ MonadRunloopImpl::MonadRunloopImpl( char const *const db_path) : chain{monad_chain_from_chain_id(chain_id)} , ledger_dir{ledger_path} - , raw_db{std::make_unique(), mpt::OnDiskDbConfig{.append = true, .compaction = true, .rewind_to_latest_finalized = true, .rd_buffers = 8192, .wr_buffers = 32, .uring_entries = 128, .sq_thread_cpu = sq_thread_cpu, .dbname_paths = {fs::path{db_path}}}} + , raw_db{mpt::OnDiskDbConfig{ + .append = true, + .compaction = true, + .rewind_to_latest_finalized = true, + .rd_buffers = 8192, + .wr_buffers = 32, + .uring_entries = 128, + .sq_thread_cpu = sq_thread_cpu, + .dbname_paths = {fs::path{db_path}}}} , triedb{raw_db, /*enable_multiblock_cache=*/true} , runloop_db{triedb, account_override} , vm{} , block_hash_buffer{} , priority_pool{nthreads, nfibers} { - // runloop_monad requires a single page-encoded timeline; the machine - // passed above stamps the primary as page-encoded. + // runloop_monad requires a single page-encoded timeline. The encoding + // comes from the state_machine_kind persisted when the pool was created + // (monad-mpt --create --state-machine monad); it is never stamped here. MONAD_ASSERT(triedb.is_page_encoded()); MONAD_ASSERT(!raw_db.timeline_active(mpt::timeline_id::secondary)); if (triedb.get_root() == nullptr) { @@ -362,6 +373,11 @@ extern "C" MonadRunloop *monad_runloop_new( init_root_logger(log_level); is_quill_running = true; } + // The on-disk Db ctor reads the persisted state_machine_kind and builds + // the StateMachine through the registry, so the factories must be + // registered first. Idempotent. + register_ethereum_state_machines(); + register_monad_state_machines(); return from_impl(new MonadRunloopImpl{chain_id, ledger_path, db_path}); } diff --git a/category/execution/runloop/runloop_interface_monad.h b/category/execution/runloop/runloop_interface_monad.h index 785bad1383..34234ce7bf 100644 --- a/category/execution/runloop/runloop_interface_monad.h +++ b/category/execution/runloop/runloop_interface_monad.h @@ -35,7 +35,8 @@ struct MonadRunloopAddress // Opaque runloop structure: typedef void MonadRunloop; -// Make a new runloop client +// Make a new runloop client. The pool at db_path must already exist and be +// page-encoded (created with monad-mpt --create --state-machine monad). MonadRunloop *monad_runloop_new( uint64_t chain_id, char const *ledger_path, char const *db_path);