Skip to main content

reth_snap_sync/bytecode/
store.rs

1//! Persists contract code ahead of the account range that commits to it.
2//!
3//! Code is content addressed, so a blob is authenticated by its hash alone and stays valid however
4//! far the pivot moves. Accounts sharing a code hash therefore need it downloaded and stored once.
5
6use crate::{SnapAttemptStore, SnapSyncError, SnapWrite};
7use alloy_primitives::{keccak256, Bytes, B256};
8use reth_db_api::{
9    tables,
10    transaction::{DbTx, DbTxMut},
11    RawKey, RawTable,
12};
13use reth_storage_api::{DBProvider, MetadataProvider, StateWriter};
14use revm::{bytecode::Bytecode, database::states::StateChangeset};
15
16/// Persistence for the code an attempt's accounts reference.
17///
18/// Blanket-implemented over the node's writers, so code joins the caller's transaction and commits
19/// with whatever else that transaction carries.
20pub trait SnapBytecodeStore {
21    /// Returns the first `limit` of `hashes` that are not stored yet, so the scan a request costs
22    /// is bounded by what it can ask for.
23    fn missing_code(
24        &self,
25        write: SnapWrite,
26        hashes: &[B256],
27        limit: usize,
28    ) -> Result<Vec<B256>, SnapSyncError>
29    where
30        Self: DBProvider;
31
32    /// Persists `codes`, each checked against the hash it was downloaded under, and returns how
33    /// many were written.
34    fn commit_bytecodes(
35        &self,
36        write: SnapWrite,
37        codes: Vec<(B256, Bytes)>,
38    ) -> Result<usize, SnapSyncError>
39    where
40        Self: StateWriter + DBProvider<Tx: DbTxMut>;
41}
42
43impl<T: MetadataProvider> SnapBytecodeStore for T {
44    fn missing_code(
45        &self,
46        write: SnapWrite,
47        hashes: &[B256],
48        limit: usize,
49    ) -> Result<Vec<B256>, SnapSyncError>
50    where
51        Self: DBProvider,
52    {
53        self.authorize_snap_write(write)?;
54        let mut missing = Vec::new();
55        for hash in hashes {
56            if missing.len() == limit {
57                break
58            }
59            // Only presence matters, so stored code is not decoded.
60            if self.tx_ref().get::<RawTable<tables::Bytecodes>>(RawKey::new(*hash))?.is_none() {
61                missing.push(*hash);
62            }
63        }
64        Ok(missing)
65    }
66
67    // Every check runs before the first write, so a refused response changes nothing.
68    fn commit_bytecodes(
69        &self,
70        write: SnapWrite,
71        codes: Vec<(B256, Bytes)>,
72    ) -> Result<usize, SnapSyncError>
73    where
74        Self: StateWriter + DBProvider<Tx: DbTxMut>,
75    {
76        self.authorize_snap_write(write)?;
77        let contracts = codes
78            .into_iter()
79            .map(|(hash, code)| {
80                let got = keccak256(&code);
81                if got != hash {
82                    return Err(SnapSyncError::CodeMismatch { expected: hash, got })
83                }
84                // Code deployed before EIP-3541 can carry the delegation prefix without being
85                // one, so authenticated bytes that do not parse as a delegation are legacy code.
86                let code = Bytecode::new_raw_checked(code.clone())
87                    .unwrap_or_else(|_| Bytecode::new_legacy(code));
88                Ok((hash, code))
89            })
90            .collect::<Result<Vec<_>, SnapSyncError>>()?;
91
92        let written = contracts.len();
93        self.write_state_changes(StateChangeset { contracts, ..Default::default() })?;
94        Ok(written)
95    }
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101    use crate::test_utils::{
102        account, generation, hashed_factory, insert_generation_headers, key, state_root,
103        verified_range,
104    };
105    use alloy_primitives::bytes;
106    use reth_provider::{
107        test_utils::MockNodeTypesWithDB, DatabaseProviderFactory, ProviderFactory,
108    };
109    use reth_trie_common::TrieAccount;
110
111    type Factory = ProviderFactory<MockNodeTypesWithDB>;
112
113    fn code(byte: u8) -> Bytes {
114        Bytes::from(vec![byte; 4])
115    }
116
117    // An account referencing `code`, distinguished by its nonce.
118    fn contract(nonce: u64, code: &Bytes) -> TrieAccount {
119        let mut contract = account(nonce);
120        contract.code_hash = keccak256(code);
121        contract
122    }
123
124    fn started(accounts: &[(B256, TrieAccount)]) -> (Factory, SnapWrite) {
125        let factory = hashed_factory();
126        insert_generation_headers(&factory);
127        let provider = factory.database_provider_rw().unwrap();
128        let write = provider.start_snap_attempt(generation(1, state_root(accounts))).unwrap();
129        provider.commit().unwrap();
130        (factory, write)
131    }
132
133    fn is_stored(provider: &impl DBProvider, code: &Bytes) -> bool {
134        provider.tx_ref().get::<tables::Bytecodes>(keccak256(code)).unwrap().is_some()
135    }
136
137    #[test]
138    fn one_blob_answers_every_account_sharing_its_hash() {
139        let shared = code(1);
140        let accounts = vec![
141            (key(1), contract(1, &shared)),
142            (key(2), account(2)),
143            (key(3), contract(3, &shared)),
144            (key(4), contract(4, &code(2))),
145        ];
146        let (factory, write) = started(&accounts);
147        let provider = factory.database_provider_rw().unwrap();
148        let referenced =
149            verified_range(&accounts, 0..accounts.len(), B256::ZERO, &[]).code_hashes();
150
151        let missing = provider.missing_code(write, &referenced, usize::MAX).unwrap();
152
153        assert_eq!(missing, [keccak256(&shared), keccak256(code(2))]);
154        assert_eq!(
155            provider.commit_bytecodes(write, vec![(missing[0], shared.clone())]).unwrap(),
156            1
157        );
158        // The shared blob answers both accounts, leaving only the other contract's code.
159        assert_eq!(
160            provider.missing_code(write, &referenced, usize::MAX).unwrap(),
161            [keccak256(code(2))]
162        );
163        assert!(is_stored(&provider, &shared));
164    }
165
166    #[test]
167    fn a_scan_stops_once_it_has_the_hashes_a_request_can_carry() {
168        let hashes: Vec<_> = (1..=4).map(|byte| keccak256(code(byte))).collect();
169        let (factory, write) = started(&[(key(1), contract(1, &code(1)))]);
170        let provider = factory.database_provider_ro().unwrap();
171
172        assert_eq!(provider.missing_code(write, &hashes, 2).unwrap(), hashes[..2]);
173    }
174
175    #[test]
176    fn code_already_stored_is_not_requested_again() {
177        let stored = code(1);
178        let accounts = vec![(key(1), contract(1, &stored))];
179        let (factory, write) = started(&accounts);
180        let provider = factory.database_provider_rw().unwrap();
181        provider.commit_bytecodes(write, vec![(keccak256(&stored), stored.clone())]).unwrap();
182        provider.commit().unwrap();
183
184        let provider = factory.database_provider_ro().unwrap();
185        assert!(provider
186            .missing_code(write, &[keccak256(&stored)], usize::MAX)
187            .unwrap()
188            .is_empty());
189        assert!(is_stored(&provider, &stored));
190    }
191
192    #[test]
193    fn code_that_does_not_hash_to_its_requested_hash_is_refused() {
194        let wanted = code(1);
195        let accounts = vec![(key(1), contract(1, &wanted))];
196        let (factory, write) = started(&accounts);
197        let provider = factory.database_provider_rw().unwrap();
198        let hash = keccak256(&wanted);
199
200        let refused = provider.commit_bytecodes(write, vec![(hash, code(2))]);
201
202        assert!(
203            matches!(refused, Err(SnapSyncError::CodeMismatch { expected, .. }) if expected == hash)
204        );
205        assert_eq!(provider.missing_code(write, &[hash], usize::MAX).unwrap(), [hash]);
206        assert!(!is_stored(&provider, &wanted));
207    }
208
209    // A delegation is exactly 23 bytes, but EIP-3541 only bars the prefix from London onwards, so
210    // an older account can reference shorter code that starts with it.
211    #[test]
212    fn delegation_shaped_code_that_predates_eip_3541_is_kept_as_legacy_code() {
213        let historical = bytes!("ef0100");
214        let accounts = vec![(key(1), contract(1, &historical))];
215        let (factory, write) = started(&accounts);
216        let provider = factory.database_provider_rw().unwrap();
217
218        let hash = keccak256(&historical);
219        assert_eq!(provider.commit_bytecodes(write, vec![(hash, historical.clone())]).unwrap(), 1);
220
221        let stored = provider.tx_ref().get::<tables::Bytecodes>(hash).unwrap().unwrap();
222        assert_eq!(stored.original_bytes(), historical);
223        assert!(provider.missing_code(write, &[hash], usize::MAX).unwrap().is_empty());
224    }
225
226    #[test]
227    fn code_is_refused_once_the_attempt_no_longer_owns_the_state() {
228        let wanted = code(1);
229        let accounts = vec![(key(1), contract(1, &wanted))];
230        let (factory, write) = started(&accounts);
231        let provider = factory.database_provider_rw().unwrap();
232        provider.advance_snap_pivot(write, generation(2, B256::repeat_byte(0xcc))).unwrap();
233
234        let refused = provider.commit_bytecodes(write, vec![(keccak256(&wanted), wanted.clone())]);
235
236        assert!(matches!(refused, Err(SnapSyncError::StaleWrite { .. })));
237        assert!(matches!(
238            provider.missing_code(write, &[keccak256(&wanted)], usize::MAX),
239            Err(SnapSyncError::StaleWrite { .. })
240        ));
241        assert!(!is_stored(&provider, &wanted));
242    }
243}