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