Skip to main content

reth_downloaders/snap/
mod.rs

1//! Downloads and verifies snap/2 ranges against
2//! [EIP-8189](https://eips.ethereum.org/EIPS/eip-8189) pivot state roots.
3//!
4//! Persistence and range selection are handled by the snap sync orchestrator.
5
6use alloy_primitives::{map::B256Set, B256, KECCAK256_EMPTY};
7use futures::Future;
8use reth_eth_wire_types::snap::{
9    AccountData, AccountRangeMessage, GetAccountRangeMessage, GetStorageRangesMessage,
10};
11use reth_network_p2p::{
12    error::RequestError,
13    snap::client::{SnapClient, SnapResponse},
14};
15use reth_network_peers::PeerId;
16use reth_tasks::Runtime;
17use reth_trie_common::{range_proof::verify_range_proof, TrieAccount, EMPTY_ROOT_HASH};
18use std::{
19    ops::Range,
20    pin::Pin,
21    task::{Context, Poll},
22};
23use tracing::debug;
24
25mod block_access_list;
26mod bytecode;
27mod request;
28mod storage;
29#[cfg(test)]
30mod test_utils;
31
32pub use block_access_list::*;
33pub use bytecode::*;
34use request::{SnapVerifier, VerifyingRequest};
35pub use storage::*;
36
37/// Downloads and verifies one account range against its requested state root.
38///
39/// Invalid responses penalize their peer and retry at high priority. Proof verification runs on
40/// the blocking pool.
41#[derive(Debug)]
42pub struct AccountRangeDownloader<C: SnapClient>(VerifyingRequest<C, GetAccountRangeMessage>);
43
44impl<C: SnapClient> AccountRangeDownloader<C> {
45    /// Creates a downloader using `runtime` for proof verification and submits the initial request.
46    /// Returns an error when the origin exceeds the limit.
47    pub fn new(
48        client: C,
49        request: GetAccountRangeMessage,
50        runtime: Runtime,
51    ) -> Result<Self, InvalidAccountRange> {
52        if request.starting_hash > request.limit_hash {
53            return Err(InvalidAccountRange {
54                origin: request.starting_hash,
55                limit: request.limit_hash,
56            })
57        }
58        let verifier = request.clone();
59        Ok(Self(VerifyingRequest::new(client, request, verifier, runtime)))
60    }
61}
62
63impl<C> Future for AccountRangeDownloader<C>
64where
65    C: SnapClient + Unpin,
66{
67    type Output = Result<AccountRangeOutcome, RequestError>;
68
69    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
70        self.get_mut().0.poll_verified(cx)
71    }
72}
73
74/// The result of an authenticated account-range request.
75#[derive(Clone, Debug, PartialEq, Eq)]
76pub enum AccountRangeOutcome {
77    /// The peer does not have the requested state root and was not penalized.
78    Unavailable {
79        /// The peer that answered.
80        peer_id: PeerId,
81    },
82    /// An account range authenticated against the requested state root.
83    Verified(VerifiedAccountRange),
84}
85
86/// A decoded account range authenticated against a state root.
87#[derive(Clone, Debug, PartialEq, Eq)]
88pub struct VerifiedAccountRange {
89    // Root the accounts were proven against. Private so a range cannot be relabelled with a root
90    // that did not authenticate it.
91    state_root: B256,
92    // Inclusive key the proof was verified from. Private for the same reason as the root.
93    origin: B256,
94    // Accounts as the response returned them, in the order every positional check assumes.
95    accounts: Vec<(B256, TrieAccount)>,
96    // Whether the requested interval may continue past this response.
97    has_more: bool,
98    // First key after the response, or none when the range ran out of trie.
99    next: Option<B256>,
100}
101
102impl VerifiedAccountRange {
103    /// State root the accounts were authenticated against.
104    pub const fn state_root(&self) -> B256 {
105        self.state_root
106    }
107
108    /// Key the range was requested from, proven to have nothing before the first account.
109    pub const fn origin(&self) -> B256 {
110        self.origin
111    }
112
113    /// Accounts in strictly increasing hashed-key order.
114    pub fn accounts(&self) -> &[(B256, TrieAccount)] {
115        &self.accounts
116    }
117
118    /// Whether another request may be needed to complete the interval.
119    ///
120    /// Conservative: can be `true` for an interval that is already complete.
121    pub const fn has_more(&self) -> bool {
122        self.has_more
123    }
124
125    /// Authenticated lower bound for the first key after the response, or `None` when the range
126    /// exhausted the trie.
127    pub const fn next(&self) -> Option<B256> {
128        self.next
129    }
130
131    /// Borrows the accounts together with the root that authenticated them.
132    pub fn batch(&self) -> VerifiedAccountBatch<'_> {
133        VerifiedAccountBatch {
134            state_root: self.state_root,
135            accounts: self.accounts.iter().map(|(hash, account)| (*hash, account)).collect(),
136        }
137    }
138
139    /// Code hashes the accounts reference, once each and in the order they appear.
140    ///
141    /// Accounts sharing code therefore yield one hash, so it is downloaded and stored once.
142    pub fn code_hashes(&self) -> Vec<B256> {
143        let mut seen = B256Set::default();
144        self.accounts
145            .iter()
146            .map(|(_, account)| account.code_hash)
147            .filter(|hash| *hash != KECCAK256_EMPTY && seen.insert(*hash))
148            .collect()
149    }
150
151    /// Borrows only the accounts that have storage, together with the root that authenticated
152    /// them.
153    ///
154    /// Accounts without storage are omitted because snap storage responses do not preserve an
155    /// outer-list position for them. Empty when no account in the range has storage.
156    pub fn storage_batch(&self) -> VerifiedAccountBatch<'_> {
157        VerifiedAccountBatch {
158            state_root: self.state_root,
159            accounts: self
160                .accounts
161                .iter()
162                .filter(|(_, account)| account.storage_root != EMPTY_ROOT_HASH)
163                .map(|(hash, account)| (*hash, account))
164                .collect(),
165        }
166    }
167}
168
169/// Accounts and the state root they were authenticated against.
170///
171/// Only obtainable from [`VerifiedAccountRange`], so requests built from it can always be checked
172/// against the root the accounts came from.
173#[derive(Clone, Debug, PartialEq, Eq)]
174pub struct VerifiedAccountBatch<'a> {
175    // Root and accounts travel together, so neither can be swapped for another generation's.
176    state_root: B256,
177    // Accounts the root authenticated, in response order.
178    accounts: Vec<(B256, &'a TrieAccount)>,
179}
180
181impl<'a> VerifiedAccountBatch<'a> {
182    /// State root the accounts were authenticated against.
183    pub const fn state_root(&self) -> B256 {
184        self.state_root
185    }
186
187    /// Accounts in the order the range returned them.
188    pub fn accounts(&self) -> &[(B256, &'a TrieAccount)] {
189        &self.accounts
190    }
191
192    /// Borrows a positional subrange of the batch, so storage can be requested in bounded chunks
193    /// without losing the root that authenticated the accounts.
194    ///
195    /// `None` when the range falls outside the batch.
196    pub fn range(&self, range: Range<usize>) -> Option<Self> {
197        self.accounts
198            .get(range)
199            .map(|accounts| Self { state_root: self.state_root, accounts: accounts.to_vec() })
200    }
201
202    // Confirms the batch is the one `request` was built from, so every returned range is checked
203    // against the root that authenticated its account.
204    pub(super) fn verify_batch(
205        &self,
206        request: &GetStorageRangesMessage,
207    ) -> Result<(), InvalidStorageRangeRequest> {
208        if request.root_hash != self.state_root {
209            return Err(InvalidStorageRangeRequest::StateRootMismatch {
210                requested: request.root_hash,
211                authenticated: self.state_root,
212            })
213        }
214        if request.account_hashes.len() != self.accounts.len() {
215            return Err(InvalidStorageRangeRequest::AccountCount {
216                requested: request.account_hashes.len(),
217                supplied: self.accounts.len(),
218            })
219        }
220        for (index, (requested, (supplied, _))) in
221            request.account_hashes.iter().zip(&self.accounts).enumerate()
222        {
223            if requested != supplied {
224                return Err(InvalidStorageRangeRequest::AccountMismatch {
225                    index,
226                    requested: *requested,
227                    supplied: *supplied,
228                })
229            }
230        }
231        Ok(())
232    }
233
234    // The accounts from `from` onwards under the same state root, or none if the batch is shorter.
235    pub(super) fn slice(mut self, from: usize) -> Option<Self> {
236        (from <= self.accounts.len()).then(|| {
237            self.accounts.drain(..from);
238            self
239        })
240    }
241}
242
243/// An account-range request whose origin exceeds its limit.
244#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
245#[error("account range origin {origin} exceeds limit {limit}")]
246pub struct InvalidAccountRange {
247    /// Inclusive origin the range was requested from.
248    pub origin: B256,
249    /// Inclusive limit the range was requested to.
250    pub limit: B256,
251}
252
253// The request itself carries everything needed to authenticate its response.
254impl SnapVerifier for GetAccountRangeMessage {
255    type Request = Self;
256    type Output = AccountRangeOutcome;
257
258    fn verify(self, peer_id: PeerId, response: SnapResponse) -> Result<Self::Output, RequestError> {
259        let SnapResponse::AccountRange(response) = response else {
260            debug!(target: "downloaders::snap", "Expected account range response");
261            return Err(RequestError::BadResponse)
262        };
263        if response.request_id != self.request_id {
264            debug!(
265                target: "downloaders::snap",
266                expected = self.request_id,
267                got = response.request_id,
268                "Account range response id mismatch"
269            );
270            return Err(RequestError::BadResponse)
271        }
272        if response.accounts.is_empty() && response.proof.is_empty() {
273            return if self.root_hash == EMPTY_ROOT_HASH {
274                Ok(AccountRangeOutcome::Verified(VerifiedAccountRange {
275                    state_root: self.root_hash,
276                    origin: self.starting_hash,
277                    accounts: Vec::new(),
278                    has_more: false,
279                    next: None,
280                }))
281            } else {
282                Ok(AccountRangeOutcome::Unavailable { peer_id })
283            }
284        }
285
286        verify_account_range(&self, response).map(AccountRangeOutcome::Verified)
287    }
288}
289
290// Authenticates the full response before trimming its optional boundary account.
291fn verify_account_range(
292    request: &GetAccountRangeMessage,
293    response: AccountRangeMessage,
294) -> Result<VerifiedAccountRange, RequestError> {
295    // Allow only the single out-of-range account needed as a boundary witness.
296    if response.accounts.iter().filter(|data| data.hash > request.limit_hash).nth(1).is_some() {
297        debug!(target: "downloaders::snap", "Account range runs past the requested limit");
298        return Err(RequestError::BadResponse)
299    }
300
301    let mut accounts =
302        response.accounts.into_iter().map(AccountData::into_trie_entry).collect::<Vec<_>>();
303    let next = verify_proof(request, &accounts, &response.proof)?;
304
305    // Authenticate the boundary account before removing it from the requested range. Once
306    // removed, it is the first key after the response, so resuming from it cannot skip it.
307    let kept = accounts.partition_point(|(hash, _)| *hash <= request.limit_hash);
308    let next = accounts.get(kept).map(|(hash, _)| *hash).or(next);
309    accounts.truncate(kept);
310    let has_more = next.is_some_and(|next| next <= request.limit_hash);
311
312    Ok(VerifiedAccountRange {
313        state_root: request.root_hash,
314        origin: request.starting_hash,
315        accounts,
316        has_more,
317        next,
318    })
319}
320
321// Re-encodes decoded accounts so the proof authenticates their canonical trie values.
322fn verify_proof(
323    request: &GetAccountRangeMessage,
324    accounts: &[(B256, TrieAccount)],
325    proof: &[alloy_primitives::Bytes],
326) -> Result<Option<B256>, RequestError> {
327    let leaves = accounts.iter().map(|(hash, account)| (*hash, alloy_rlp::encode(account)));
328    verify_range_proof(request.root_hash, request.starting_hash, request.limit_hash, leaves, proof)
329        .map_err(|error| {
330            debug!(target: "downloaders::snap", %error, "Invalid account range proof");
331            RequestError::BadResponse
332        })
333}
334
335#[cfg(test)]
336#[allow(clippy::clone_on_copy)]
337mod tests {
338    use super::{request::MAX_RETRIES, test_utils::TestSnapClient, *};
339    use alloy_primitives::{Bytes, U256};
340    use reth_eth_wire_types::snap::ByteCodesMessage;
341    use reth_network_p2p::{error::PeerRequestResult, priority::Priority};
342    use reth_network_peers::WithPeerId;
343    use reth_trie_common::{proof::ProofRetainer, root::state_root, HashBuilder, Nibbles};
344    use std::sync::Arc;
345
346    const MAX_HASH: B256 = B256::new([0xff; B256::len_bytes()]);
347
348    fn key(value: u64) -> B256 {
349        B256::left_padding_from(&value.to_be_bytes())
350    }
351
352    fn account(nonce: u64) -> TrieAccount {
353        TrieAccount { nonce, balance: U256::from(1), ..Default::default() }
354    }
355
356    fn root(accounts: &[(B256, TrieAccount)]) -> B256 {
357        state_root(accounts.to_vec())
358    }
359
360    fn root_and_proof(accounts: &[(B256, TrieAccount)], targets: &[B256]) -> (B256, Vec<Bytes>) {
361        let targets = targets.iter().copied().map(Nibbles::unpack).collect();
362        let mut builder = HashBuilder::default().with_proof_retainer(ProofRetainer::new(targets));
363        for (key, account) in accounts {
364            builder.add_leaf(Nibbles::unpack(*key), &alloy_rlp::encode(account));
365        }
366        let root = builder.root();
367        let proof = builder
368            .take_proof_nodes()
369            .into_nodes_sorted()
370            .into_iter()
371            .map(|(_, node)| node)
372            .collect();
373        (root, proof)
374    }
375
376    fn request(root_hash: B256) -> GetAccountRangeMessage {
377        GetAccountRangeMessage {
378            request_id: 1,
379            root_hash,
380            starting_hash: B256::ZERO,
381            limit_hash: MAX_HASH,
382            response_bytes: 512 * 1024,
383        }
384    }
385
386    fn response(peer: PeerId, message: AccountRangeMessage) -> PeerRequestResult<SnapResponse> {
387        Ok(WithPeerId::new(peer, SnapResponse::AccountRange(message)))
388    }
389
390    fn downloader(
391        client: Arc<TestSnapClient>,
392        request: GetAccountRangeMessage,
393    ) -> Result<AccountRangeDownloader<Arc<TestSnapClient>>, InvalidAccountRange> {
394        AccountRangeDownloader::new(client, request, Runtime::test())
395    }
396
397    #[test]
398    fn verifies_and_decodes_without_an_ambient_runtime() {
399        let accounts = vec![(key(1), account(7)), (key(2), account(8))];
400        let root_hash = root(&accounts);
401        let peer = PeerId::random();
402        let message = AccountRangeMessage {
403            request_id: 1,
404            accounts: accounts
405                .iter()
406                .map(|(key, account)| AccountData::from_trie_account(*key, account))
407                .collect(),
408            proof: Vec::new(),
409        };
410        let client = Arc::new(TestSnapClient::new([response(peer, message)]));
411
412        let downloader = downloader(Arc::clone(&client), request(root_hash)).unwrap();
413        let outcome = futures::executor::block_on(downloader).unwrap();
414
415        assert_eq!(
416            outcome,
417            AccountRangeOutcome::Verified(VerifiedAccountRange {
418                state_root: root_hash,
419                origin: B256::ZERO,
420                accounts,
421                has_more: false,
422                next: None,
423            })
424        );
425        assert!(client.reported().is_empty());
426        assert_eq!(*client.priorities(), [Priority::Normal]);
427    }
428
429    #[tokio::test]
430    async fn invalid_peer_is_reported_and_request_is_retried_at_high_priority() {
431        let accounts = vec![(key(1), account(7))];
432        let root_hash = root(&accounts);
433        let bad_peer = PeerId::random();
434        let good_peer = PeerId::random();
435        let bad = Ok(WithPeerId::new(
436            bad_peer,
437            SnapResponse::ByteCodes(ByteCodesMessage { request_id: 1, codes: Vec::new() }),
438        ));
439        let good = response(
440            good_peer,
441            AccountRangeMessage {
442                request_id: 1,
443                accounts: vec![AccountData::from_trie_account(accounts[0].0, &accounts[0].1)],
444                proof: Vec::new(),
445            },
446        );
447        let client = Arc::new(TestSnapClient::new([bad, good]));
448
449        let outcome = downloader(Arc::clone(&client), request(root_hash)).unwrap().await.unwrap();
450
451        assert!(matches!(outcome, AccountRangeOutcome::Verified(_)));
452        assert_eq!(*client.reported(), [bad_peer]);
453        assert_eq!(*client.priorities(), [Priority::Normal, Priority::High]);
454    }
455
456    #[tokio::test]
457    async fn unavailable_state_is_not_a_bad_peer_response() {
458        let peer = PeerId::random();
459        let message =
460            AccountRangeMessage { request_id: 1, accounts: Vec::new(), proof: Vec::new() };
461        let client = Arc::new(TestSnapClient::new([response(peer, message)]));
462
463        let outcome = downloader(Arc::clone(&client), request(B256::repeat_byte(0x11)))
464            .unwrap()
465            .await
466            .unwrap();
467
468        assert_eq!(outcome, AccountRangeOutcome::Unavailable { peer_id: peer });
469        assert!(client.reported().is_empty());
470    }
471
472    #[test]
473    fn a_subrange_narrows_the_accounts_and_keeps_their_root() {
474        let accounts = vec![(key(1), account(7)), (key(2), account(8)), (key(3), account(9))];
475        let root_hash = root(&accounts);
476        let range = VerifiedAccountRange {
477            state_root: root_hash,
478            origin: B256::ZERO,
479            accounts: accounts.clone(),
480            has_more: false,
481            next: None,
482        };
483
484        let batch = range.batch();
485        let chunk = batch.range(1..3).expect("chunk is inside the batch");
486        let expected =
487            accounts[1..3].iter().map(|(hash, account)| (*hash, account)).collect::<Vec<_>>();
488        assert_eq!(chunk.accounts(), expected);
489        assert_eq!(chunk.state_root(), root_hash);
490
491        assert_eq!(batch.range(2..4), None);
492    }
493
494    #[test]
495    fn code_hashes_are_listed_once_in_the_order_the_accounts_reference_them() {
496        let shared = B256::repeat_byte(0x11);
497        let mut first = account(1);
498        first.code_hash = shared;
499        let mut third = account(3);
500        third.code_hash = shared;
501        let mut fourth = account(4);
502        fourth.code_hash = B256::repeat_byte(0x44);
503        let accounts =
504            vec![(key(1), first), (key(2), account(2)), (key(3), third), (key(4), fourth)];
505        let range = VerifiedAccountRange {
506            state_root: root(&accounts),
507            origin: B256::ZERO,
508            accounts,
509            has_more: false,
510            next: None,
511        };
512
513        assert_eq!(range.code_hashes(), vec![shared, B256::repeat_byte(0x44)]);
514    }
515
516    #[test]
517    fn storage_batch_omits_interleaved_accounts_without_storage() {
518        let mut first = account(1);
519        first.storage_root = B256::repeat_byte(0x11);
520        let empty = account(2);
521        let mut third = account(3);
522        third.storage_root = B256::repeat_byte(0x33);
523        let accounts = vec![(key(1), first), (key(2), empty), (key(3), third)];
524        let root_hash = root(&accounts);
525        let range = VerifiedAccountRange {
526            state_root: root_hash,
527            origin: B256::ZERO,
528            accounts,
529            has_more: false,
530            next: None,
531        };
532
533        let batch = range.storage_batch();
534
535        assert_eq!(batch.state_root(), root_hash);
536        assert_eq!(
537            batch.accounts().iter().map(|(hash, _)| *hash).collect::<Vec<_>>(),
538            vec![key(1), key(3)]
539        );
540
541        let chunk = batch.range(1..2).expect("chunk is inside the batch");
542        assert_eq!(
543            chunk.accounts().iter().map(|(hash, _)| *hash).collect::<Vec<_>>(),
544            vec![key(3)]
545        );
546        assert_eq!(chunk.state_root(), root_hash);
547    }
548
549    #[test]
550    fn invalid_request_range_is_rejected_before_submission() {
551        let client = Arc::new(TestSnapClient::new(std::iter::empty()));
552        let mut request = request(B256::repeat_byte(0x11));
553        request.starting_hash = key(2);
554        request.limit_hash = key(1);
555
556        assert!(matches!(
557            downloader(Arc::clone(&client), request),
558            Err(InvalidAccountRange { .. })
559        ));
560        assert!(client.priorities().is_empty());
561    }
562
563    #[tokio::test]
564    async fn authenticates_then_trims_an_account_past_the_limit() {
565        let accounts = vec![(key(1), account(7)), (key(3), account(8)), (key(4), account(9))];
566        let (root_hash, proof) = root_and_proof(&accounts, &[key(1), key(3)]);
567        let peer = PeerId::random();
568        let message = AccountRangeMessage {
569            request_id: 1,
570            accounts: accounts[..2]
571                .iter()
572                .map(|(key, account)| AccountData::from_trie_account(*key, account))
573                .collect(),
574            proof,
575        };
576        let client = Arc::new(TestSnapClient::new([response(peer, message)]));
577        let mut request = request(root_hash);
578        request.limit_hash = key(2);
579
580        let outcome = downloader(Arc::clone(&client), request).unwrap().await.unwrap();
581
582        assert_eq!(
583            outcome,
584            AccountRangeOutcome::Verified(VerifiedAccountRange {
585                state_root: root_hash,
586                origin: B256::ZERO,
587                accounts: vec![accounts[0].clone()],
588                has_more: false,
589                next: Some(key(3)),
590            })
591        );
592        assert!(client.reported().is_empty());
593    }
594
595    #[tokio::test]
596    async fn range_running_past_the_limit_is_rejected() {
597        let accounts = vec![(key(1), account(7)), (key(3), account(8)), (key(4), account(9))];
598        let (root_hash, proof) = root_and_proof(&accounts, &[key(1), key(4)]);
599        let peer = PeerId::random();
600        let message = AccountRangeMessage {
601            request_id: 1,
602            accounts: accounts
603                .iter()
604                .map(|(key, account)| AccountData::from_trie_account(*key, account))
605                .collect(),
606            proof,
607        };
608        let attempts = usize::from(MAX_RETRIES) + 1;
609        let client = Arc::new(TestSnapClient::new(
610            std::iter::repeat_with(|| response(peer, message.clone())).take(attempts),
611        ));
612        let mut request = request(root_hash);
613        request.limit_hash = key(2);
614
615        let error = downloader(Arc::clone(&client), request).unwrap().await.unwrap_err();
616
617        assert_eq!(error, RequestError::BadResponse);
618        assert_eq!(client.reported().len(), attempts);
619    }
620
621    #[tokio::test]
622    async fn account_at_the_limit_completes_the_requested_interval() {
623        let accounts = vec![(key(1), account(7)), (key(2), account(8)), (key(3), account(9))];
624        let (root_hash, proof) = root_and_proof(&accounts, &[key(1), key(2)]);
625        let peer = PeerId::random();
626        let message = AccountRangeMessage {
627            request_id: 1,
628            accounts: accounts[..2]
629                .iter()
630                .map(|(key, account)| AccountData::from_trie_account(*key, account))
631                .collect(),
632            proof,
633        };
634        let client = Arc::new(TestSnapClient::new([response(peer, message)]));
635        let mut request = request(root_hash);
636        request.limit_hash = key(2);
637
638        let outcome = downloader(Arc::clone(&client), request).unwrap().await.unwrap();
639
640        assert_eq!(
641            outcome,
642            AccountRangeOutcome::Verified(VerifiedAccountRange {
643                state_root: root_hash,
644                origin: B256::ZERO,
645                accounts: accounts[..2].to_vec(),
646                has_more: false,
647                next: Some(key(3)),
648            })
649        );
650        assert!(client.reported().is_empty());
651    }
652
653    // The first account after the limit proves an empty interval.
654    #[tokio::test]
655    async fn empty_interval_is_proven_by_the_first_account_after_the_limit() {
656        let accounts = vec![(key(1), account(7)), (key(9), account(8))];
657        let (root_hash, proof) = root_and_proof(&accounts, &[key(3), key(9)]);
658        let peer = PeerId::random();
659        let message = AccountRangeMessage {
660            request_id: 1,
661            accounts: vec![AccountData::from_trie_account(accounts[1].0, &accounts[1].1)],
662            proof,
663        };
664        let client = Arc::new(TestSnapClient::new([response(peer, message)]));
665        let mut request = request(root_hash);
666        request.starting_hash = key(3);
667        request.limit_hash = key(5);
668
669        let outcome = downloader(Arc::clone(&client), request).unwrap().await.unwrap();
670
671        assert_eq!(
672            outcome,
673            AccountRangeOutcome::Verified(VerifiedAccountRange {
674                state_root: root_hash,
675                origin: key(3),
676                accounts: Vec::new(),
677                has_more: false,
678                next: Some(key(9)),
679            })
680        );
681        assert!(client.reported().is_empty());
682    }
683
684    #[tokio::test]
685    async fn empty_interval_is_proven_without_a_boundary_account() {
686        let accounts = vec![(key(1), account(7)), (key(9), account(8))];
687        let (root_hash, proof) = root_and_proof(&accounts, &[key(3), key(5)]);
688        let peer = PeerId::random();
689        let message = AccountRangeMessage { request_id: 1, accounts: Vec::new(), proof };
690        let client = Arc::new(TestSnapClient::new([response(peer, message)]));
691        let mut request = request(root_hash);
692        request.starting_hash = key(3);
693        request.limit_hash = key(5);
694
695        let outcome = downloader(Arc::clone(&client), request).unwrap().await.unwrap();
696
697        assert_eq!(
698            outcome,
699            AccountRangeOutcome::Verified(VerifiedAccountRange {
700                state_root: root_hash,
701                origin: key(3),
702                accounts: Vec::new(),
703                has_more: false,
704                next: Some(key(9)),
705            })
706        );
707        assert!(client.reported().is_empty());
708    }
709
710    // A proof that continues past the limit completes the requested interval.
711    #[tokio::test]
712    async fn range_ending_before_the_limit_needs_no_further_request() {
713        let accounts = vec![(key(1), account(7)), (key(9), account(8))];
714        let (root_hash, proof) = root_and_proof(&accounts, &[key(1)]);
715        let peer = PeerId::random();
716        let message = AccountRangeMessage {
717            request_id: 1,
718            accounts: vec![AccountData::from_trie_account(accounts[0].0, &accounts[0].1)],
719            proof,
720        };
721        let client = Arc::new(TestSnapClient::new([response(peer, message)]));
722        let mut request = request(root_hash);
723        request.limit_hash = key(5);
724
725        let outcome = downloader(Arc::clone(&client), request).unwrap().await.unwrap();
726
727        assert_eq!(
728            outcome,
729            AccountRangeOutcome::Verified(VerifiedAccountRange {
730                state_root: root_hash,
731                origin: B256::ZERO,
732                accounts: vec![accounts[0].clone()],
733                has_more: false,
734                next: Some(key(9)),
735            })
736        );
737    }
738
739    #[tokio::test]
740    async fn range_ending_before_a_covered_key_reports_more() {
741        let accounts = vec![(key(1), account(7)), (key(3), account(8))];
742        let (root_hash, proof) = root_and_proof(&accounts, &[key(1)]);
743        let peer = PeerId::random();
744        let message = AccountRangeMessage {
745            request_id: 1,
746            accounts: vec![AccountData::from_trie_account(accounts[0].0, &accounts[0].1)],
747            proof,
748        };
749        let client = Arc::new(TestSnapClient::new([response(peer, message)]));
750        let mut request = request(root_hash);
751        request.limit_hash = key(5);
752
753        let outcome = downloader(Arc::clone(&client), request).unwrap().await.unwrap();
754
755        assert_eq!(
756            outcome,
757            AccountRangeOutcome::Verified(VerifiedAccountRange {
758                state_root: root_hash,
759                origin: B256::ZERO,
760                accounts: vec![accounts[0].clone()],
761                has_more: true,
762                next: Some(key(3)),
763            })
764        );
765    }
766
767    #[tokio::test]
768    async fn request_errors_retry_without_duplicate_peer_penalties() {
769        let accounts = vec![(key(1), account(7))];
770        let root_hash = root(&accounts);
771        let peer = PeerId::random();
772        let good = response(
773            peer,
774            AccountRangeMessage {
775                request_id: 1,
776                accounts: vec![AccountData::from_trie_account(accounts[0].0, &accounts[0].1)],
777                proof: Vec::new(),
778            },
779        );
780        let client = Arc::new(TestSnapClient::new([
781            Err(RequestError::Timeout),
782            Err(RequestError::BadResponse),
783            good,
784        ]));
785
786        downloader(Arc::clone(&client), request(root_hash)).unwrap().await.unwrap();
787
788        assert!(client.reported().is_empty());
789        assert_eq!(*client.priorities(), [Priority::Normal, Priority::High, Priority::High]);
790    }
791
792    #[tokio::test]
793    async fn stops_after_the_retry_budget_is_exhausted() {
794        let peers = [PeerId::random(), PeerId::random(), PeerId::random()];
795        let responses = peers.map(|peer| {
796            Ok(WithPeerId::new(
797                peer,
798                SnapResponse::ByteCodes(ByteCodesMessage { request_id: 1, codes: Vec::new() }),
799            ))
800        });
801        let client = Arc::new(TestSnapClient::new(responses));
802
803        let error = downloader(Arc::clone(&client), request(B256::repeat_byte(0x11)))
804            .unwrap()
805            .await
806            .unwrap_err();
807
808        assert_eq!(error, RequestError::BadResponse);
809        assert_eq!(*client.reported(), peers);
810        assert_eq!(*client.priorities(), [Priority::Normal, Priority::High, Priority::High]);
811    }
812}