Skip to main content

reth_trie_parallel/
state_root_task.rs

1//! State-root task interface types shared between the engine tree and the payload builder.
2//!
3//! The "state-root task" is the background multiproof and sparse-trie pipeline that computes
4//! state roots incrementally while a block executes. This module holds its boundary types:
5//! the input messages, the [`StateRootSink`](crate::state_root_task::StateRootSink) and
6//! stream views that feed it, and the handles
7//! that await its result. The per-block strategy abstraction that decides whether and how the
8//! task runs lives in `reth-engine-tree` under `tree::state_root_strategy`.
9
10use crate::error::StateRootTaskError;
11use alloy_evm::block::OnStateHook;
12use alloy_primitives::{keccak256, map::B256Map, B256};
13use reth_trie::{
14    updates::TrieUpdates, HashedPostState, HashedStorage, MultiProofTargetsV2, ProofV2Target,
15};
16use revm::state::EvmState;
17use std::{fmt, sync::Arc};
18use tracing::trace;
19
20/// Messages used internally by the multi proof task.
21#[derive(Debug)]
22pub enum StateRootMessage {
23    /// Prefetch proof targets
24    PrefetchProofs(MultiProofTargetsV2),
25    /// New state update from transaction execution.
26    StateUpdate(EvmState),
27    /// Pre-hashed state update from BAL conversion that can be applied directly without proofs.
28    HashedStateUpdate(HashedPostState),
29    /// Signals state update stream end.
30    ///
31    /// This is triggered by block execution, indicating that no additional state updates are
32    /// expected.
33    FinishedStateUpdates,
34}
35
36/// Outcome of the state root computation, including the state root itself with
37/// the trie updates.
38#[derive(Debug, Clone)]
39pub struct StateRootComputeOutcome {
40    /// The state root.
41    pub state_root: B256,
42    /// The trie updates.
43    pub trie_updates: Arc<TrieUpdates>,
44    /// Hashed post state produced while computing the state root.
45    pub hashed_state: Arc<HashedPostState>,
46    /// Debug recorders taken from the sparse tries, keyed by `None` for account trie
47    /// and `Some(address)` for storage tries.
48    #[cfg(feature = "trie-debug")]
49    pub debug_recorders: Vec<(Option<B256>, reth_trie_sparse::debug_recorder::TrieDebugRecorder)>,
50}
51
52/// Handle to a background sparse trie state root computation.
53///
54/// Used by both the engine (during `newPayload`) and the payload builder (during `FCU`-triggered
55/// block building). Provides channels for streaming state updates into the pipeline and receiving
56/// the final computed state root.
57///
58/// Created by the engine's state-root strategy.
59#[derive(Debug)]
60pub struct StateRootHandle {
61    /// The state root that the cached sparse trie is anchored at (parent block's state root).
62    cached_trie_state_root: B256,
63    /// Best-effort hint capability, taken once by prewarm wiring.
64    hint: Option<StateRootHintStream>,
65    /// The single authoritative update capability.
66    ///
67    /// Taken exactly once, either as an execution hook (serial execution) or as a hashed
68    /// update stream (parallel BAL streaming), so per block exactly one producer can finish
69    /// the update stream. Only producers hold update senders: once the taken capabilities are
70    /// dropped or finished, the update channel closes and the task knows producers are done.
71    authoritative: Option<StateRootUpdateStream>,
72    /// Guard whose drop cancels the state-root task if it is still running.
73    cancel_guard: StateRootTaskCancelGuard,
74    /// Receiver for the final state root result.
75    state_root_rx:
76        Option<std::sync::mpsc::Receiver<Result<StateRootComputeOutcome, StateRootTaskError>>>,
77    /// Receiver for the hashed post state.
78    hashed_state_rx: Option<std::sync::mpsc::Receiver<Arc<HashedPostState>>>,
79}
80
81impl StateRootHandle {
82    /// Creates a new [`StateRootHandle`].
83    pub fn new(
84        cached_trie_state_root: B256,
85        updates_tx: crossbeam_channel::Sender<StateRootMessage>,
86        cancel_guard: StateRootTaskCancelGuard,
87        state_root_rx: std::sync::mpsc::Receiver<
88            Result<StateRootComputeOutcome, StateRootTaskError>,
89        >,
90        hashed_state_rx: std::sync::mpsc::Receiver<Arc<HashedPostState>>,
91    ) -> Self {
92        let sink: Arc<dyn StateRootSink> = Arc::new(SparseTrieStateRootSink::new(updates_tx));
93        Self {
94            cached_trie_state_root,
95            hint: Some(StateRootHintStream::new(Arc::clone(&sink))),
96            authoritative: Some(StateRootUpdateStream::new(sink)),
97            cancel_guard,
98            state_root_rx: Some(state_root_rx),
99            hashed_state_rx: Some(hashed_state_rx),
100        }
101    }
102
103    /// Returns the state root that the cached sparse trie is anchored at.
104    pub const fn cached_trie_state_root(&self) -> B256 {
105        self.cached_trie_state_root
106    }
107
108    /// Takes the best-effort hint capability used by transaction prewarming.
109    ///
110    /// # Panics
111    ///
112    /// If called more than once.
113    pub const fn take_hint_stream(&mut self) -> StateRootHintStream {
114        self.hint.take().expect("hint stream already taken")
115    }
116
117    /// Takes the authoritative update capability as an EVM state hook.
118    ///
119    /// The hook finishes the update stream when dropped. It shares one slot with
120    /// [`Self::take_hashed_update_stream`], so only one of the two can exist per block.
121    ///
122    /// # Panics
123    ///
124    /// If the authoritative capability was already taken in either form.
125    pub fn take_execution_hook(&mut self) -> StateRootUpdateHook {
126        self.take_hashed_update_stream().into_state_hook()
127    }
128
129    /// Takes the authoritative update capability as a pre-hashed update stream.
130    ///
131    /// The stream is finished explicitly with [`StateRootUpdateStream::finish`]. It shares
132    /// one slot with [`Self::take_execution_hook`], so only one of the two can exist per
133    /// block.
134    ///
135    /// # Panics
136    ///
137    /// If the authoritative capability was already taken in either form.
138    pub const fn take_hashed_update_stream(&mut self) -> StateRootUpdateStream {
139        self.authoritative.take().expect("authoritative update capability already taken")
140    }
141
142    /// Awaits the state root computation result.
143    ///
144    /// # Panics
145    ///
146    /// If called more than once.
147    pub fn state_root(&mut self) -> Result<StateRootComputeOutcome, StateRootTaskError> {
148        self.state_root_rx
149            .take()
150            .expect("state_root already taken")
151            .recv()
152            .map_err(|_| StateRootTaskError::Other("sparse trie task dropped".to_string()))?
153    }
154
155    /// Takes the state root receiver for use with custom waiting logic (e.g., timeouts).
156    ///
157    /// # Panics
158    ///
159    /// If called more than once.
160    pub const fn take_state_root_rx(
161        &mut self,
162    ) -> std::sync::mpsc::Receiver<Result<StateRootComputeOutcome, StateRootTaskError>> {
163        self.state_root_rx.take().expect("state_root already taken")
164    }
165
166    /// Takes the hashed state receiver
167    ///
168    /// # Panics
169    ///
170    /// If called more than once.
171    pub const fn take_hashed_state_rx(
172        &mut self,
173    ) -> std::sync::mpsc::Receiver<Arc<HashedPostState>> {
174        self.hashed_state_rx.take().expect("hashed_state already taken")
175    }
176
177    /// Converts this sparse-trie handle into the opaque handle passed to payload builders.
178    ///
179    /// The payload builder only executes transactions, so the handle carries the execution
180    /// hook; the hint capability is dropped here.
181    pub fn into_payload_state_root_handle(mut self) -> PayloadStateRootHandle {
182        let hook = self.take_execution_hook();
183        PayloadStateRootHandle {
184            name: "sparse-trie",
185            hook: Some(hook),
186            cancel_guard: Some(self.cancel_guard),
187            state_root_rx: self.state_root_rx.take(),
188            hashed_state_rx: self.hashed_state_rx.take(),
189        }
190    }
191}
192
193/// Guard that cancels a state-root task when dropped.
194///
195/// The task watches the paired receiver in its event loop. No message is ever sent: the guard
196/// dropping disconnects the channel, which the task treats as the consumer abandoning the
197/// computation (for example on a timeout fallback or when a payload job is dropped unused).
198#[derive(Debug)]
199pub struct StateRootTaskCancelGuard(#[allow(dead_code)] crossbeam_channel::Sender<()>);
200
201impl StateRootTaskCancelGuard {
202    /// Creates a guard and the receiver a task watches for cancellation.
203    pub fn channel() -> (Self, crossbeam_channel::Receiver<()>) {
204        let (tx, rx) = crossbeam_channel::bounded(0);
205        (Self(tx), rx)
206    }
207}
208
209/// Opaque state-root task handle passed to payload builders.
210pub struct PayloadStateRootHandle {
211    name: &'static str,
212    /// Execution hook that streams per-transaction updates; taken once when building starts.
213    hook: Option<StateRootUpdateHook>,
214    /// Cancels the backing task when the handle is dropped without consuming the result.
215    cancel_guard: Option<StateRootTaskCancelGuard>,
216    state_root_rx:
217        Option<std::sync::mpsc::Receiver<Result<StateRootComputeOutcome, StateRootTaskError>>>,
218    hashed_state_rx: Option<std::sync::mpsc::Receiver<Arc<HashedPostState>>>,
219}
220
221impl fmt::Debug for PayloadStateRootHandle {
222    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
223        f.debug_struct("PayloadStateRootHandle")
224            .field("name", &self.name)
225            .field("has_hook", &self.hook.is_some())
226            .field("has_cancel_guard", &self.cancel_guard.is_some())
227            .field("has_state_root_rx", &self.state_root_rx.is_some())
228            .field("has_hashed_state_rx", &self.hashed_state_rx.is_some())
229            .finish()
230    }
231}
232
233impl PayloadStateRootHandle {
234    /// Creates an opaque payload state-root handle.
235    ///
236    /// Tasks with a drop-to-cancel guard should attach it via the `StateRootHandle`
237    /// conversion; handles created here rely on their own task lifecycle.
238    pub const fn new(
239        name: &'static str,
240        hook: Option<StateRootUpdateHook>,
241        state_root_rx: std::sync::mpsc::Receiver<
242            Result<StateRootComputeOutcome, StateRootTaskError>,
243        >,
244        hashed_state_rx: Option<std::sync::mpsc::Receiver<Arc<HashedPostState>>>,
245    ) -> Self {
246        Self { name, hook, cancel_guard: None, state_root_rx: Some(state_root_rx), hashed_state_rx }
247    }
248
249    /// Returns the task name used in logs.
250    pub const fn name(&self) -> &'static str {
251        self.name
252    }
253
254    /// Takes the state hook that streams execution updates and finishes the stream on drop.
255    ///
256    /// # Panics
257    ///
258    /// If the handle was created without an execution hook, or the hook was already taken.
259    pub const fn take_state_hook(&mut self) -> StateRootUpdateHook {
260        self.hook.take().expect("payload state root task missing execution hook")
261    }
262
263    /// Awaits the state root computation result.
264    ///
265    /// # Panics
266    ///
267    /// If called more than once.
268    pub fn state_root(&mut self) -> Result<StateRootComputeOutcome, StateRootTaskError> {
269        self.state_root_rx
270            .take()
271            .expect("state_root already taken")
272            .recv()
273            .map_err(|_| StateRootTaskError::Other("state root task dropped".to_string()))?
274    }
275
276    /// Takes the state root receiver for use with custom waiting logic (e.g., timeouts).
277    ///
278    /// Dropping the handle continues to cancel the backing task if it owns a cancellation guard.
279    ///
280    /// # Panics
281    ///
282    /// If called more than once.
283    pub const fn take_state_root_rx(
284        &mut self,
285    ) -> std::sync::mpsc::Receiver<Result<StateRootComputeOutcome, StateRootTaskError>> {
286        self.state_root_rx.take().expect("state_root already taken")
287    }
288
289    /// Takes the hashed state receiver, if the handle was built with one and it was not taken
290    /// yet.
291    pub const fn try_take_hashed_state_rx(
292        &mut self,
293    ) -> Option<std::sync::mpsc::Receiver<Arc<HashedPostState>>> {
294        self.hashed_state_rx.take()
295    }
296}
297
298/// Hashed account and storage keys that a state-root task may want to prefetch.
299///
300/// Hints are not authoritative. They may be missing, duplicated, stale, or ignored by a task.
301/// The conversions from and to proof-target types allocate; that cost is accepted because
302/// hints are produced on prewarm workers, off the block-execution thread.
303#[derive(Debug, Clone, Default)]
304pub struct StateAccessHint {
305    /// Hashed account keys that may be touched later in the block.
306    pub accounts: Vec<B256>,
307    /// Hashed storage keys keyed by hashed account.
308    pub storages: B256Map<Vec<B256>>,
309}
310
311impl From<MultiProofTargetsV2> for StateAccessHint {
312    fn from(targets: MultiProofTargetsV2) -> Self {
313        Self {
314            accounts: targets.account_targets.into_iter().map(|target| target.key()).collect(),
315            storages: targets
316                .storage_targets
317                .into_iter()
318                .map(|(account, slots)| {
319                    (account, slots.into_iter().map(|target| target.key()).collect())
320                })
321                .collect(),
322        }
323    }
324}
325
326impl From<StateAccessHint> for MultiProofTargetsV2 {
327    fn from(hint: StateAccessHint) -> Self {
328        Self {
329            account_targets: hint.accounts.into_iter().map(ProofV2Target::from).collect(),
330            storage_targets: hint
331                .storages
332                .into_iter()
333                .map(|(account, slots)| {
334                    (account, slots.into_iter().map(ProofV2Target::from).collect())
335                })
336                .collect(),
337        }
338    }
339}
340
341/// Semantic update stream consumed by state-root tasks.
342pub trait StateRootSink: Send + Sync + 'static {
343    /// Best-effort access hint from transaction prewarming.
344    fn on_access_hint(&self, _hint: StateAccessHint) {}
345
346    /// Authoritative state update from normal block execution.
347    fn on_state_update(&self, state: EvmState);
348
349    /// Authoritative pre-hashed state update, currently used by BAL streaming.
350    fn on_hashed_state_update(&self, state: HashedPostState);
351
352    /// Signals that no more authoritative state updates are expected.
353    fn on_updates_finished(&self);
354}
355
356/// Hint-only view of a state-root stream.
357#[derive(Clone)]
358pub struct StateRootHintStream {
359    inner: Arc<dyn StateRootSink>,
360}
361
362impl fmt::Debug for StateRootHintStream {
363    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
364        f.debug_struct("StateRootHintStream").finish_non_exhaustive()
365    }
366}
367
368impl StateRootHintStream {
369    /// Creates a new hint stream view.
370    pub fn new(inner: Arc<dyn StateRootSink>) -> Self {
371        Self { inner }
372    }
373
374    /// Emits a best-effort access hint.
375    pub fn on_access_hint(&self, hint: StateAccessHint) {
376        self.inner.on_access_hint(hint);
377    }
378}
379
380/// Authoritative update capability of a state-root stream.
381///
382/// Exactly one of these exists per state-root task, so exactly one producer can end the
383/// update stream: either the EVM state hook made with [`Self::into_state_hook`] (finishes on
384/// drop) or a pre-hashed update producer such as BAL streaming (calls [`Self::finish`]). The
385/// type is deliberately not `Clone` and finishing consumes it, so a second end-of-stream
386/// signal cannot be produced.
387///
388/// Dropping the stream without calling [`Self::finish`] (for example when a producer dies)
389/// deliberately does not finish it: an unfinished stream means the updates are incomplete,
390/// and the task must not compute a root from them.
391pub struct StateRootUpdateStream {
392    inner: Arc<dyn StateRootSink>,
393}
394
395impl fmt::Debug for StateRootUpdateStream {
396    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
397        f.debug_struct("StateRootUpdateStream").finish_non_exhaustive()
398    }
399}
400
401impl StateRootUpdateStream {
402    /// Creates a new authoritative update stream backed by the given sink.
403    pub fn new(inner: Arc<dyn StateRootSink>) -> Self {
404        Self { inner }
405    }
406
407    /// Emits an authoritative pre-hashed state update.
408    pub fn on_hashed_state_update(&self, state: HashedPostState) {
409        self.inner.on_hashed_state_update(state);
410    }
411
412    /// Finishes the authoritative update stream.
413    pub fn finish(self) {
414        self.inner.on_updates_finished();
415    }
416
417    /// Converts this capability into an EVM state hook that finishes the stream on drop.
418    ///
419    /// See [`StateRootUpdateHook`] for why the hook finishes on drop while the bare stream
420    /// does not, and how a panic during execution is excluded from that.
421    pub fn into_state_hook(self) -> StateRootUpdateHook {
422        StateRootUpdateHook { inner: self.inner }
423    }
424}
425
426/// EVM hook that forwards state updates into a [`StateRootSink`].
427///
428/// Dropping the hook signals the end of the update stream, so the hook is deliberately not
429/// `Clone`: a second copy would fire a spurious end-of-stream signal.
430///
431/// Unlike [`StateRootUpdateStream::finish`], the end of the stream is signaled by drop and
432/// not by an explicit call, because the EVM owns the hook until it is dropped and gives it no
433/// other end-of-execution signal. A drop during a panic unwind is excluded: execution died
434/// mid-block, so the stream stays unfinished and the task reports an error instead of
435/// computing a root from incomplete updates. Execution that fails by returning an error still
436/// drops the hook normally and finishes the stream; the caller abandons the result in that
437/// case, and the stored trie is rejected by the anchor check on the next block.
438pub struct StateRootUpdateHook {
439    inner: Arc<dyn StateRootSink>,
440}
441
442impl fmt::Debug for StateRootUpdateHook {
443    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
444        f.debug_struct("StateRootUpdateHook").finish_non_exhaustive()
445    }
446}
447
448impl OnStateHook for StateRootUpdateHook {
449    fn on_state(&mut self, state: EvmState) {
450        self.inner.on_state_update(state);
451    }
452}
453
454impl Drop for StateRootUpdateHook {
455    fn drop(&mut self) {
456        // A drop during a panic unwind means execution died mid-block. Leave the stream
457        // unfinished so the task fails instead of computing a root from partial updates.
458        if std::thread::panicking() {
459            return;
460        }
461        self.inner.on_updates_finished();
462    }
463}
464
465#[derive(Debug, Clone)]
466struct SparseTrieStateRootSink {
467    sender: crossbeam_channel::Sender<StateRootMessage>,
468}
469
470impl SparseTrieStateRootSink {
471    const fn new(sender: crossbeam_channel::Sender<StateRootMessage>) -> Self {
472        Self { sender }
473    }
474}
475
476impl StateRootSink for SparseTrieStateRootSink {
477    fn on_access_hint(&self, hint: StateAccessHint) {
478        let _ = self.sender.send(StateRootMessage::PrefetchProofs(hint.into()));
479    }
480
481    fn on_state_update(&self, state: EvmState) {
482        let _ = self.sender.send(StateRootMessage::StateUpdate(state));
483    }
484
485    fn on_hashed_state_update(&self, state: HashedPostState) {
486        let _ = self.sender.send(StateRootMessage::HashedStateUpdate(state));
487    }
488
489    fn on_updates_finished(&self) {
490        let _ = self.sender.send(StateRootMessage::FinishedStateUpdates);
491    }
492}
493
494/// Converts [`EvmState`] to [`HashedPostState`] by keccak256-hashing addresses and storage slots.
495pub fn evm_state_to_hashed_post_state(update: EvmState) -> HashedPostState {
496    let mut hashed_state = HashedPostState::with_capacity(update.len());
497
498    for (address, account) in update {
499        if account.is_touched() {
500            let hashed_address = keccak256(address);
501            trace!(target: "trie::parallel::sparse", ?address, ?hashed_address, "Adding account to state update");
502
503            let destroyed = account.is_selfdestructed();
504            if account.info != account.original_info() {
505                let info = if destroyed { None } else { Some(account.info.into()) };
506                hashed_state.accounts.insert(hashed_address, info);
507            }
508
509            let mut changed_storage_iter = account
510                .storage
511                .into_iter()
512                .filter(|(_slot, value)| value.is_changed())
513                .map(|(slot, value)| (keccak256(B256::from(slot)), value.present_value))
514                .peekable();
515
516            if !destroyed && changed_storage_iter.peek().is_some() {
517                hashed_state
518                    .storages
519                    .insert(hashed_address, HashedStorage::from_iter(false, changed_storage_iter));
520            }
521        }
522    }
523
524    hashed_state
525}
526
527#[cfg(test)]
528mod tests {
529    use super::*;
530    use alloy_primitives::{Address, U256};
531    use revm::state::{Account, EvmStorageSlot, TransactionId};
532    use std::{
533        sync::atomic::{AtomicUsize, Ordering},
534        time::Duration,
535    };
536
537    #[test]
538    fn created_selfdestruct_does_not_emit_storage() {
539        let address = Address::repeat_byte(0x01);
540        let mut account = Account::default();
541        account.mark_touch();
542        assert!(account.mark_created_locally());
543        assert!(account.mark_selfdestructed_locally());
544        account.info.nonce = 1;
545        account.storage.insert(
546            U256::from(1),
547            EvmStorageSlot::new_changed(U256::ZERO, U256::from(2), TransactionId::ZERO),
548        );
549
550        let hashed_state =
551            evm_state_to_hashed_post_state(EvmState::from_iter([(address, account)]));
552        let hashed_address = keccak256(address);
553
554        assert_eq!(hashed_state.accounts.get(&hashed_address), Some(&None));
555        assert!(!hashed_state.storages.contains_key(&hashed_address));
556    }
557
558    #[test]
559    fn existing_selfdestruct_does_not_emit_storage() {
560        let address = Address::repeat_byte(0x02);
561        let mut account = Account::default();
562        account.info.nonce = 1;
563        account.set_current_info_as_original();
564        account.mark_touch();
565        assert!(account.mark_selfdestructed_locally());
566        account.selfdestruct();
567        account.storage.insert(
568            U256::from(1),
569            EvmStorageSlot::new_changed(U256::ZERO, U256::from(2), TransactionId::ZERO),
570        );
571
572        let hashed_state =
573            evm_state_to_hashed_post_state(EvmState::from_iter([(address, account)]));
574        let hashed_address = keccak256(address);
575
576        assert_eq!(hashed_state.accounts.get(&hashed_address), Some(&None));
577        assert!(!hashed_state.storages.contains_key(&hashed_address));
578    }
579
580    #[derive(Default)]
581    struct CountingSink {
582        access_hints: AtomicUsize,
583        state_updates: AtomicUsize,
584        hashed_state_updates: AtomicUsize,
585        finished_updates: AtomicUsize,
586    }
587
588    impl StateRootSink for CountingSink {
589        fn on_access_hint(&self, hint: StateAccessHint) {
590            assert_eq!(hint.accounts, vec![B256::repeat_byte(0x01)]);
591            assert_eq!(
592                hint.storages.get(&B256::repeat_byte(0x02)),
593                Some(&vec![B256::repeat_byte(0x03)])
594            );
595            self.access_hints.fetch_add(1, Ordering::Relaxed);
596        }
597
598        fn on_state_update(&self, state: EvmState) {
599            assert!(state.is_empty());
600            self.state_updates.fetch_add(1, Ordering::Relaxed);
601        }
602
603        fn on_hashed_state_update(&self, state: HashedPostState) {
604            assert!(state.accounts.is_empty());
605            assert!(state.storages.is_empty());
606            self.hashed_state_updates.fetch_add(1, Ordering::Relaxed);
607        }
608
609        fn on_updates_finished(&self) {
610            self.finished_updates.fetch_add(1, Ordering::Relaxed);
611        }
612    }
613
614    #[test]
615    fn state_access_hint_converts_to_sparse_targets() {
616        let account = B256::repeat_byte(0x01);
617        let storage_account = B256::repeat_byte(0x02);
618        let storage_slot = B256::repeat_byte(0x03);
619
620        let mut storages = B256Map::default();
621        storages.insert(storage_account, vec![storage_slot]);
622        let hint = StateAccessHint { accounts: vec![account], storages };
623
624        let targets = MultiProofTargetsV2::from(hint);
625        assert_eq!(targets.account_targets.len(), 1);
626        assert_eq!(targets.account_targets[0].key(), account);
627        assert_eq!(targets.storage_targets.len(), 1);
628        assert_eq!(targets.storage_targets[&storage_account].len(), 1);
629        assert_eq!(targets.storage_targets[&storage_account][0].key(), storage_slot);
630
631        let hint = StateAccessHint::from(targets);
632        assert_eq!(hint.accounts, vec![account]);
633        assert_eq!(hint.storages.len(), 1);
634        assert_eq!(hint.storages[&storage_account], vec![storage_slot]);
635    }
636
637    #[test]
638    fn state_root_capabilities_forward_to_sink() {
639        let sink = Arc::new(CountingSink::default());
640
641        let hint_stream = StateRootHintStream::new(sink.clone());
642        let mut storages = B256Map::default();
643        storages.insert(B256::repeat_byte(0x02), vec![B256::repeat_byte(0x03)]);
644        hint_stream
645            .on_access_hint(StateAccessHint { accounts: vec![B256::repeat_byte(0x01)], storages });
646
647        let updates = StateRootUpdateStream::new(sink.clone());
648        updates.on_hashed_state_update(HashedPostState::default());
649        updates.finish();
650
651        {
652            let mut hook = StateRootUpdateStream::new(sink.clone()).into_state_hook();
653            hook.on_state(EvmState::default());
654        }
655
656        assert_eq!(sink.access_hints.load(Ordering::Relaxed), 1);
657        assert_eq!(sink.state_updates.load(Ordering::Relaxed), 1);
658        assert_eq!(sink.hashed_state_updates.load(Ordering::Relaxed), 1);
659        assert_eq!(sink.finished_updates.load(Ordering::Relaxed), 2);
660    }
661
662    /// A hook dropped by a panic unwind must not finish the stream: the updates are
663    /// incomplete, and a finish marker would make the task compute a root from them.
664    #[test]
665    fn hook_dropped_during_panic_does_not_finish_stream() {
666        let sink = Arc::new(CountingSink::default());
667        let hook = StateRootUpdateStream::new(sink.clone()).into_state_hook();
668
669        let result = std::thread::spawn(move || {
670            let _hook = hook;
671            panic!("execution died mid-block");
672        })
673        .join();
674
675        assert!(result.is_err());
676        assert_eq!(sink.finished_updates.load(Ordering::Relaxed), 0);
677    }
678
679    /// The authoritative capability is a single slot: taking it as a hook and then again as
680    /// a hashed update stream (or in any other combination) must panic.
681    #[test]
682    #[should_panic(expected = "authoritative update capability already taken")]
683    fn authoritative_capability_can_only_be_taken_once() {
684        let (updates_tx, _updates_rx) = crossbeam_channel::unbounded();
685        let (cancel_guard, _cancel_rx) = StateRootTaskCancelGuard::channel();
686        let (_state_root_tx, state_root_rx) = std::sync::mpsc::channel();
687        let (_hashed_state_tx, hashed_state_rx) = std::sync::mpsc::channel();
688        let mut handle = StateRootHandle::new(
689            B256::ZERO,
690            updates_tx,
691            cancel_guard,
692            state_root_rx,
693            hashed_state_rx,
694        );
695
696        let _hook = handle.take_execution_hook();
697        let _ = handle.take_hashed_update_stream();
698    }
699
700    /// Lifecycle of the opaque handle a strategy hands to the payload builder: the execution
701    /// hook streams updates into the sink and signals completion on drop, the hashed-state
702    /// receiver can be taken exactly once, and the outcome arrives through the state-root
703    /// channel.
704    #[test]
705    fn payload_state_root_handle_lifecycle() {
706        let sink = Arc::new(CountingSink::default());
707        let hook = StateRootUpdateStream::new(sink.clone()).into_state_hook();
708
709        let (state_root_tx, state_root_rx) = std::sync::mpsc::channel();
710        let (hashed_state_tx, hashed_state_rx) = std::sync::mpsc::channel();
711        let mut handle =
712            PayloadStateRootHandle::new("test", Some(hook), state_root_rx, Some(hashed_state_rx));
713
714        assert_eq!(handle.name(), "test");
715
716        {
717            let mut hook = handle.take_state_hook();
718            hook.on_state(EvmState::default());
719        }
720        assert_eq!(sink.state_updates.load(Ordering::Relaxed), 1);
721        assert_eq!(sink.finished_updates.load(Ordering::Relaxed), 1);
722
723        hashed_state_tx.send(Arc::new(HashedPostState::default())).unwrap();
724        let rx = handle.try_take_hashed_state_rx().expect("first take returns the receiver");
725        assert!(rx.recv().is_ok());
726        assert!(handle.try_take_hashed_state_rx().is_none(), "second take returns None");
727
728        state_root_tx
729            .send(Ok(StateRootComputeOutcome {
730                state_root: B256::repeat_byte(0x42),
731                trie_updates: Arc::new(TrieUpdates::default()),
732                hashed_state: Arc::new(HashedPostState::default()),
733                #[cfg(feature = "trie-debug")]
734                debug_recorders: Vec::new(),
735            }))
736            .unwrap();
737        let outcome = handle.state_root().expect("outcome is delivered");
738        assert_eq!(outcome.state_root, B256::repeat_byte(0x42));
739    }
740
741    #[test]
742    #[should_panic(expected = "state_root already taken")]
743    fn payload_state_root_receiver_can_only_be_taken_once() {
744        let (_state_root_tx, state_root_rx) = std::sync::mpsc::channel();
745        let mut handle = PayloadStateRootHandle::new("test", None, state_root_rx, None);
746
747        let _state_root_rx = handle.take_state_root_rx();
748        let _ = handle.take_state_root_rx();
749    }
750
751    #[test]
752    fn payload_state_root_receiver_retains_cancellation() {
753        let (updates_tx, _updates_rx) = crossbeam_channel::unbounded();
754        let (cancel_guard, cancel_rx) = StateRootTaskCancelGuard::channel();
755        let (_state_root_tx, state_root_rx) = std::sync::mpsc::channel();
756        let (_hashed_state_tx, hashed_state_rx) = std::sync::mpsc::channel();
757        let mut handle = StateRootHandle::new(
758            B256::ZERO,
759            updates_tx,
760            cancel_guard,
761            state_root_rx,
762            hashed_state_rx,
763        )
764        .into_payload_state_root_handle();
765
766        let state_root_rx = handle.take_state_root_rx();
767        assert!(matches!(
768            state_root_rx.recv_timeout(Duration::ZERO),
769            Err(std::sync::mpsc::RecvTimeoutError::Timeout)
770        ));
771        assert!(matches!(cancel_rx.try_recv(), Err(crossbeam_channel::TryRecvError::Empty)));
772
773        drop(handle);
774        assert!(matches!(
775            cancel_rx.recv_timeout(Duration::from_secs(1)),
776            Err(crossbeam_channel::RecvTimeoutError::Disconnected)
777        ));
778    }
779}