Skip to main content

reth_provider/providers/database/
save_blocks.rs

1use alloy_consensus::BlockHeader;
2use alloy_eips::BlockNumHash;
3use alloy_primitives::BlockNumber;
4use reth_chain_state::ExecutedBlock;
5use reth_ethereum_primitives::EthPrimitives;
6use reth_primitives_traits::NodePrimitives;
7
8/// Input for advancing the engine's two persistence frontiers.
9///
10/// The Finish checkpoint's block number (`db_tip`) tracks complete block, execution, and history
11/// data. `Finish.partial_state_trie` tracks the hashed-state/trie frontier, which may lag behind it
12/// while a suffix of blocks is retained in memory as a mask. The hashed-state/trie tables are not
13/// a complete snapshot at `db_tip` while this suffix exists; the database and in-memory mask
14/// together represent the canonical state.
15///
16/// `blocks` contains every canonical block in `(prev_partial_state_trie, new_db_tip]`, ordered from
17/// oldest to newest. The four frontiers derive the ranges written during this operation:
18///
19/// - `(prev_db_tip, new_db_tip]`: persist block, execution, and history data, if the database
20///   frontier advances;
21/// - `(prev_partial_state_trie, new_partial_state_trie]`: consider hashed-state/trie updates for
22///   persistence; and
23/// - `(new_partial_state_trie, new_db_tip]`: retain as the masking suffix and do not persist its
24///   hashed-state/trie updates.
25///
26/// Updates in the second range that are overwritten by the masking suffix are also omitted. This
27/// is the work partial persistence avoids: only the newest value needs to be applied later when
28/// the masking block leaves memory. If both new frontiers are equal, the masking suffix is empty
29/// and hashed state/trie are fully flushed through `new_db_tip`.
30///
31/// Genesis must already be initialized because this input only advances an existing database tip.
32#[derive(Debug)]
33pub struct SaveBlocksInput<N: NodePrimitives = EthPrimitives> {
34    blocks: Vec<ExecutedBlock<N>>,
35    prev_db_tip: BlockNumber,
36    prev_partial_state_trie: BlockNumber,
37    new_db_tip: BlockNumber,
38    new_partial_state_trie: BlockNumber,
39}
40
41impl<N: NodePrimitives> SaveBlocksInput<N> {
42    /// Creates an input that advances the existing frontiers to `new_db_tip` and
43    /// `new_partial_state_trie`.
44    ///
45    /// Either frontier may advance independently. `new_partial_state_trie` may remain behind
46    /// `prev_db_tip` while a masking window grows, and `new_db_tip` may remain unchanged while the
47    /// state/trie frontier catches up.
48    ///
49    /// # Panics
50    ///
51    /// Panics if either frontier moves backwards, neither frontier advances, the state/trie
52    /// frontier exceeds the database frontier, or `blocks` is not the exact contiguous range
53    /// `(prev_partial_state_trie, new_db_tip]`.
54    pub fn new(
55        blocks: Vec<ExecutedBlock<N>>,
56        prev_db_tip: BlockNumber,
57        prev_partial_state_trie: BlockNumber,
58        new_db_tip: BlockNumber,
59        new_partial_state_trie: BlockNumber,
60    ) -> Self {
61        assert!(
62            prev_partial_state_trie <= prev_db_tip,
63            "previous state/trie tip must not exceed previous database tip"
64        );
65        assert!(prev_db_tip <= new_db_tip, "database tip must not move backwards");
66        assert!(
67            prev_partial_state_trie <= new_partial_state_trie,
68            "state/trie tip must not move backwards"
69        );
70        assert!(
71            prev_db_tip < new_db_tip || prev_partial_state_trie < new_partial_state_trie,
72            "at least one persistence frontier must advance"
73        );
74        assert!(
75            new_partial_state_trie <= new_db_tip,
76            "new state/trie tip must not exceed new database tip"
77        );
78
79        let expected_len = new_db_tip.saturating_sub(prev_partial_state_trie) as usize;
80        assert_eq!(
81            blocks.len(),
82            expected_len,
83            "blocks must cover (prev_partial_state_trie, new_db_tip]"
84        );
85        assert!(
86            blocks.iter().enumerate().all(|(index, block)| {
87                block.recovered_block().number() == prev_partial_state_trie + index as u64 + 1
88            }),
89            "blocks must cover (prev_partial_state_trie, new_db_tip]"
90        );
91
92        Self { blocks, prev_db_tip, prev_partial_state_trie, new_db_tip, new_partial_state_trie }
93    }
94
95    /// Returns the previous Finish checkpoint block number.
96    pub const fn prev_db_tip(&self) -> BlockNumber {
97        self.prev_db_tip
98    }
99
100    /// Returns the previous `Finish.partial_state_trie` frontier.
101    pub const fn prev_partial_state_trie(&self) -> BlockNumber {
102        self.prev_partial_state_trie
103    }
104
105    /// Returns the new Finish checkpoint block number.
106    pub const fn new_db_tip(&self) -> BlockNumber {
107        self.new_db_tip
108    }
109
110    /// Returns the new `Finish.partial_state_trie` frontier.
111    pub const fn new_partial_state_trie(&self) -> BlockNumber {
112        self.new_partial_state_trie
113    }
114
115    /// Returns the block at the new Finish checkpoint.
116    pub fn last_block(&self) -> BlockNumHash {
117        self.blocks
118            .last()
119            .expect("constructor ensures a non-empty block range")
120            .recovered_block()
121            .num_hash()
122    }
123
124    /// Returns the first block whose block, execution, and history data will be persisted.
125    pub fn first_persist_rest_block(&self) -> Option<&ExecutedBlock<N>> {
126        self.persist_rest_blocks().first()
127    }
128
129    /// Returns newly persisted blocks whose block, execution, and history data should be written.
130    pub fn persist_rest_blocks(&self) -> &[ExecutedBlock<N>] {
131        &self.blocks[(self.prev_db_tip - self.prev_partial_state_trie) as usize..]
132    }
133
134    /// Returns all blocks whose hashed-state/trie updates are candidates for persistence.
135    ///
136    /// Updates overwritten by [`Self::state_trie_masking_blocks`] are filtered out by the writer.
137    pub fn state_trie_blocks(&self) -> &[ExecutedBlock<N>] {
138        &self.blocks[..(self.new_partial_state_trie - self.prev_partial_state_trie) as usize]
139    }
140
141    /// Returns the fixed suffix whose hashed-state/trie updates remain in memory.
142    ///
143    /// This suffix suppresses older updates to the same hashed keys and trie nodes. It is also the
144    /// overlay required to use the database at the new Finish checkpoint.
145    pub fn state_trie_masking_blocks(&self) -> &[ExecutedBlock<N>] {
146        &self.blocks[(self.new_partial_state_trie - self.prev_partial_state_trie) as usize..]
147    }
148}
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153    use reth_chain_state::test_utils::TestBlockBuilder;
154
155    #[test]
156    fn splits_frontier_ranges() {
157        let blocks = TestBlockBuilder::eth().get_executed_blocks(6..23).collect();
158        let input = SaveBlocksInput::new(blocks, 11, 5, 22, 16);
159
160        assert_eq!(
161            input
162                .persist_rest_blocks()
163                .iter()
164                .map(|block| block.recovered_block().number())
165                .collect::<Vec<_>>(),
166            (12..=22).collect::<Vec<_>>()
167        );
168        assert_eq!(
169            input
170                .state_trie_blocks()
171                .iter()
172                .map(|block| block.recovered_block().number())
173                .collect::<Vec<_>>(),
174            (6..=16).collect::<Vec<_>>()
175        );
176        assert_eq!(
177            input
178                .state_trie_masking_blocks()
179                .iter()
180                .map(|block| block.recovered_block().number())
181                .collect::<Vec<_>>(),
182            (17..=22).collect::<Vec<_>>()
183        );
184    }
185
186    #[test]
187    fn grows_masking_window_without_forcing_new_state_writes() {
188        let blocks = TestBlockBuilder::eth().get_executed_blocks(13..17).collect();
189        let input = SaveBlocksInput::new(blocks, 15, 12, 16, 13);
190
191        assert_eq!(input.first_persist_rest_block().unwrap().recovered_block().number(), 16);
192        assert_eq!(input.state_trie_blocks()[0].recovered_block().number(), 13);
193        assert_eq!(
194            input
195                .state_trie_masking_blocks()
196                .iter()
197                .map(|block| block.recovered_block().number())
198                .collect::<Vec<_>>(),
199            vec![14, 15, 16]
200        );
201    }
202
203    #[test]
204    fn advances_only_state_trie_frontier() {
205        let blocks = TestBlockBuilder::eth().get_executed_blocks(1..2).collect();
206        let input = SaveBlocksInput::new(blocks, 1, 0, 1, 1);
207
208        assert!(input.persist_rest_blocks().is_empty());
209        assert!(input.first_persist_rest_block().is_none());
210        assert_eq!(input.state_trie_blocks()[0].recovered_block().number(), 1);
211        assert!(input.state_trie_masking_blocks().is_empty());
212    }
213
214    #[test]
215    #[should_panic(expected = "at least one persistence frontier must advance")]
216    fn requires_a_frontier_to_advance() {
217        let _ = SaveBlocksInput::<EthPrimitives>::new(vec![], 1, 1, 1, 1);
218    }
219
220    #[test]
221    #[should_panic(expected = "blocks must cover (prev_partial_state_trie, new_db_tip]")]
222    fn requires_contiguous_blocks() {
223        let blocks = TestBlockBuilder::eth().get_executed_blocks(2..4).collect();
224        let _ = SaveBlocksInput::new(blocks, 0, 0, 2, 2);
225    }
226}