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`] 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::TrieUpdatesSorted, 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<TrieUpdatesSorted>,
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            on_payload_built: None,
186        }
187    }
188}
189
190/// Guard that cancels a state-root task when dropped.
191///
192/// The task watches the paired receiver in its event loop. No message is ever sent: the guard
193/// dropping disconnects the channel, which the task treats as the consumer abandoning the
194/// computation (for example on a timeout fallback or when a payload job is dropped unused).
195#[derive(Debug)]
196pub struct StateRootTaskCancelGuard(#[allow(dead_code)] crossbeam_channel::Sender<()>);
197
198impl StateRootTaskCancelGuard {
199    /// Creates a guard and the receiver a task watches for cancellation.
200    pub fn channel() -> (Self, crossbeam_channel::Receiver<()>) {
201        let (tx, rx) = crossbeam_channel::bounded(0);
202        (Self(tx), rx)
203    }
204}
205
206/// Opaque state-root task handle passed to payload builders.
207pub struct PayloadStateRootHandle {
208    name: &'static str,
209    /// Execution hook that streams per-transaction updates; taken once when building starts.
210    hook: Option<StateRootUpdateHook>,
211    /// Cancels the backing task when the handle is dropped without consuming the result.
212    cancel_guard: Option<StateRootTaskCancelGuard>,
213    state_root_rx:
214        Option<std::sync::mpsc::Receiver<Result<StateRootComputeOutcome, StateRootTaskError>>>,
215    hashed_state_rx: Option<std::sync::mpsc::Receiver<Arc<HashedPostState>>>,
216    /// Returns retained state to the engine once the completed block's identity is known.
217    on_payload_built: Option<Box<dyn FnOnce(B256, B256) + Send>>,
218}
219
220impl fmt::Debug for PayloadStateRootHandle {
221    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
222        f.debug_struct("PayloadStateRootHandle")
223            .field("name", &self.name)
224            .field("has_hook", &self.hook.is_some())
225            .field("has_cancel_guard", &self.cancel_guard.is_some())
226            .field("has_state_root_rx", &self.state_root_rx.is_some())
227            .field("has_hashed_state_rx", &self.hashed_state_rx.is_some())
228            .finish()
229    }
230}
231
232impl PayloadStateRootHandle {
233    /// Creates an opaque payload state-root handle.
234    ///
235    /// Tasks with a drop-to-cancel guard should attach it via the `StateRootHandle`
236    /// conversion; handles created here rely on their own task lifecycle.
237    pub const fn new(
238        name: &'static str,
239        hook: Option<StateRootUpdateHook>,
240        state_root_rx: std::sync::mpsc::Receiver<
241            Result<StateRootComputeOutcome, StateRootTaskError>,
242        >,
243        hashed_state_rx: Option<std::sync::mpsc::Receiver<Arc<HashedPostState>>>,
244    ) -> Self {
245        Self {
246            name,
247            hook,
248            cancel_guard: None,
249            state_root_rx: Some(state_root_rx),
250            hashed_state_rx,
251            on_payload_built: None,
252        }
253    }
254
255    /// Attaches a callback receiving the completed payload's block hash and state root.
256    ///
257    /// The callback must verify that any retained state matches the returned payload.
258    pub fn with_on_payload_built(
259        mut self,
260        callback: impl FnOnce(B256, B256) + Send + 'static,
261    ) -> Self {
262        self.on_payload_built = Some(Box::new(callback));
263        self
264    }
265
266    /// Takes the callback to invoke after a successful build, before returning its payload.
267    /// Aborted, cancelled, or failed builds must drop the callback without invoking it.
268    pub fn take_on_payload_built(&mut self) -> Option<Box<dyn FnOnce(B256, B256) + Send>> {
269        self.on_payload_built.take()
270    }
271
272    /// Returns the task name used in logs.
273    pub const fn name(&self) -> &'static str {
274        self.name
275    }
276
277    /// Takes the state hook that streams execution updates and finishes the stream on drop.
278    ///
279    /// # Panics
280    ///
281    /// If the handle was created without an execution hook, or the hook was already taken.
282    pub const fn take_state_hook(&mut self) -> StateRootUpdateHook {
283        self.hook.take().expect("payload state root task missing execution hook")
284    }
285
286    /// Awaits the state root computation result.
287    ///
288    /// # Panics
289    ///
290    /// If called more than once.
291    pub fn state_root(&mut self) -> Result<StateRootComputeOutcome, StateRootTaskError> {
292        self.state_root_rx
293            .take()
294            .expect("state_root already taken")
295            .recv()
296            .map_err(|_| StateRootTaskError::Other("state root task dropped".to_string()))?
297    }
298
299    /// Takes the state root receiver for use with custom waiting logic (e.g., timeouts).
300    ///
301    /// Dropping the handle continues to cancel the backing task if it owns a cancellation guard.
302    ///
303    /// # Panics
304    ///
305    /// If called more than once.
306    pub const fn take_state_root_rx(
307        &mut self,
308    ) -> std::sync::mpsc::Receiver<Result<StateRootComputeOutcome, StateRootTaskError>> {
309        self.state_root_rx.take().expect("state_root already taken")
310    }
311
312    /// Takes the hashed state receiver, if the handle was built with one and it was not taken
313    /// yet.
314    pub const fn try_take_hashed_state_rx(
315        &mut self,
316    ) -> Option<std::sync::mpsc::Receiver<Arc<HashedPostState>>> {
317        self.hashed_state_rx.take()
318    }
319}
320
321/// Hashed account and storage keys that a state-root task may want to prefetch.
322///
323/// Hints are not authoritative. They may be missing, duplicated, stale, or ignored by a task.
324/// The conversions from and to proof-target types allocate; that cost is accepted because
325/// hints are produced on prewarm workers, off the block-execution thread.
326#[derive(Debug, Clone, Default)]
327pub struct StateAccessHint {
328    /// Hashed account keys that may be touched later in the block.
329    pub accounts: Vec<B256>,
330    /// Hashed storage keys keyed by hashed account.
331    pub storages: B256Map<Vec<B256>>,
332}
333
334impl From<MultiProofTargetsV2> for StateAccessHint {
335    fn from(targets: MultiProofTargetsV2) -> Self {
336        Self {
337            accounts: targets.account_targets.into_iter().map(|target| target.key()).collect(),
338            storages: targets
339                .storage_targets
340                .into_iter()
341                .map(|(account, slots)| {
342                    (account, slots.into_iter().map(|target| target.key()).collect())
343                })
344                .collect(),
345        }
346    }
347}
348
349impl From<StateAccessHint> for MultiProofTargetsV2 {
350    fn from(hint: StateAccessHint) -> Self {
351        Self {
352            account_targets: hint.accounts.into_iter().map(ProofV2Target::from).collect(),
353            storage_targets: hint
354                .storages
355                .into_iter()
356                .map(|(account, slots)| {
357                    (account, slots.into_iter().map(ProofV2Target::from).collect())
358                })
359                .collect(),
360        }
361    }
362}
363
364/// Semantic update stream consumed by state-root tasks.
365pub trait StateRootSink: Send + Sync + 'static {
366    /// Best-effort access hint from transaction prewarming.
367    fn on_access_hint(&self, _hint: StateAccessHint) {}
368
369    /// Authoritative state update from normal block execution.
370    fn on_state_update(&self, state: EvmState);
371
372    /// Authoritative pre-hashed state update, currently used by BAL streaming.
373    fn on_hashed_state_update(&self, state: HashedPostState);
374
375    /// Signals that no more authoritative state updates are expected.
376    fn on_updates_finished(&self);
377}
378
379/// Hint-only view of a state-root stream.
380#[derive(Clone)]
381pub struct StateRootHintStream {
382    inner: Arc<dyn StateRootSink>,
383}
384
385impl fmt::Debug for StateRootHintStream {
386    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
387        f.debug_struct("StateRootHintStream").finish_non_exhaustive()
388    }
389}
390
391impl StateRootHintStream {
392    /// Creates a new hint stream view.
393    pub fn new(inner: Arc<dyn StateRootSink>) -> Self {
394        Self { inner }
395    }
396
397    /// Emits a best-effort access hint.
398    pub fn on_access_hint(&self, hint: StateAccessHint) {
399        self.inner.on_access_hint(hint);
400    }
401}
402
403/// Authoritative update capability of a state-root stream.
404///
405/// Exactly one of these exists per state-root task, so exactly one producer can end the
406/// update stream: either the EVM state hook made with [`Self::into_state_hook`] (finishes on
407/// drop) or a pre-hashed update producer such as BAL streaming (calls [`Self::finish`]). The
408/// type is deliberately not `Clone` and finishing consumes it, so a second end-of-stream
409/// signal cannot be produced.
410///
411/// Dropping the stream without calling [`Self::finish`] (for example when a producer dies)
412/// deliberately does not finish it: an unfinished stream means the updates are incomplete,
413/// and the task must not compute a root from them.
414pub struct StateRootUpdateStream {
415    inner: Arc<dyn StateRootSink>,
416}
417
418impl fmt::Debug for StateRootUpdateStream {
419    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
420        f.debug_struct("StateRootUpdateStream").finish_non_exhaustive()
421    }
422}
423
424impl StateRootUpdateStream {
425    /// Creates a new authoritative update stream backed by the given sink.
426    pub fn new(inner: Arc<dyn StateRootSink>) -> Self {
427        Self { inner }
428    }
429
430    /// Emits an authoritative pre-hashed state update.
431    pub fn on_hashed_state_update(&self, state: HashedPostState) {
432        self.inner.on_hashed_state_update(state);
433    }
434
435    /// Finishes the authoritative update stream.
436    pub fn finish(self) {
437        self.inner.on_updates_finished();
438    }
439
440    /// Converts this capability into an EVM state hook that finishes the stream on drop.
441    ///
442    /// See [`StateRootUpdateHook`] for why the hook finishes on drop while the bare stream
443    /// does not, and how a panic during execution is excluded from that.
444    pub fn into_state_hook(self) -> StateRootUpdateHook {
445        StateRootUpdateHook { inner: self.inner }
446    }
447}
448
449/// EVM hook that forwards state updates into a [`StateRootSink`].
450///
451/// Dropping the hook signals the end of the update stream, so the hook is deliberately not
452/// `Clone`: a second copy would fire a spurious end-of-stream signal.
453///
454/// Unlike [`StateRootUpdateStream::finish`], the end of the stream is signaled by drop and
455/// not by an explicit call, because the EVM owns the hook until it is dropped and gives it no
456/// other end-of-execution signal. A drop during a panic unwind is excluded: execution died
457/// mid-block, so the stream stays unfinished and the task reports an error instead of
458/// computing a root from incomplete updates. Execution that fails by returning an error still
459/// drops the hook normally and finishes the stream; the caller abandons the result in that
460/// case, and the stored trie is rejected by the anchor check on the next block.
461pub struct StateRootUpdateHook {
462    inner: Arc<dyn StateRootSink>,
463}
464
465impl fmt::Debug for StateRootUpdateHook {
466    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
467        f.debug_struct("StateRootUpdateHook").finish_non_exhaustive()
468    }
469}
470
471impl OnStateHook for StateRootUpdateHook {
472    fn on_state(&mut self, state: EvmState) {
473        self.inner.on_state_update(state);
474    }
475}
476
477impl Drop for StateRootUpdateHook {
478    fn drop(&mut self) {
479        // A drop during a panic unwind means execution died mid-block. Leave the stream
480        // unfinished so the task fails instead of computing a root from partial updates.
481        if std::thread::panicking() {
482            return;
483        }
484        self.inner.on_updates_finished();
485    }
486}
487
488#[derive(Debug, Clone)]
489struct SparseTrieStateRootSink {
490    sender: crossbeam_channel::Sender<StateRootMessage>,
491}
492
493impl SparseTrieStateRootSink {
494    const fn new(sender: crossbeam_channel::Sender<StateRootMessage>) -> Self {
495        Self { sender }
496    }
497}
498
499impl StateRootSink for SparseTrieStateRootSink {
500    fn on_access_hint(&self, hint: StateAccessHint) {
501        let _ = self.sender.send(StateRootMessage::PrefetchProofs(hint.into()));
502    }
503
504    fn on_state_update(&self, state: EvmState) {
505        let _ = self.sender.send(StateRootMessage::StateUpdate(state));
506    }
507
508    fn on_hashed_state_update(&self, state: HashedPostState) {
509        let _ = self.sender.send(StateRootMessage::HashedStateUpdate(state));
510    }
511
512    fn on_updates_finished(&self) {
513        let _ = self.sender.send(StateRootMessage::FinishedStateUpdates);
514    }
515}
516
517/// Converts [`EvmState`] to [`HashedPostState`] by keccak256-hashing addresses and storage slots.
518pub fn evm_state_to_hashed_post_state(update: EvmState) -> HashedPostState {
519    let mut hashed_state = HashedPostState::with_capacity(update.len());
520
521    for (address, account) in update {
522        if account.is_touched() {
523            let hashed_address = keccak256(address);
524            trace!(target: "trie::parallel::sparse", ?address, ?hashed_address, "Adding account to state update");
525
526            let destroyed = account.is_selfdestructed();
527            // EIP-161: a touched account that ends up empty is deleted, so it must be emitted
528            // as a removal rather than as an all-zero account. This mirrors what revm does in
529            // the bundle path (`CacheAccount::touch_empty_eip161`) and what the sibling
530            // producer for this consumer already does in `send_bal_hashed_state`.
531            // An address that never existed and still does not exist is not a deletion: revm
532            // emits no transition for `LoadedNotExisting`. Skipping it matches the bundle
533            // producer; every `None` here becomes a storage-trie cursor walk in `StateRoot`.
534            let deleted = destroyed || (account.is_empty() && !account.is_loaded_as_not_existing());
535            if deleted {
536                hashed_state.accounts.insert(hashed_address, None);
537            } else if account.info != account.original_info() {
538                // A touched but unchanged account produces no bundle transition either.
539                hashed_state.accounts.insert(hashed_address, Some(account.info.into()));
540            }
541
542            let mut changed_storage_iter = account
543                .storage
544                .into_iter()
545                .filter(|(_slot, value)| value.is_changed())
546                .map(|(slot, value)| (keccak256(B256::from(slot)), value.present_value))
547                .peekable();
548
549            if !destroyed && changed_storage_iter.peek().is_some() {
550                hashed_state
551                    .storages
552                    .insert(hashed_address, HashedStorage::from_iter(changed_storage_iter));
553            }
554        }
555    }
556
557    hashed_state
558}
559
560#[cfg(test)]
561mod tests {
562    use super::*;
563    use alloy_primitives::{Address, U256};
564    use revm::state::{Account, EvmStorageSlot, TransactionId};
565    use std::{
566        sync::atomic::{AtomicUsize, Ordering},
567        time::Duration,
568    };
569
570    #[test]
571    fn created_selfdestruct_does_not_emit_storage() {
572        let address = Address::repeat_byte(0x01);
573        let mut account = Account::new_not_existing(TransactionId::ZERO);
574        account.mark_touch();
575        assert!(account.mark_created_locally());
576        assert!(account.mark_selfdestructed_locally());
577        account.info.nonce = 1;
578        account.storage.insert(
579            U256::from(1),
580            EvmStorageSlot::new_changed(U256::ZERO, U256::from(2), TransactionId::ZERO),
581        );
582
583        let hashed_state =
584            evm_state_to_hashed_post_state(EvmState::from_iter([(address, account)]));
585        let hashed_address = keccak256(address);
586
587        assert_eq!(hashed_state.accounts.get(&hashed_address), Some(&None));
588        assert!(!hashed_state.storages.contains_key(&hashed_address));
589    }
590
591    #[test]
592    fn existing_selfdestruct_does_not_emit_storage() {
593        let address = Address::repeat_byte(0x02);
594        let mut account = Account::default();
595        account.info.nonce = 1;
596        account.set_current_info_as_original();
597        account.mark_touch();
598        assert!(account.mark_selfdestructed_locally());
599        account.selfdestruct();
600        account.storage.insert(
601            U256::from(1),
602            EvmStorageSlot::new_changed(U256::ZERO, U256::from(2), TransactionId::ZERO),
603        );
604
605        let hashed_state =
606            evm_state_to_hashed_post_state(EvmState::from_iter([(address, account)]));
607        let hashed_address = keccak256(address);
608
609        assert_eq!(hashed_state.accounts.get(&hashed_address), Some(&None));
610        assert!(!hashed_state.storages.contains_key(&hashed_address));
611    }
612
613    /// An account drained to zero balance, with nonce 0 and no code, is EIP-161-empty and
614    /// canonical execution deletes it. Asserts the converter reports the deletion as `None`,
615    /// matching `HashedPostState::from_bundle_state`, so `write_hashed_state` removes the
616    /// `HashedAccounts` row instead of upserting an all-zero one.
617    #[test]
618    fn emptied_account_is_deleted() {
619        let address = Address::repeat_byte(0x05);
620        let mut account = Account::default();
621        // Pre-state: the account exists and holds a balance.
622        account.info.balance = U256::from(1);
623        account.set_current_info_as_original();
624        // This block drains it. Not selfdestructed: an ordinary value transfer out.
625        account.mark_touch();
626        account.info.balance = U256::ZERO;
627        assert!(account.is_empty(), "the drained account must be EIP-161-empty");
628        assert!(!account.is_selfdestructed());
629
630        let hashed_state =
631            evm_state_to_hashed_post_state(EvmState::from_iter([(address, account)]));
632
633        assert_eq!(hashed_state.accounts.get(&keccak256(address)), Some(&None));
634    }
635
636    /// A pre-existing EIP-161-empty account that is merely touched must be deleted. revm marks it
637    /// for removal (`touch_empty_eip161`) and the `BundleState` path reports `None`. Asserts the
638    /// converter emits the deletion even though `info == original_info()`, so an existing all-zero
639    /// `HashedAccounts` row can be cleared by a touch.
640    #[test]
641    fn touched_preexisting_empty_account_is_deleted() {
642        let address = Address::repeat_byte(0x06);
643        // Empty in the pre-state too: nonce 0, balance 0, no code.
644        let mut account = Account::default();
645        account.set_current_info_as_original();
646        account.mark_touch();
647        assert!(account.is_empty());
648
649        let hashed_state =
650            evm_state_to_hashed_post_state(EvmState::from_iter([(address, account)]));
651
652        assert_eq!(hashed_state.accounts.get(&keccak256(address)), Some(&None));
653    }
654
655    /// The two `HashedPostState` producers that feed the same consumer must agree.
656    ///
657    /// `evm_state_to_hashed_post_state` converts the raw `EvmState` handed to the state hook;
658    /// `HashedPostState::from_bundle_state` converts the `BundleState` revm produces from the
659    /// same execution. Since "perf: avoid hashing the state twice" the engine persists whichever
660    /// one it gets, so a disagreement between them is a disagreement about durable state.
661    ///
662    /// Asserts both producers agree for a touched account drained to EIP-161-empty, which revm
663    /// marks for removal in the bundle path (`CacheAccount::touch_empty_eip161`).
664    #[test]
665    fn matches_bundle_state_for_emptied_account() {
666        use revm::{
667            database::{states::bundle_state::BundleRetention, State},
668            state::AccountInfo,
669            DatabaseCommit,
670        };
671
672        let address = Address::repeat_byte(0x07);
673        let pre = AccountInfo { balance: U256::from(1), ..Default::default() };
674
675        // The EvmState the state hook observes: a funded account drained to empty.
676        let mut account = Account::from(pre.clone());
677        account.mark_touch();
678        account.info.balance = U256::ZERO;
679        let evm_state = EvmState::from_iter([(address, account)]);
680
681        // Same execution, through revm's own bundle machinery.
682        let mut db = State::builder().with_bundle_update().build();
683        db.insert_account(address, pre);
684        db.commit(evm_state.clone());
685        db.merge_transitions(BundleRetention::PlainState);
686        let bundle = db.take_bundle();
687
688        let from_bundle =
689            HashedPostState::from_bundle_state::<reth_trie::KeccakKeyHasher>(bundle.state.iter());
690        let from_hook = evm_state_to_hashed_post_state(evm_state);
691
692        assert_eq!(
693            from_hook.accounts, from_bundle.accounts,
694            "state-hook and bundle producers disagree about durable account state"
695        );
696    }
697
698    /// An account created during the block whose final info is empty. EIP-161 deletes it, so
699    /// the bundle producer reports a removal.
700    #[test]
701    fn created_empty_account_matches_bundle_state() {
702        use revm::{
703            database::{states::bundle_state::BundleRetention, State},
704            DatabaseCommit,
705        };
706
707        let address = Address::repeat_byte(0x08);
708        let mut account = Account::default();
709        account.mark_touch();
710        assert!(account.mark_created_locally());
711        assert!(account.is_empty());
712        let evm_state = EvmState::from_iter([(address, account)]);
713
714        let mut db = State::builder().with_bundle_update().build();
715        db.commit(evm_state.clone());
716        db.merge_transitions(BundleRetention::PlainState);
717        let bundle = db.take_bundle();
718
719        let from_bundle =
720            HashedPostState::from_bundle_state::<reth_trie::KeccakKeyHasher>(bundle.state.iter());
721        let from_hook = evm_state_to_hashed_post_state(evm_state);
722
723        assert_eq!(
724            from_hook.accounts.get(&keccak256(address)).and_then(Option::as_ref),
725            from_bundle.accounts.get(&keccak256(address)).and_then(Option::as_ref),
726            "state-hook and bundle producers disagree about a created-empty account"
727        );
728    }
729
730    /// A zero-value call to an address that never existed leaves it non-existent. revm's bundle
731    /// path emits no transition for it (`CacheAccount::touch_empty_eip161` returns `None` for
732    /// `LoadedNotExisting`). Asserts the converter also emits nothing, rather than a deletion
733    /// that would queue a `HashedAccounts` delete and a storage-trie wipe for an unused address.
734    #[test]
735    fn touched_never_existing_account_matches_bundle_state() {
736        use revm::{
737            database::{states::bundle_state::BundleRetention, State},
738            DatabaseCommit,
739        };
740
741        let address = Address::repeat_byte(0x09);
742        let mut account = Account::new_not_existing(TransactionId::default());
743        account.mark_touch();
744        let evm_state = EvmState::from_iter([(address, account)]);
745
746        let mut db = State::builder().with_bundle_update().build();
747        db.commit(evm_state.clone());
748        db.merge_transitions(BundleRetention::PlainState);
749        let bundle = db.take_bundle();
750
751        let from_bundle =
752            HashedPostState::from_bundle_state::<reth_trie::KeccakKeyHasher>(bundle.state.iter());
753        let from_hook = evm_state_to_hashed_post_state(evm_state);
754
755        assert_eq!(
756            from_hook.accounts, from_bundle.accounts,
757            "state-hook and bundle producers disagree about a never-existing touched account"
758        );
759    }
760
761    /// An account touched without being changed. revm's bundle producer reports nothing for it.
762    /// Asserts the converter agrees, so a block full of zero-value calls does not rewrite
763    /// unchanged `HashedAccounts` rows.
764    #[test]
765    fn touched_unchanged_account_matches_bundle_state() {
766        use revm::{
767            database::{states::bundle_state::BundleRetention, State},
768            state::AccountInfo,
769            DatabaseCommit,
770        };
771
772        let address = Address::repeat_byte(0x0a);
773        let pre = AccountInfo { balance: U256::from(7), nonce: 1, ..Default::default() };
774        let mut account = Account::from(pre.clone());
775        account.mark_touch();
776        assert!(!account.is_empty());
777        let evm_state = EvmState::from_iter([(address, account)]);
778
779        let mut db = State::builder().with_bundle_update().build();
780        db.insert_account(address, pre);
781        db.commit(evm_state.clone());
782        db.merge_transitions(BundleRetention::PlainState);
783        let bundle = db.take_bundle();
784
785        let from_bundle =
786            HashedPostState::from_bundle_state::<reth_trie::KeccakKeyHasher>(bundle.state.iter());
787        let from_hook = evm_state_to_hashed_post_state(evm_state);
788
789        assert_eq!(
790            from_hook.accounts, from_bundle.accounts,
791            "state-hook and bundle producers disagree about a touched but unchanged account"
792        );
793    }
794
795    #[derive(Default)]
796    struct CountingSink {
797        access_hints: AtomicUsize,
798        state_updates: AtomicUsize,
799        hashed_state_updates: AtomicUsize,
800        finished_updates: AtomicUsize,
801    }
802
803    impl StateRootSink for CountingSink {
804        fn on_access_hint(&self, hint: StateAccessHint) {
805            assert_eq!(hint.accounts, vec![B256::repeat_byte(0x01)]);
806            assert_eq!(
807                hint.storages.get(&B256::repeat_byte(0x02)),
808                Some(&vec![B256::repeat_byte(0x03)])
809            );
810            self.access_hints.fetch_add(1, Ordering::Relaxed);
811        }
812
813        fn on_state_update(&self, state: EvmState) {
814            assert!(state.is_empty());
815            self.state_updates.fetch_add(1, Ordering::Relaxed);
816        }
817
818        fn on_hashed_state_update(&self, state: HashedPostState) {
819            assert!(state.accounts.is_empty());
820            assert!(state.storages.is_empty());
821            self.hashed_state_updates.fetch_add(1, Ordering::Relaxed);
822        }
823
824        fn on_updates_finished(&self) {
825            self.finished_updates.fetch_add(1, Ordering::Relaxed);
826        }
827    }
828
829    #[test]
830    fn state_access_hint_converts_to_sparse_targets() {
831        let account = B256::repeat_byte(0x01);
832        let storage_account = B256::repeat_byte(0x02);
833        let storage_slot = B256::repeat_byte(0x03);
834
835        let mut storages = B256Map::default();
836        storages.insert(storage_account, vec![storage_slot]);
837        let hint = StateAccessHint { accounts: vec![account], storages };
838
839        let targets = MultiProofTargetsV2::from(hint);
840        assert_eq!(targets.account_targets.len(), 1);
841        assert_eq!(targets.account_targets[0].key(), account);
842        assert_eq!(targets.storage_targets.len(), 1);
843        assert_eq!(targets.storage_targets[&storage_account].len(), 1);
844        assert_eq!(targets.storage_targets[&storage_account][0].key(), storage_slot);
845
846        let hint = StateAccessHint::from(targets);
847        assert_eq!(hint.accounts, vec![account]);
848        assert_eq!(hint.storages.len(), 1);
849        assert_eq!(hint.storages[&storage_account], vec![storage_slot]);
850    }
851
852    #[test]
853    fn state_root_capabilities_forward_to_sink() {
854        let sink = Arc::new(CountingSink::default());
855
856        let hint_stream = StateRootHintStream::new(sink.clone());
857        let mut storages = B256Map::default();
858        storages.insert(B256::repeat_byte(0x02), vec![B256::repeat_byte(0x03)]);
859        hint_stream
860            .on_access_hint(StateAccessHint { accounts: vec![B256::repeat_byte(0x01)], storages });
861
862        let updates = StateRootUpdateStream::new(sink.clone());
863        updates.on_hashed_state_update(HashedPostState::default());
864        updates.finish();
865
866        {
867            let mut hook = StateRootUpdateStream::new(sink.clone()).into_state_hook();
868            hook.on_state(EvmState::default());
869        }
870
871        assert_eq!(sink.access_hints.load(Ordering::Relaxed), 1);
872        assert_eq!(sink.state_updates.load(Ordering::Relaxed), 1);
873        assert_eq!(sink.hashed_state_updates.load(Ordering::Relaxed), 1);
874        assert_eq!(sink.finished_updates.load(Ordering::Relaxed), 2);
875    }
876
877    /// A hook dropped by a panic unwind must not finish the stream: the updates are
878    /// incomplete, and a finish marker would make the task compute a root from them.
879    #[test]
880    fn hook_dropped_during_panic_does_not_finish_stream() {
881        let sink = Arc::new(CountingSink::default());
882        let hook = StateRootUpdateStream::new(sink.clone()).into_state_hook();
883
884        let result = std::thread::spawn(move || {
885            let _hook = hook;
886            panic!("execution died mid-block");
887        })
888        .join();
889
890        assert!(result.is_err());
891        assert_eq!(sink.finished_updates.load(Ordering::Relaxed), 0);
892    }
893
894    /// The authoritative capability is a single slot: taking it as a hook and then again as
895    /// a hashed update stream (or in any other combination) must panic.
896    #[test]
897    #[should_panic(expected = "authoritative update capability already taken")]
898    fn authoritative_capability_can_only_be_taken_once() {
899        let (updates_tx, _updates_rx) = crossbeam_channel::unbounded();
900        let (cancel_guard, _cancel_rx) = StateRootTaskCancelGuard::channel();
901        let (_state_root_tx, state_root_rx) = std::sync::mpsc::channel();
902        let (_hashed_state_tx, hashed_state_rx) = std::sync::mpsc::channel();
903        let mut handle = StateRootHandle::new(
904            B256::ZERO,
905            updates_tx,
906            cancel_guard,
907            state_root_rx,
908            hashed_state_rx,
909        );
910
911        let _hook = handle.take_execution_hook();
912        let _ = handle.take_hashed_update_stream();
913    }
914
915    /// Lifecycle of the opaque handle a strategy hands to the payload builder: the execution
916    /// hook streams updates into the sink and signals completion on drop, the hashed-state
917    /// receiver can be taken exactly once, and the outcome arrives through the state-root
918    /// channel.
919    #[test]
920    fn payload_state_root_handle_lifecycle() {
921        let sink = Arc::new(CountingSink::default());
922        let hook = StateRootUpdateStream::new(sink.clone()).into_state_hook();
923
924        let (state_root_tx, state_root_rx) = std::sync::mpsc::channel();
925        let (hashed_state_tx, hashed_state_rx) = std::sync::mpsc::channel();
926        let mut handle =
927            PayloadStateRootHandle::new("test", Some(hook), state_root_rx, Some(hashed_state_rx));
928
929        assert_eq!(handle.name(), "test");
930
931        {
932            let mut hook = handle.take_state_hook();
933            hook.on_state(EvmState::default());
934        }
935        assert_eq!(sink.state_updates.load(Ordering::Relaxed), 1);
936        assert_eq!(sink.finished_updates.load(Ordering::Relaxed), 1);
937
938        hashed_state_tx.send(Arc::new(HashedPostState::default())).unwrap();
939        let rx = handle.try_take_hashed_state_rx().expect("first take returns the receiver");
940        assert!(rx.recv().is_ok());
941        assert!(handle.try_take_hashed_state_rx().is_none(), "second take returns None");
942
943        state_root_tx
944            .send(Ok(StateRootComputeOutcome {
945                state_root: B256::repeat_byte(0x42),
946                trie_updates: Arc::new(TrieUpdatesSorted::default()),
947                hashed_state: Arc::new(HashedPostState::default()),
948            }))
949            .unwrap();
950        let outcome = handle.state_root().expect("outcome is delivered");
951        assert_eq!(outcome.state_root, B256::repeat_byte(0x42));
952    }
953
954    #[test]
955    #[should_panic(expected = "state_root already taken")]
956    fn payload_state_root_receiver_can_only_be_taken_once() {
957        let (_state_root_tx, state_root_rx) = std::sync::mpsc::channel();
958        let mut handle = PayloadStateRootHandle::new("test", None, state_root_rx, None);
959
960        let _state_root_rx = handle.take_state_root_rx();
961        let _ = handle.take_state_root_rx();
962    }
963
964    #[test]
965    fn payload_state_root_receiver_retains_cancellation() {
966        let (updates_tx, _updates_rx) = crossbeam_channel::unbounded();
967        let (cancel_guard, cancel_rx) = StateRootTaskCancelGuard::channel();
968        let (_state_root_tx, state_root_rx) = std::sync::mpsc::channel();
969        let (_hashed_state_tx, hashed_state_rx) = std::sync::mpsc::channel();
970        let mut handle = StateRootHandle::new(
971            B256::ZERO,
972            updates_tx,
973            cancel_guard,
974            state_root_rx,
975            hashed_state_rx,
976        )
977        .into_payload_state_root_handle();
978
979        let state_root_rx = handle.take_state_root_rx();
980        assert!(matches!(
981            state_root_rx.recv_timeout(Duration::ZERO),
982            Err(std::sync::mpsc::RecvTimeoutError::Timeout)
983        ));
984        assert!(matches!(cancel_rx.try_recv(), Err(crossbeam_channel::TryRecvError::Empty)));
985
986        drop(handle);
987        assert!(matches!(
988            cancel_rx.recv_timeout(Duration::from_secs(1)),
989            Err(crossbeam_channel::RecvTimeoutError::Disconnected)
990        ));
991    }
992}