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