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