Skip to content

Commit 23d4782

Browse files
committed
feat: share sparse trie pipeline with payload builder
1 parent 5736e64 commit 23d4782

8 files changed

Lines changed: 433 additions & 48 deletions

File tree

src/main.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -458,6 +458,13 @@ fn main() -> eyre::Result<()> {
458458
reth_bsc::shared::set_engine_api_tx(node.engine_api_tx.clone().unwrap()).unwrap();
459459
tracing::debug!("set engine api tx successfully");
460460

461+
// Publish CanonicalInMemoryState so the sparse-trie spawner can resolve
462+
// in-memory parents back to the on-disk anchor. Concrete `BlockchainProvider`
463+
// is only reachable here (post-launch); the generic builder context isn't.
464+
let _ = reth_bsc::shared::set_canonical_in_memory_state(
465+
node.provider.canonical_in_memory_state(),
466+
);
467+
461468
// Set the IPC client
462469
reth_bsc::shared::set_ipc_client(ipc_path).await.unwrap();
463470

src/node/engine.rs

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,134 @@ where
111111
MiningConfig::from_env()
112112
};
113113

114+
// Register the sparse-trie state-root spawner, if enabled.
115+
//
116+
// We construct a long-lived `PayloadProcessor` keyed to a fresh `BscEvmConfig`
117+
// (built from chain_spec — same source as the rest of the BSC pipeline). For
118+
// each build job, the registered closure constructs a one-shot
119+
// `OverlayStateProviderFactory` anchored at the parent block hash and calls
120+
// `spawn_state_root` to get a `StateRootHandle`.
121+
//
122+
// `TreeConfig::default()` is used here as a workaround — the engine-launch
123+
// `TreeConfig` (which honors `--engine.*` CLI flags) is not reachable from
124+
// `BscPayloadServiceBuilder`. Default values are reasonable for the sparse-trie
125+
// background task; revisit if performance testing shows we need CLI-tunable
126+
// worker counts or cache sizes.
127+
if mining_config.use_sparse_trie_state_root
128+
&& !rust_eth_triedb::triedb_manager::is_triedb_active()
129+
{
130+
use alloy_consensus::BlockHeader;
131+
use reth_chain_state::LazyOverlay;
132+
use reth_engine_tree::tree::{
133+
payload_processor::PayloadProcessor, precompile_cache::PrecompileCacheMap,
134+
TreeConfig,
135+
};
136+
use reth_provider::providers::{OverlayBuilder, OverlayStateProviderFactory};
137+
use reth_tasks::{Runtime, RuntimeBuilder, RuntimeConfig, TokioConfig};
138+
use reth_trie_db::ChangesetCache;
139+
140+
let chain_spec = Arc::new(ctx.config().chain.clone().as_ref().clone());
141+
let bsc_evm_config = crate::node::evm::config::BscEvmConfig::new(chain_spec);
142+
let tree_config = Arc::new(TreeConfig::default());
143+
let provider = ctx.provider().clone();
144+
145+
// Build a dedicated `reth_tasks::Runtime` for the sparse-trie pools,
146+
// attached to the existing tokio handle so we don't spin up a second tokio
147+
// executor. Rayon pools default to `available_parallelism()`, matching what
148+
// the engine uses for its own PayloadProcessor.
149+
//
150+
// TODO: in a follow-up, share the engine's Runtime instead of constructing
151+
// a parallel one — this currently means two sets of rayon pools competing
152+
// for CPU. Acceptable for a first cut; revisit if perf testing shows
153+
// contention.
154+
let tokio_handle = ctx.task_executor().handle().clone();
155+
let runtime = RuntimeBuilder::new(
156+
RuntimeConfig::default().with_tokio(TokioConfig::existing_handle(tokio_handle)),
157+
)
158+
.build()
159+
.map_err(|e| eyre::eyre!("failed to build sparse-trie Runtime: {e}"))?;
160+
let _: &Runtime = &runtime; // type-check anchor
161+
162+
let payload_processor = std::sync::Arc::new(PayloadProcessor::new(
163+
runtime,
164+
bsc_evm_config,
165+
tree_config.as_ref(),
166+
PrecompileCacheMap::default(),
167+
));
168+
169+
let tree_config_for_closure = tree_config.clone();
170+
let spawn_fn: crate::shared::SparseTrieSpawnFn = std::sync::Arc::new(
171+
move |parent_hash: alloy_primitives::B256,
172+
parent_state_root: alloy_primitives::B256| {
173+
// Walk the in-memory canonical chain to find the on-disk anchor.
174+
// Without this, proof workers fail with `BlockHashNotFound` whenever
175+
// the parent hasn't been persisted yet (the common case during fast
176+
// block production with last_persisted_number lagging head).
177+
//
178+
// Mirrors `payload_validator::get_parent_lazy_overlay` from upstream
179+
// reth, which the engine's own PayloadProcessor uses.
180+
//
181+
// `canonical_in_memory_state` is published from main.rs after launch
182+
// (it's only reachable on the concrete BlockchainProvider). If for
183+
// some reason it's not yet set (race during early startup), fall back
184+
// to anchoring at parent_hash directly — proof workers will fail and
185+
// builder will use the synchronous state_root_with_updates path.
186+
let (anchor_hash, lazy_overlay) = if let Some(cim) =
187+
crate::shared::get_canonical_in_memory_state()
188+
{
189+
match cim.state_by_hash(parent_hash) {
190+
Some(state) => {
191+
// chain() yields newest-to-oldest including self, exactly
192+
// the order LazyOverlay::new requires.
193+
let blocks: Vec<ExecutedBlock<crate::BscPrimitives>> =
194+
state.chain().map(|bs| bs.block()).collect();
195+
// Anchor = parent of the oldest in-memory block (= on-disk tip).
196+
let anchor = blocks
197+
.last()
198+
.map(|b| b.recovered_block().parent_hash())
199+
.unwrap_or(parent_hash);
200+
(anchor, Some(LazyOverlay::new(blocks)))
201+
}
202+
None => {
203+
// Parent already persisted — anchor directly, no overlay.
204+
(parent_hash, None)
205+
}
206+
}
207+
} else {
208+
(parent_hash, None)
209+
};
210+
211+
let overlay_builder = OverlayBuilder::<crate::BscPrimitives>::new(
212+
anchor_hash,
213+
ChangesetCache::default(),
214+
)
215+
.with_lazy_overlay(lazy_overlay);
216+
217+
let overlay_factory = OverlayStateProviderFactory::new(
218+
provider.clone(),
219+
overlay_builder,
220+
);
221+
Some(payload_processor.spawn_state_root(
222+
overlay_factory,
223+
parent_state_root,
224+
false, // halve_workers
225+
tree_config_for_closure.as_ref(),
226+
))
227+
},
228+
);
229+
230+
if crate::shared::set_sparse_trie_spawn_fn(spawn_fn).is_err() {
231+
tracing::warn!(
232+
"Sparse-trie spawner already registered, keeping existing one"
233+
);
234+
} else {
235+
info!(
236+
"Sparse-trie state-root spawner registered \
237+
(use_sparse_trie_state_root=true, triedb=inactive)"
238+
);
239+
}
240+
}
241+
114242
// Skip mining setup if disabled
115243
if !mining_config.is_mining_enabled() {
116244
info!("Mining is disabled in configuration");

src/node/evm/builder.rs

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,9 +175,59 @@ where
175175
state: impl StateProvider,
176176
) -> Result<BlockBuilderOutcomeWithDiffLayer<BscPrimitives>, BlockExecutionError> {
177177
let finish_start = std::time::Instant::now();
178+
// `executor.finish()` runs BSC's post-execution system txs (slash spoiled
179+
// validator, distribute fees / finality rewards, breathe-block validator-set
180+
// updates). Any `state_hook` previously installed on the executor — including
181+
// the sparse-trie hook — captures those state changes here. Consuming the
182+
// executor on the next line drops the hook closure, which triggers
183+
// `StateHookSender::drop` → `FinishedStateUpdates` → sparse-trie task can
184+
// safely return from `state_root()` below.
178185
let (evm, result) = self.executor.finish()?;
179186
let (db, evm_env) = evm.finish();
180187

188+
// Sparse-trie state-root collection: now that the executor (and therefore the
189+
// state_hook) has been dropped, the background task has all updates and is
190+
// finalizing. Take the handle from ctx, block on `state_root()`, and stash the
191+
// result in the sink — the MDBX branch below reads it.
192+
//
193+
// Failures fall through silently to the legacy `state_root_with_updates` path,
194+
// logged at WARN. A failure here is non-fatal but indicates the task panicked
195+
// or its channel was dropped; investigate via the trace target below.
196+
if let Some(handle_slot) = self.ctx.trie_handle.take() {
197+
if let Some(mut handle) = handle_slot.lock().unwrap().take() {
198+
let wait_start = std::time::Instant::now();
199+
match handle.state_root() {
200+
Ok(outcome) => {
201+
let updates = std::sync::Arc::try_unwrap(outcome.trie_updates)
202+
.unwrap_or_else(|arc| (*arc).clone());
203+
if let Some(sink) = self.ctx.state_root_precomputed_sink.as_ref() {
204+
*sink.lock().unwrap() = Some((outcome.state_root, updates));
205+
} else {
206+
// No sink registered — write into the field (which the
207+
// MDBX branch also reads as fallback).
208+
self.precomputed_state_root = Some((outcome.state_root, updates));
209+
}
210+
tracing::debug!(
211+
target: "bsc::builder",
212+
parent_hash = %self.parent.hash(),
213+
block_number = %(self.parent.number + 1),
214+
wait_ms = wait_start.elapsed().as_millis(),
215+
"Sparse-trie state-root delivered post-finish()"
216+
);
217+
}
218+
Err(err) => {
219+
tracing::warn!(
220+
target: "bsc::builder",
221+
parent_hash = %self.parent.hash(),
222+
block_number = %(self.parent.number + 1),
223+
%err,
224+
"Sparse-trie task failed post-finish(); falling back to state_root_with_updates"
225+
);
226+
}
227+
}
228+
}
229+
}
230+
181231
let assembled_system_txs = {
182232
let mut inner = self.shared_ctx.inner.borrow_mut();
183233
std::mem::take(&mut inner.assembled_system_txs)
@@ -228,11 +278,21 @@ where
228278
"Calculated state root using triedb"
229279
);
230280
(new_root, TrieUpdates::default(), Some(new_difflayer))
231-
} else if let Some((root, updates)) = self.precomputed_state_root.take() {
281+
} else if let Some((root, updates)) = self
282+
.ctx
283+
.state_root_precomputed_sink
284+
.as_ref()
285+
.and_then(|sink| sink.lock().unwrap().take())
286+
.or_else(|| self.precomputed_state_root.take())
287+
{
232288
// Fast path: sparse-trie background task already computed the root concurrently
233289
// with execution. See `crate::shared::spawn_sparse_trie_state_root` and reth 2.0
234290
// `--engine.share-sparse-trie-with-payload-builder` semantics for the upstream
235291
// mechanism we mirror.
292+
//
293+
// Preferred source is the sink on `self.ctx` (filled by the payload layer
294+
// post-exec); the field on `Self` is a fallback that `fn finish` populates
295+
// when callers route through the trait method.
236296
tracing::debug!(
237297
target: "bsc::builder",
238298
parent_hash = %self.parent.hash(),

src/node/evm/config.rs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,27 @@ pub struct BscNextBlockEnvAttributes {
6969
/// Sink for transporting `turn_length` from builder to payload layer without writing to
7070
/// TURN_LENGTH_CACHE prematurely.
7171
pub turn_length_sink: Option<Arc<Mutex<Option<u8>>>>,
72+
/// Sink for precomputed `(state_root, trie_updates)` from a sparse-trie background
73+
/// task. Filled by payload layer between exec and `finish_with_difflayer` so the
74+
/// builder's MDBX branch can skip the blocking `state_root_with_updates` call. See
75+
/// [`BscBlockExecutionCtx::state_root_precomputed_sink`] for full semantics.
76+
pub state_root_precomputed_sink: Option<
77+
Arc<Mutex<Option<(alloy_primitives::B256, reth_trie_common::updates::TrieUpdates)>>>,
78+
>,
79+
/// Sparse-trie state-root handle, threaded through to `finish_with_difflayer`.
80+
///
81+
/// Stored here (in `Arc<Mutex<Option<_>>>` so `Clone` works for the type-erased
82+
/// builder path) so that `state_root()` can be called **after** `executor.finish()`
83+
/// runs BSC's post-execution system transactions (slash, fee distribution,
84+
/// validator-set updates). Those system txs change state via the same executor
85+
/// that has the `state_hook` installed; the hook is dropped naturally when the
86+
/// executor is consumed by `finish()`, which sends `FinishedStateUpdates` to the
87+
/// background task. Only after that drop is it safe to await `state_root()`.
88+
///
89+
/// `None` when sparse-trie is disabled or in TrieDB mode.
90+
pub trie_handle: Option<
91+
Arc<Mutex<Option<reth_engine_tree::tree::multiproof::StateRootHandle>>>,
92+
>,
7293
}
7394

7495
impl<H: BlockHeader> BuildPendingEnv<H> for BscNextBlockEnvAttributes {
@@ -79,6 +100,8 @@ impl<H: BlockHeader> BuildPendingEnv<H> for BscNextBlockEnvAttributes {
79100
triedb_prefetcher: None,
80101
validator_cache_sink: None,
81102
turn_length_sink: None,
103+
state_root_precomputed_sink: None,
104+
trie_handle: None,
82105
}
83106
}
84107
}
@@ -135,6 +158,29 @@ pub struct BscBlockExecutionCtx<'a> {
135158
pub validator_cache_sink: Option<ValidatorCacheSink>,
136159
/// Sink for `turn_length` — same lifecycle as `validator_cache_sink`.
137160
pub turn_length_sink: Option<Arc<Mutex<Option<u8>>>>,
161+
/// Sink for a precomputed `(state_root, trie_updates)` from a sparse-trie background
162+
/// task (reth 2.0 mechanism).
163+
///
164+
/// Write direction is **reversed** vs the other sinks: the payload layer fills this
165+
/// **before** calling `finish_with_difflayer`, and the builder's MDBX branch reads
166+
/// it to skip the synchronous `state_root_with_updates` call. `None` in the bid
167+
/// simulator path and when the `--mining.use-sparse-trie-state-root` flag is off,
168+
/// triggering the legacy state-root path.
169+
pub state_root_precomputed_sink: Option<
170+
Arc<Mutex<Option<(alloy_primitives::B256, reth_trie_common::updates::TrieUpdates)>>>,
171+
>,
172+
/// Sparse-trie state-root handle. The builder consumes this **after**
173+
/// `executor.finish()` runs BSC's post-execution system transactions (slash,
174+
/// fee distribution, validator-set updates), so those state changes are
175+
/// captured by the executor's `state_hook` before the hook is dropped (which
176+
/// signals the sparse-trie task to finalize). Calling `state_root()` before
177+
/// the executor is dropped would deadlock the task on `FinishedStateUpdates`.
178+
///
179+
/// `Arc<Mutex<Option<_>>>` because `StateRootHandle` is `!Clone` (single-use
180+
/// receiver) and `BscBlockExecutionCtx` derives `Clone`.
181+
pub trie_handle: Option<
182+
Arc<Mutex<Option<reth_engine_tree::tree::multiproof::StateRootHandle>>>,
183+
>,
138184
}
139185

140186
impl<'a> BscBlockExecutionCtx<'a> {
@@ -440,6 +486,8 @@ where
440486
triedb_prefetcher: None,
441487
validator_cache_sink: None,
442488
turn_length_sink: None,
489+
state_root_precomputed_sink: None,
490+
trie_handle: None,
443491
})
444492
}
445493

@@ -466,6 +514,8 @@ where
466514
triedb_prefetcher: attributes.triedb_prefetcher,
467515
validator_cache_sink: attributes.validator_cache_sink,
468516
turn_length_sink: attributes.turn_length_sink,
517+
state_root_precomputed_sink: attributes.state_root_precomputed_sink,
518+
trie_handle: attributes.trie_handle,
469519
})
470520
}
471521

@@ -541,6 +591,8 @@ where
541591
triedb_prefetcher: None,
542592
validator_cache_sink: None,
543593
turn_length_sink: None,
594+
state_root_precomputed_sink: None,
595+
trie_handle: None,
544596
})
545597
}
546598

src/node/miner/bid_simulator.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -459,6 +459,10 @@ where
459459
triedb_prefetcher,
460460
validator_cache_sink: Some(bid_validator_cache_sink.clone()),
461461
turn_length_sink: Some(bid_turn_length_sink.clone()),
462+
// Bid simulation does not run alongside a sparse-trie task —
463+
// builder will fall through to state_root_with_updates.
464+
state_root_precomputed_sink: None,
465+
trie_handle: None,
462466
},
463467
)
464468
.map_err(PayloadBuilderError::other)

src/node/miner/bsc_miner.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -733,6 +733,7 @@ where
733733
// Filled in by BscPayloadJob::start when sparse-trie state-root is enabled
734734
// and the engine has registered a spawner. Falls back to legacy path when None.
735735
state_root_precomputed: std::sync::Arc::new(std::sync::Mutex::new(None)),
736+
trie_handle: std::sync::Arc::new(std::sync::Mutex::new(None)),
736737
};
737738

738739
let parent_hash = mining_ctx.parent_header.hash();

0 commit comments

Comments
 (0)