Skip to main content

reth_snap_sync/bytecode/
download.rs

1//! Downloads the code an account range's contracts reference, committing every verified response.
2
3use crate::{common::DownloadContext, SnapBytecodeStore, SnapSyncError, VerifiedRange};
4use reth_db_api::transaction::DbTxMut;
5use reth_downloaders::snap::{BytecodeDownloader, BytecodeOutcome};
6use reth_eth_wire_types::snap::GetByteCodesMessage;
7use reth_network_p2p::snap::client::SnapClient;
8use reth_network_peers::PeerId;
9use reth_storage_api::{
10    DBProvider, DatabaseProviderFactory, MetadataProvider, MetadataWriter, StateWriter,
11};
12use reth_tasks::Runtime;
13use std::fmt;
14
15/// Default number of code hashes asked for per request.
16pub const DEFAULT_CODE_HASHES: usize = 128;
17
18/// Downloads the code an account range still needs, one request at a time.
19///
20/// Code is content addressed, so a hash is requested only while no blob is stored for it, however
21/// many accounts reference it and whichever attempt fetched it.
22pub struct BytecodeDownload<C, F> {
23    context: DownloadContext<C, F>,
24    // Code hashes asked for per request.
25    max_hashes: usize,
26}
27
28impl<C, F> BytecodeDownload<C, F> {
29    /// Creates a download that requests whatever code the store does not already hold.
30    pub const fn new(client: C, factory: F, runtime: Runtime) -> Self {
31        Self {
32            context: DownloadContext::new(client, factory, runtime),
33            max_hashes: DEFAULT_CODE_HASHES,
34        }
35    }
36
37    /// Returns this download asking peers for at most `response_bytes` per response.
38    pub const fn with_response_bytes(mut self, response_bytes: u64) -> Self {
39        self.context.set_response_bytes(response_bytes);
40        self
41    }
42
43    /// Returns this download asking for at most `max_hashes` code hashes per request, at least one.
44    pub const fn with_max_hashes(mut self, max_hashes: usize) -> Self {
45        self.max_hashes = if max_hashes == 0 { 1 } else { max_hashes };
46        self
47    }
48}
49
50impl<C, F> BytecodeDownload<C, F>
51where
52    C: SnapClient + Clone + Unpin,
53    F: DatabaseProviderFactory + Clone + 'static,
54    F::Provider: MetadataProvider,
55    F::ProviderRW: MetadataProvider + MetadataWriter + StateWriter + DBProvider<Tx: DbTxMut>,
56{
57    /// Requests code `range` still needs and commits the verified response.
58    ///
59    /// [`BytecodeStep::Complete`] once every hash it references is stored, so the range can commit
60    /// without supplying code. Whatever a peer leaves out stays missing for the next call.
61    pub async fn next(&mut self, range: &VerifiedRange) -> Result<BytecodeStep, SnapSyncError> {
62        let write = range.write();
63        let referenced = range.range().code_hashes();
64        if referenced.is_empty() {
65            return Ok(BytecodeStep::Complete)
66        }
67        let limit = self.max_hashes;
68        let missing = self
69            .context
70            .read(move |provider| provider.missing_code(write, &referenced, limit))
71            .await?;
72        if missing.is_empty() {
73            return Ok(BytecodeStep::Complete)
74        }
75
76        let request = GetByteCodesMessage {
77            request_id: self.context.next_request_id(),
78            hashes: missing,
79            response_bytes: self.context.response_bytes(),
80        };
81        let downloader = BytecodeDownloader::new(
82            self.context.client().clone(),
83            request,
84            self.context.runtime().clone(),
85        )
86        .expect("missing code is never empty");
87        let codes = match downloader.await? {
88            BytecodeOutcome::Verified(verified) => verified.into_codes(),
89            BytecodeOutcome::Unavailable { peer_id } => {
90                return Ok(BytecodeStep::Unavailable { peer_id })
91            }
92        };
93
94        let persisted =
95            self.context.commit(move |provider| provider.commit_bytecodes(write, codes)).await?;
96        Ok(BytecodeStep::Committed { persisted })
97    }
98}
99
100impl<C, F> fmt::Debug for BytecodeDownload<C, F> {
101    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102        f.debug_struct("BytecodeDownload")
103            .field("context", &self.context)
104            .field("max_hashes", &self.max_hashes)
105            .finish()
106    }
107}
108
109/// What one request of a [`BytecodeDownload`] produced.
110#[derive(Debug)]
111pub enum BytecodeStep {
112    /// A response was committed, storing this many code blobs.
113    Committed {
114        /// Blobs the response supplied, at most the hashes it was asked for.
115        persisted: usize,
116    },
117    /// The peer held none of the requested code, so all of it stays missing.
118    Unavailable {
119        /// Peer that answered without the code, so the retry can go elsewhere.
120        peer_id: PeerId,
121    },
122    /// Every hash the range's accounts reference is stored.
123    Complete,
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129    use crate::{
130        test_utils::{
131            account, byte_codes, generation, hashed_factory_with_roots, key, state_root,
132            verified_range, verified_repair, ScriptedSnapClient,
133        },
134        SnapAccountStore, SnapAttemptStore,
135    };
136    use alloy_primitives::{keccak256, Bytes, B256};
137    use reth_db_api::{tables, transaction::DbTx};
138    use reth_eth_wire_types::snap::ByteCodesMessage;
139    use reth_network_p2p::{error::PeerRequestResult, snap::client::SnapResponse};
140    use reth_network_peers::WithPeerId;
141    use reth_provider::{test_utils::MockNodeTypesWithDB, ProviderFactory};
142    use reth_trie_common::TrieAccount;
143    use std::sync::Arc;
144
145    type Factory = ProviderFactory<MockNodeTypesWithDB>;
146    type Download = BytecodeDownload<Arc<ScriptedSnapClient>, Factory>;
147
148    fn code(byte: u8) -> Bytes {
149        Bytes::from(vec![byte; 4])
150    }
151
152    fn contract(nonce: u64, code: &Bytes) -> TrieAccount {
153        let mut contract = account(nonce);
154        contract.code_hash = keccak256(code);
155        contract
156    }
157
158    // Two contracts sharing one blob, a plain account, and a contract with its own blob.
159    fn accounts() -> Vec<(B256, TrieAccount)> {
160        vec![
161            (key(1), contract(1, &code(1))),
162            (key(2), account(2)),
163            (key(3), contract(3, &code(1))),
164            (key(4), contract(4, &code(2))),
165        ]
166    }
167
168    // An attempt that fetched all of `accounts` as one range, not yet committed.
169    fn started(accounts: &[(B256, TrieAccount)]) -> (Factory, VerifiedRange) {
170        let factory = hashed_factory_with_roots(&[state_root(accounts); 3]);
171        let provider = factory.database_provider_rw().unwrap();
172        let write = provider.start_snap_attempt(generation(1, state_root(accounts))).unwrap();
173        provider.start_account_coverage(write).unwrap();
174        provider.commit().unwrap();
175        let range = verified_range(accounts, 0..accounts.len(), B256::ZERO, &[]);
176        (factory, VerifiedRange::new(write, range))
177    }
178
179    fn download(
180        responses: impl IntoIterator<Item = PeerRequestResult<SnapResponse>>,
181        factory: Factory,
182    ) -> (Arc<ScriptedSnapClient>, Download) {
183        let client = Arc::new(ScriptedSnapClient::new(responses));
184        (Arc::clone(&client), BytecodeDownload::new(client, factory, Runtime::test()))
185    }
186
187    async fn committed(download: &mut Download, range: &VerifiedRange) -> usize {
188        match download.next(range).await.unwrap() {
189            BytecodeStep::Committed { persisted } => persisted,
190            step => panic!("expected a commit, got {step:?}"),
191        }
192    }
193
194    fn stored(factory: &Factory, code: &Bytes) -> bool {
195        let provider = factory.database_provider_ro().unwrap();
196        provider.tx_ref().get::<tables::Bytecodes>(keccak256(code)).unwrap().is_some()
197    }
198
199    #[tokio::test]
200    async fn shared_code_is_requested_once_and_persisted_for_every_account() {
201        let accounts = accounts();
202        let (factory, range) = started(&accounts);
203        let (client, mut download) =
204            download([byte_codes(1, &[code(1), code(2)])], factory.clone());
205
206        assert_eq!(committed(&mut download, &range).await, 2);
207
208        assert!(matches!(download.next(&range).await.unwrap(), BytecodeStep::Complete));
209        assert_eq!(*client.code_requests(), [vec![keccak256(code(1)), keccak256(code(2))]]);
210        assert!(stored(&factory, &code(1)) && stored(&factory, &code(2)));
211
212        // With its code stored, the range commits without supplying any.
213        let provider = factory.database_provider_rw().unwrap();
214        let coverage = provider
215            .commit_account_range(range.write(), range.range(), Default::default(), Vec::new())
216            .unwrap();
217        provider.commit().unwrap();
218        assert!(coverage.is_complete());
219    }
220
221    #[tokio::test]
222    async fn a_repair_requests_only_the_code_of_its_account() {
223        let accounts = accounts();
224        let (factory, range) = started(&accounts);
225        let write = range.write();
226        let (client, mut download) = download([byte_codes(1, &[code(2)])], factory);
227
228        // Key 2 holds no code, and the contract at key 3 only proves where its range ends.
229        let plain =
230            VerifiedRange::new(write, verified_repair(&accounts, 1..3, key(2), &[key(2), key(3)]));
231        assert!(matches!(download.next(&plain).await.unwrap(), BytecodeStep::Complete));
232        let contract =
233            VerifiedRange::new(write, verified_repair(&accounts, 3..4, key(4), &[key(4)]));
234        assert!(matches!(
235            download.next(&contract).await.unwrap(),
236            BytecodeStep::Committed { persisted: 1 }
237        ));
238
239        assert_eq!(*client.code_requests(), [vec![keccak256(code(2))]]);
240    }
241
242    #[tokio::test]
243    async fn code_a_peer_does_not_have_stays_missing() {
244        let accounts = accounts();
245        let (factory, range) = started(&accounts);
246        let peer = PeerId::random();
247        let empty = ByteCodesMessage { request_id: 1, codes: Vec::new() };
248        let responses = [
249            Ok(WithPeerId::new(peer, SnapResponse::ByteCodes(empty))),
250            // A peer that holds only the first blob answers with a subsequence.
251            byte_codes(2, &[code(1)]),
252            byte_codes(3, &[code(2)]),
253        ];
254        let (client, mut download) = download(responses, factory.clone());
255
256        let step = download.next(&range).await.unwrap();
257
258        assert!(matches!(step, BytecodeStep::Unavailable { peer_id } if peer_id == peer));
259        assert!(!stored(&factory, &code(1)));
260        assert_eq!(committed(&mut download, &range).await, 1);
261        // The unanswered hash is asked for again, the stored one is not.
262        assert_eq!(committed(&mut download, &range).await, 1);
263        assert!(matches!(download.next(&range).await.unwrap(), BytecodeStep::Complete));
264        assert_eq!(
265            *client.code_requests(),
266            [
267                vec![keccak256(code(1)), keccak256(code(2))],
268                vec![keccak256(code(1)), keccak256(code(2))],
269                vec![keccak256(code(2))],
270            ]
271        );
272    }
273
274    #[tokio::test]
275    async fn requests_ask_for_at_most_the_configured_hashes() {
276        let accounts = accounts();
277        let (factory, range) = started(&accounts);
278        let responses = [byte_codes(1, &[code(1)]), byte_codes(2, &[code(2)])];
279        let (client, download) = download(responses, factory);
280        let mut download = download.with_max_hashes(1);
281
282        committed(&mut download, &range).await;
283        committed(&mut download, &range).await;
284
285        assert!(matches!(download.next(&range).await.unwrap(), BytecodeStep::Complete));
286        assert_eq!(*client.code_requests(), [vec![keccak256(code(1))], vec![keccak256(code(2))]]);
287    }
288}