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