Skip to main content

reth_chain_state/
preserved_sparse_trie.rs

1//! Preserved sparse trie for reuse across payload validations.
2
3use alloy_primitives::B256;
4use reth_trie_sparse::SparseStateTrie;
5use std::{fmt, sync::mpsc::Receiver};
6use tracing::debug;
7
8/// Type alias for the sparse trie type used in preservation.
9pub type SparseTrie = SparseStateTrie;
10
11/// A preserved sparse trie that can be reused across payload validations.
12pub struct PreservedSparseTrie {
13    /// The preserved sparse state trie, or a handle to wait for it.
14    trie: PreservedSparseTrieInner,
15    /// Hash of the block whose post-state this trie represents.
16    ///
17    /// Used to verify continuity: a new payload's parent hash must match this before the existing
18    /// sparse trie nodes can be reused.
19    block_hash: B256,
20    /// Parent block hash of the earliest overlay state covered by this trie.
21    anchor_hash: B256,
22}
23
24impl fmt::Debug for PreservedSparseTrie {
25    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
26        f.debug_struct("PreservedSparseTrie")
27            .field("block_hash", &self.block_hash)
28            .field("anchor_hash", &self.anchor_hash)
29            .finish_non_exhaustive()
30    }
31}
32
33impl PreservedSparseTrie {
34    /// Creates a new anchored preserved trie.
35    ///
36    /// The `block_hash` identifies the trie's post-state. The `anchor_hash` is the parent block
37    /// hash of the earliest overlay state covered by the trie.
38    pub const fn anchored(trie: SparseTrie, block_hash: B256, anchor_hash: B256) -> Self {
39        Self { trie: PreservedSparseTrieInner::Ready(trie), block_hash, anchor_hash }
40    }
41
42    /// Associates a trie still being finalized with its completed block and pruning anchor.
43    pub const fn pending(trie: Receiver<SparseTrie>, block_hash: B256, anchor_hash: B256) -> Self {
44        Self { trie: PreservedSparseTrieInner::Pending(trie), block_hash, anchor_hash }
45    }
46
47    /// Returns the hash of the block whose post-state this trie represents.
48    pub const fn block_hash(&self) -> B256 {
49        self.block_hash
50    }
51
52    /// Returns the parent block hash of the earliest overlay state covered by this trie.
53    pub const fn anchor_hash(&self) -> B256 {
54        self.anchor_hash
55    }
56
57    /// Consumes self and returns the trie if it can be reused for the parent block.
58    ///
59    /// If the parent block hash does not match the preserved trie's block hash, this drops the
60    /// trie and returns `None` so the caller can create a fresh sparse trie.
61    pub fn into_trie_for(
62        self,
63        parent_hash: B256,
64    ) -> Result<Option<SparseTrie>, PreservedSparseTrieError> {
65        if self.block_hash == parent_hash {
66            let trie = match self.trie {
67                PreservedSparseTrieInner::Ready(trie) => trie,
68                PreservedSparseTrieInner::Pending(rx) => match rx.recv() {
69                    Ok(trie) => trie,
70                    Err(_) => {
71                        return Err(PreservedSparseTrieError::ProducerDropped {
72                            block_hash: self.block_hash,
73                        })
74                    }
75                },
76            };
77            debug!(
78                target: "engine::tree::payload_processor",
79                block_hash = %self.block_hash,
80                anchor_hash = %self.anchor_hash,
81                "Reusing anchored sparse trie for continuation payload"
82            );
83            Ok(Some(trie))
84        } else {
85            debug!(
86                target: "engine::tree::payload_processor",
87                block_hash = %self.block_hash,
88                anchor_hash = %self.anchor_hash,
89                %parent_hash,
90                "Dropping anchored sparse trie - parent hash mismatch"
91            );
92            Ok(None)
93        }
94    }
95}
96
97/// Error returned when consuming a preserved sparse trie.
98#[derive(Debug, Clone, Copy, PartialEq, Eq)]
99pub enum PreservedSparseTrieError {
100    /// The producer of a pending preserved sparse trie dropped before publishing it.
101    ProducerDropped {
102        /// The block hash the pending trie was expected to represent.
103        block_hash: B256,
104    },
105}
106
107impl fmt::Display for PreservedSparseTrieError {
108    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
109        match self {
110            Self::ProducerDropped { block_hash } => {
111                write!(f, "pending preserved sparse trie producer dropped for {block_hash}")
112            }
113        }
114    }
115}
116
117impl std::error::Error for PreservedSparseTrieError {}
118
119#[allow(clippy::large_enum_variant)]
120enum PreservedSparseTrieInner {
121    Ready(SparseTrie),
122    Pending(Receiver<SparseTrie>),
123}
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128    use std::sync::mpsc;
129
130    #[test]
131    fn pending_trie_exposes_block_hash_before_completion() {
132        let block_hash = B256::with_last_byte(1);
133        let anchor_hash = B256::with_last_byte(2);
134        let (tx, rx) = mpsc::channel();
135        let preserved = PreservedSparseTrie::pending(rx, block_hash, anchor_hash);
136
137        assert_eq!(preserved.block_hash(), block_hash);
138        assert_eq!(preserved.anchor_hash(), anchor_hash);
139        tx.send(SparseTrie::default()).unwrap();
140        assert!(preserved.into_trie_for(block_hash).unwrap().is_some());
141    }
142
143    #[test]
144    fn pending_trie_with_mismatched_parent_does_not_wait() {
145        let block_hash = B256::with_last_byte(1);
146        let other_block_hash = B256::with_last_byte(2);
147        let anchor_hash = B256::with_last_byte(3);
148        let (tx, rx) = mpsc::channel();
149        let preserved = PreservedSparseTrie::pending(rx, block_hash, anchor_hash);
150
151        assert!(preserved.into_trie_for(other_block_hash).unwrap().is_none());
152        assert!(tx.send(SparseTrie::default()).is_err());
153    }
154}