Skip to main content

reth_downloaders/snap/
storage.rs

1//! Downloads storage ranges and authenticates them against account storage roots.
2//!
3//! Responses remain positional, and only the final partial range may use the shared boundary
4//! proof defined by the Snap protocol.
5
6use super::{
7    request::{SnapVerifier, VerifyingRequest},
8    VerifiedAccountBatch,
9};
10use alloy_primitives::{B256, U256};
11use futures::Future;
12use reth_eth_wire_types::snap::{
13    GetStorageRangesMessage, RangeBound, StorageData, StorageRangesMessage,
14};
15use reth_network_p2p::{
16    error::RequestError,
17    snap::client::{SnapClient, SnapResponse},
18};
19use reth_network_peers::PeerId;
20use reth_tasks::Runtime;
21use reth_trie_common::{range_proof::verify_range_proof, EMPTY_ROOT_HASH};
22use std::{
23    pin::Pin,
24    task::{Context, Poll},
25};
26use tracing::debug;
27
28// Keeps storage requests inclusive through the full trie keyspace.
29const MAX_HASH: B256 = B256::new([0xff; B256::len_bytes()]);
30
31/// Downloads storage ranges authenticated by a verified account range.
32#[derive(Debug)]
33pub struct StorageRangeDownloader<C: SnapClient>(VerifyingRequest<C, StorageRangeVerifier>);
34
35impl<C: SnapClient> StorageRangeDownloader<C> {
36    /// Validates the authenticated batch against the request before submitting it.
37    ///
38    /// `batch` must hold the accounts in `request.account_hashes` order, under the same state
39    /// root; their storage roots authenticate the returned ranges. Pairing a batch with a request
40    /// for another root would penalize a peer that answered honestly.
41    ///
42    /// Accounts without storage must be omitted, since snap responses do not preserve an
43    /// outer-list position for them. [`super::VerifiedAccountRange::storage_batch`] selects the
44    /// accounts to request, and [`VerifiedAccountBatch::range`] splits them into bounded chunks.
45    pub fn new(
46        client: C,
47        request: GetStorageRangesMessage,
48        batch: &VerifiedAccountBatch<'_>,
49        runtime: Runtime,
50    ) -> Result<Self, InvalidStorageRangeRequest> {
51        let origin = request.starting_hash.unwrap_or(B256::ZERO);
52        let limit = request.limit_hash.unwrap_or(MAX_HASH);
53        if origin > limit {
54            return Err(InvalidStorageRangeRequest::ReversedBounds { origin, limit })
55        }
56        if request.account_hashes.is_empty() {
57            return Err(InvalidStorageRangeRequest::NoAccounts)
58        }
59        // Servers disagree on whether a finite limit applies only to the first account or every
60        // account, so the final proof cannot be assigned reliably for this request shape.
61        if request.account_hashes.len() > 1 && origin == B256::ZERO && limit != MAX_HASH {
62            return Err(InvalidStorageRangeRequest::LimitedMultipleAccounts {
63                accounts: request.account_hashes.len(),
64            })
65        }
66        batch.verify_batch(&request)?;
67        let mut storage_roots = Vec::with_capacity(batch.accounts().len());
68        for (index, (account_hash, account)) in batch.accounts().iter().enumerate() {
69            if account.storage_root == EMPTY_ROOT_HASH {
70                return Err(InvalidStorageRangeRequest::EmptyStorageRoot {
71                    index,
72                    account_hash: *account_hash,
73                })
74            }
75            storage_roots.push(account.storage_root);
76        }
77
78        let verifier = StorageRangeVerifier { request: request.clone(), storage_roots };
79        Ok(Self(VerifyingRequest::new(client, request, verifier, runtime)))
80    }
81}
82
83impl<C> Future for StorageRangeDownloader<C>
84where
85    C: SnapClient + Unpin,
86{
87    type Output = Result<StorageRangeOutcome, RequestError>;
88
89    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
90        self.get_mut().0.poll_verified(cx)
91    }
92}
93
94/// Result of an authenticated storage-ranges request.
95#[derive(Clone, Debug, PartialEq, Eq)]
96pub enum StorageRangeOutcome {
97    /// The responder lacks the requested state or account.
98    Unavailable {
99        /// Peer that returned the empty response.
100        peer_id: PeerId,
101    },
102    /// Ranges authenticated against their account storage roots.
103    Verified(VerifiedStorageRanges),
104}
105
106/// Positional storage ranges authenticated against their accounts.
107///
108/// Carries the request it answers so [`Self::follow_up`] can resume it.
109#[derive(Clone, Debug, PartialEq, Eq)]
110pub struct VerifiedStorageRanges {
111    // Slots per account. Private so they stay consistent with the position to resume at.
112    ranges: Vec<VerifiedStorageRange>,
113    // Where a follow-up resumes, or none when every requested account is complete.
114    continuation: Option<StorageRangeContinuation>,
115    // The request these ranges answer. Retained so a follow-up cannot be built against another.
116    request: Box<GetStorageRangesMessage>,
117}
118
119impl VerifiedStorageRanges {
120    /// Consumes the result and returns the ranges it authenticated.
121    pub fn into_ranges(self) -> Vec<VerifiedStorageRange> {
122        self.ranges
123    }
124
125    /// Resumes this response, narrowing `batch` to the accounts the new request covers.
126    ///
127    /// `Ok(None)` once every requested account is complete, and an error when `batch` is not the
128    /// one the answered request was built from. Each resumption asks for strictly less than the
129    /// last, and only its first account stays bounded.
130    pub fn follow_up<'a>(
131        &self,
132        request_id: u64,
133        batch: VerifiedAccountBatch<'a>,
134    ) -> Result<
135        Option<(GetStorageRangesMessage, VerifiedAccountBatch<'a>)>,
136        InvalidStorageRangeRequest,
137    > {
138        batch.verify_batch(&self.request)?;
139        let Some(continuation) = self.continuation else { return Ok(None) };
140        let (index, starting_hash, limit_hash) = match continuation {
141            StorageRangeContinuation::Partial { account_index: 0, starting_hash, .. } => {
142                (0, starting_hash.into(), self.request.limit_hash)
143            }
144            StorageRangeContinuation::Partial { account_index, starting_hash, .. } => {
145                (account_index, starting_hash.into(), RangeBound::default())
146            }
147            StorageRangeContinuation::NextAccount { account_index, .. } => {
148                (account_index, RangeBound::default(), RangeBound::default())
149            }
150        };
151
152        let request = GetStorageRangesMessage {
153            request_id,
154            account_hashes: self.request.account_hashes[index..].to_vec(),
155            starting_hash,
156            limit_hash,
157            ..(*self.request).clone()
158        };
159        let narrowed = batch.slice(index).expect("checked batch covers the continuation");
160        Ok(Some((request, narrowed)))
161    }
162}
163
164/// Decoded storage slots authenticated against one account.
165#[derive(Clone, Debug, PartialEq, Eq)]
166pub struct VerifiedStorageRange {
167    /// Hashed address of the owning account.
168    pub account_hash: B256,
169    /// Non-zero slots in increasing hashed-key order.
170    pub slots: Vec<(B256, U256)>,
171}
172
173// Resume position for an incomplete storage-ranges response.
174#[derive(Clone, Copy, Debug, PartialEq, Eq)]
175enum StorageRangeContinuation {
176    /// The final returned account remains incomplete.
177    Partial {
178        /// Position in the original account list.
179        account_index: usize,
180        /// Hashed address at `account_index`.
181        account_hash: B256,
182        /// Inclusive slot origin for the next request.
183        starting_hash: B256,
184    },
185    /// A later requested account was not returned.
186    NextAccount {
187        /// Position in the original account list.
188        account_index: usize,
189        /// Hashed address at `account_index`.
190        account_hash: B256,
191    },
192}
193
194/// A storage request that does not match its authenticated accounts.
195#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
196pub enum InvalidStorageRangeRequest {
197    /// The request targets a different state root than the one that authenticated the accounts.
198    #[error(
199        "storage range requests state root {requested}, but accounts are authenticated by {authenticated}"
200    )]
201    StateRootMismatch {
202        /// State root in the wire request.
203        requested: B256,
204        /// State root the account batch was verified against.
205        authenticated: B256,
206    },
207    /// No account was requested.
208    #[error("storage range request contains no accounts")]
209    NoAccounts,
210    /// A finite limit with a zero origin was requested for multiple accounts.
211    #[error("limited storage range request contains {accounts} accounts")]
212    LimitedMultipleAccounts {
213        /// Number of requested accounts.
214        accounts: usize,
215    },
216    /// The inclusive bounds are reversed.
217    #[error("storage range origin {origin} exceeds limit {limit}")]
218    ReversedBounds {
219        /// Requested inclusive origin.
220        origin: B256,
221        /// Requested inclusive limit.
222        limit: B256,
223    },
224    /// The request and authenticated batch have different lengths.
225    #[error("storage range request has {requested} accounts but {supplied} were supplied")]
226    AccountCount {
227        /// Number of requested accounts.
228        requested: usize,
229        /// Number of authenticated accounts.
230        supplied: usize,
231    },
232    /// A requested account differs from its authenticated position.
233    #[error(
234        "storage range account {index} requests {requested}, but authenticated account is {supplied}"
235    )]
236    AccountMismatch {
237        /// Position of the mismatch.
238        index: usize,
239        /// Hash in the wire request.
240        requested: B256,
241        /// Hash in the authenticated batch.
242        supplied: B256,
243    },
244    /// An account has no storage to download.
245    #[error("storage range account {index} ({account_hash}) has an empty storage root")]
246    EmptyStorageRoot {
247        /// Position of the account in the request.
248        index: usize,
249        /// Hashed address of the account.
250        account_hash: B256,
251    },
252}
253
254// Checks a storage response against the request and the account roots it was built from.
255#[derive(Clone, Debug)]
256struct StorageRangeVerifier {
257    // The request being answered, which every response is bound to.
258    request: GetStorageRangesMessage,
259    // Authenticated storage root per requested account, in the same order.
260    storage_roots: Vec<B256>,
261}
262
263impl SnapVerifier for StorageRangeVerifier {
264    type Request = GetStorageRangesMessage;
265    type Output = StorageRangeOutcome;
266
267    fn verify(self, peer_id: PeerId, response: SnapResponse) -> Result<Self::Output, RequestError> {
268        self.verify_response(peer_id, response)
269    }
270}
271
272impl StorageRangeVerifier {
273    // Every returned list must match the account at the same request position.
274    fn verify_response(
275        &self,
276        peer_id: PeerId,
277        response: SnapResponse,
278    ) -> Result<StorageRangeOutcome, RequestError> {
279        let Some(response) = self.accepted_response(response)? else {
280            return Ok(StorageRangeOutcome::Unavailable { peer_id })
281        };
282
283        // Only the last returned range may carry the response's shared boundary proof.
284        let proof_index =
285            response.slots.len().checked_sub(1).filter(|_| !response.proof.is_empty());
286        let mut ranges = Vec::with_capacity(response.slots.len());
287        let mut bounded_next = None;
288
289        for (index, slots) in response.slots.iter().enumerate() {
290            let proof = if proof_index == Some(index) { response.proof.as_slice() } else { &[] };
291            let verified = self.verify_range(index, slots, proof)?;
292            ranges.push(verified.range);
293            bounded_next = verified.within_limit;
294        }
295
296        let continuation = self.continuation(&ranges, bounded_next);
297        Ok(StorageRangeOutcome::Verified(VerifiedStorageRanges {
298            ranges,
299            continuation,
300            request: Box::new(self.request.clone()),
301        }))
302    }
303
304    // Binds the reply to this request before any peer-supplied data is trusted. `None` means the
305    // responder simply lacks the state.
306    fn accepted_response(
307        &self,
308        response: SnapResponse,
309    ) -> Result<Option<StorageRangesMessage>, RequestError> {
310        let SnapResponse::StorageRanges(mut response) = response else {
311            debug!(target: "downloaders::snap", "Expected storage ranges response");
312            return Err(RequestError::BadResponse)
313        };
314        if response.request_id != self.request.request_id {
315            debug!(
316                target: "downloaders::snap",
317                expected = self.request.request_id,
318                got = response.request_id,
319                "Storage ranges response id mismatch"
320            );
321            return Err(RequestError::BadResponse)
322        }
323        if response.slots.len() > self.request.account_hashes.len() {
324            debug!(target: "downloaders::snap", "Storage response contains extra ranges");
325            return Err(RequestError::BadResponse)
326        }
327        if response.slots.is_empty() {
328            if response.proof.is_empty() {
329                return Ok(None)
330            }
331            // A boundary proof can authenticate an empty suffix without an encoded inner list.
332            response.slots.push(Vec::new());
333        }
334        Ok(Some(response))
335    }
336
337    // Authenticates one account's slots against its storage root.
338    fn verify_range(
339        &self,
340        index: usize,
341        slots: &[StorageData],
342        proof: &[alloy_primitives::Bytes],
343    ) -> Result<VerifiedRange, RequestError> {
344        let account_hash = self.request.account_hashes[index];
345        // Only the first account inherits the request bounds; the rest are whole tries.
346        let (origin, limit) = if index == 0 {
347            (
348                self.request.starting_hash.unwrap_or(B256::ZERO),
349                self.request.limit_hash.unwrap_or(MAX_HASH),
350            )
351        } else {
352            (B256::ZERO, MAX_HASH)
353        };
354
355        // One boundary slot may sit past the inclusive limit and is verified before removal.
356        if slots.iter().filter(|slot| slot.hash > limit).nth(1).is_some() {
357            debug!(target: "downloaders::snap", %account_hash, "Storage range exceeds limit");
358            return Err(RequestError::BadResponse)
359        }
360
361        let mut decoded = Self::decode_slots(account_hash, origin, slots)?;
362        let leaves = decoded.iter().map(|(hash, value)| (*hash, alloy_rlp::encode(value)));
363        let next = verify_range_proof(self.storage_roots[index], origin, limit, leaves, proof)
364            .map_err(|error| {
365                debug!(
366                    target: "downloaders::snap",
367                    %account_hash,
368                    %error,
369                    "Invalid storage range proof"
370                );
371                RequestError::BadResponse
372            })?;
373
374        // Resuming at or before the origin would reissue this request forever, so a peer that
375        // reports one has withheld a slot it was asked for.
376        if next.is_some_and(|next| next <= origin) {
377            debug!(target: "downloaders::snap", %account_hash, "Storage range does not advance");
378            return Err(RequestError::BadResponse)
379        }
380
381        decoded.truncate(decoded.partition_point(|(hash, _)| *hash <= limit));
382        Ok(VerifiedRange {
383            range: VerifiedStorageRange { account_hash, slots: decoded },
384            within_limit: next.filter(|next| *next <= limit),
385        })
386    }
387
388    // A response is incomplete either part way through its last account, or because a requested
389    // account was omitted entirely.
390    fn continuation(
391        &self,
392        ranges: &[VerifiedStorageRange],
393        final_next: Option<B256>,
394    ) -> Option<StorageRangeContinuation> {
395        if let Some(starting_hash) = final_next {
396            return Some(StorageRangeContinuation::Partial {
397                account_index: ranges.len() - 1,
398                account_hash: ranges.last().expect("a response range exists").account_hash,
399                starting_hash,
400            })
401        }
402        let account_index = ranges.len();
403        self.request.account_hashes.get(account_index).copied().map(|account_hash| {
404            StorageRangeContinuation::NextAccount { account_index, account_hash }
405        })
406    }
407
408    // Storage values are canonical non-zero trie leaves.
409    fn decode_slots(
410        account_hash: B256,
411        origin: B256,
412        slots: &[StorageData],
413    ) -> Result<Vec<(B256, U256)>, RequestError> {
414        let mut decoded = Vec::with_capacity(slots.len());
415        let mut previous = None;
416
417        for slot in slots {
418            if slot.hash < origin || previous.is_some_and(|previous| slot.hash <= previous) {
419                debug!(
420                    target: "downloaders::snap",
421                    %account_hash,
422                    "Storage slots precede origin or are not strictly ordered"
423                );
424                return Err(RequestError::BadResponse)
425            }
426            let value = slot.value().map_err(|error| {
427                debug!(target: "downloaders::snap", %account_hash, %error, "Invalid storage value");
428                RequestError::BadResponse
429            })?;
430            if value.is_zero() {
431                debug!(target: "downloaders::snap", %account_hash, "Storage trie contains zero leaf");
432                return Err(RequestError::BadResponse)
433            }
434            previous = Some(slot.hash);
435            decoded.push((slot.hash, value));
436        }
437        Ok(decoded)
438    }
439}
440
441// One account's verified slots and whether it continues within the request limit.
442struct VerifiedRange {
443    // The account's slots, once its proof checked out.
444    range: VerifiedStorageRange,
445    // First slot after the range when it remains within the requested limit.
446    within_limit: Option<B256>,
447}
448
449#[cfg(test)]
450mod tests {
451    use super::{
452        super::{request::MAX_RETRIES, test_utils::TestSnapClient, VerifiedAccountRange},
453        *,
454    };
455    use alloy_primitives::{Bytes, KECCAK256_EMPTY};
456    use reth_network_p2p::{error::PeerRequestResult, priority::Priority};
457    use reth_network_peers::WithPeerId;
458    use reth_trie_common::{
459        proof::ProofRetainer, HashBuilder, Nibbles, TrieAccount, EMPTY_ROOT_HASH,
460    };
461    use std::sync::Arc;
462
463    fn key(value: u64) -> B256 {
464        B256::left_padding_from(&value.to_be_bytes())
465    }
466
467    // Storage leaves are RLP-encoded non-zero integers, so the trie value is the encoded slot.
468    fn slots(values: &[(B256, u64)]) -> Vec<(B256, U256)> {
469        values.iter().map(|(hash, value)| (*hash, U256::from(*value))).collect()
470    }
471
472    fn storage_root(slots: &[(B256, U256)], targets: &[B256]) -> (B256, Vec<Bytes>) {
473        let targets = targets.iter().copied().map(Nibbles::unpack).collect();
474        let mut builder = HashBuilder::default().with_proof_retainer(ProofRetainer::new(targets));
475        for (hash, value) in slots {
476            builder.add_leaf(Nibbles::unpack(*hash), &alloy_rlp::encode(value));
477        }
478        let root = builder.root();
479        let proof = builder
480            .take_proof_nodes()
481            .into_nodes_sorted()
482            .into_iter()
483            .map(|(_, node)| node)
484            .collect();
485        (root, proof)
486    }
487
488    // Every request in these tests targets the state root that authenticated its accounts.
489    const STATE_ROOT: B256 = B256::repeat_byte(0xaa);
490
491    // Accounts reach the downloader only through a range that authenticated them.
492    fn verified_range(accounts: &[(B256, TrieAccount)]) -> VerifiedAccountRange {
493        VerifiedAccountRange {
494            state_root: STATE_ROOT,
495            accounts: accounts.to_vec(),
496            has_more: false,
497            next: None,
498        }
499    }
500
501    fn account(storage_root: B256) -> TrieAccount {
502        TrieAccount { nonce: 1, balance: U256::from(2), storage_root, code_hash: KECCAK256_EMPTY }
503    }
504
505    fn wire_slots(slots: &[(B256, U256)]) -> Vec<StorageData> {
506        slots.iter().map(|(hash, value)| StorageData::from_value(*hash, *value)).collect()
507    }
508
509    fn account_refs(accounts: &[(B256, TrieAccount)]) -> Vec<(B256, &TrieAccount)> {
510        accounts.iter().map(|(hash, account)| (*hash, account)).collect()
511    }
512
513    fn request(accounts: &[(B256, TrieAccount)]) -> GetStorageRangesMessage {
514        GetStorageRangesMessage {
515            request_id: 1,
516            root_hash: STATE_ROOT,
517            account_hashes: accounts.iter().map(|(hash, _)| *hash).collect(),
518            starting_hash: B256::ZERO.into(),
519            limit_hash: MAX_HASH.into(),
520            response_bytes: 512 * 1024,
521        }
522    }
523
524    fn response(
525        peer: PeerId,
526        request_id: u64,
527        slots: Vec<Vec<StorageData>>,
528        proof: Vec<Bytes>,
529    ) -> PeerRequestResult<SnapResponse> {
530        Ok(WithPeerId::new(
531            peer,
532            SnapResponse::StorageRanges(StorageRangesMessage { request_id, slots, proof }),
533        ))
534    }
535
536    fn rejecting_client(
537        response: impl Fn(PeerId) -> PeerRequestResult<SnapResponse>,
538    ) -> Arc<TestSnapClient> {
539        let peer = PeerId::random();
540        Arc::new(TestSnapClient::new((0..=MAX_RETRIES).map(|_| response(peer))))
541    }
542
543    fn downloader<C: SnapClient>(
544        client: C,
545        request: GetStorageRangesMessage,
546        accounts: &[(B256, TrieAccount)],
547    ) -> Result<StorageRangeDownloader<C>, InvalidStorageRangeRequest> {
548        let range = verified_range(accounts);
549        StorageRangeDownloader::new(client, request, &range.batch(), Runtime::test())
550    }
551
552    #[tokio::test]
553    async fn complete_ranges_for_multiple_accounts_are_verified() {
554        let first = slots(&[(key(1), 11), (key(2), 12)]);
555        let second = slots(&[(key(3), 13)]);
556        let (first_root, _) = storage_root(&first, &[]);
557        let (second_root, proof) = storage_root(&second, &[key(3)]);
558        let accounts = vec![(key(100), account(first_root)), (key(200), account(second_root))];
559        let client = Arc::new(TestSnapClient::new([response(
560            PeerId::random(),
561            1,
562            vec![wire_slots(&first), wire_slots(&second)],
563            proof,
564        )]));
565
566        let outcome =
567            downloader(Arc::clone(&client), request(&accounts), &accounts).unwrap().await.unwrap();
568
569        let StorageRangeOutcome::Verified(verified) = outcome else { panic!("verified ranges") };
570        assert_eq!(verified.ranges.len(), 2);
571        assert_eq!(verified.ranges[0].slots, first);
572        assert_eq!(verified.ranges[1].slots, second);
573        assert_eq!(verified.continuation, None);
574        assert!(client.reported().is_empty());
575    }
576
577    #[tokio::test]
578    async fn partial_final_range_reports_its_slot_continuation() {
579        let all = slots(&[(key(1), 11), (key(2), 12), (key(3), 13)]);
580        let (root, proof) = storage_root(&all, &[B256::ZERO, key(1)]);
581        let accounts = vec![(key(100), account(root))];
582        let client = Arc::new(TestSnapClient::new([response(
583            PeerId::random(),
584            1,
585            vec![wire_slots(&all[..1])],
586            proof,
587        )]));
588
589        let outcome =
590            downloader(Arc::clone(&client), request(&accounts), &accounts).unwrap().await.unwrap();
591
592        let StorageRangeOutcome::Verified(verified) = outcome else { panic!("verified ranges") };
593        assert_eq!(verified.ranges[0].slots, all[..1]);
594        assert_eq!(
595            verified.continuation,
596            Some(StorageRangeContinuation::Partial {
597                account_index: 0,
598                account_hash: key(100),
599                starting_hash: key(2),
600            })
601        );
602        assert!(client.reported().is_empty());
603    }
604
605    #[tokio::test]
606    async fn an_omitted_account_resumes_at_the_next_position() {
607        let first = slots(&[(key(1), 11)]);
608        let (first_root, _) = storage_root(&first, &[]);
609        let accounts =
610            vec![(key(100), account(first_root)), (key(200), account(B256::repeat_byte(0xbb)))];
611        let client = Arc::new(TestSnapClient::new([response(
612            PeerId::random(),
613            1,
614            vec![wire_slots(&first)],
615            Vec::new(),
616        )]));
617
618        let outcome =
619            downloader(Arc::clone(&client), request(&accounts), &accounts).unwrap().await.unwrap();
620
621        let StorageRangeOutcome::Verified(verified) = outcome else { panic!("verified ranges") };
622        assert_eq!(verified.ranges.len(), 1);
623        assert_eq!(
624            verified.continuation,
625            Some(StorageRangeContinuation::NextAccount {
626                account_index: 1,
627                account_hash: key(200),
628            })
629        );
630    }
631
632    #[test]
633    fn empty_storage_roots_must_be_filtered_before_submission() {
634        let accounts = vec![
635            (key(100), account(B256::repeat_byte(0xaa))),
636            (key(200), account(EMPTY_ROOT_HASH)),
637            (key(300), account(B256::repeat_byte(0xbb))),
638        ];
639        let client = TestSnapClient::new([response(PeerId::random(), 1, Vec::new(), Vec::new())]);
640
641        assert_eq!(
642            downloader(&client, request(&accounts), &accounts).unwrap_err(),
643            InvalidStorageRangeRequest::EmptyStorageRoot { index: 1, account_hash: key(200) }
644        );
645        assert!(client.priorities().is_empty());
646
647        let range = verified_range(&accounts);
648        let batch = range.storage_batch();
649        let mut storage_request = request(&accounts);
650        storage_request.account_hashes = batch.accounts().iter().map(|(hash, _)| *hash).collect();
651        assert_eq!(storage_request.account_hashes, vec![key(100), key(300)]);
652        assert!(
653            StorageRangeDownloader::new(&client, storage_request, &batch, Runtime::test()).is_ok()
654        );
655        assert_eq!(*client.priorities(), [Priority::Normal]);
656    }
657
658    #[tokio::test]
659    async fn a_response_without_slots_or_proof_reports_the_state_unavailable() {
660        let batches = [
661            vec![(key(100), account(B256::repeat_byte(0xbb)))],
662            vec![
663                (key(100), account(B256::repeat_byte(0xaa))),
664                (key(200), account(B256::repeat_byte(0xbb))),
665            ],
666        ];
667        for accounts in batches {
668            let peer = PeerId::random();
669            let client = Arc::new(TestSnapClient::new([response(peer, 1, Vec::new(), Vec::new())]));
670
671            let outcome = downloader(Arc::clone(&client), request(&accounts), &accounts)
672                .unwrap()
673                .await
674                .unwrap();
675
676            assert_eq!(outcome, StorageRangeOutcome::Unavailable { peer_id: peer });
677            // Lacking the state is not misbehaviour.
678            assert!(client.reported().is_empty());
679        }
680    }
681
682    #[tokio::test]
683    async fn a_mismatched_request_id_exhausts_the_retry_budget() {
684        let all = slots(&[(key(1), 11)]);
685        let (root, _) = storage_root(&all, &[]);
686        let accounts = vec![(key(100), account(root))];
687
688        let client = rejecting_client(|peer| response(peer, 9, vec![wire_slots(&all)], Vec::new()));
689        let error = downloader(Arc::clone(&client), request(&accounts), &accounts)
690            .unwrap()
691            .await
692            .unwrap_err();
693
694        assert_eq!(error, RequestError::BadResponse);
695        // One penalty per attempt, then the budget is spent.
696        assert_eq!(client.reported().len(), usize::from(MAX_RETRIES) + 1);
697    }
698
699    #[tokio::test]
700    async fn a_range_proved_against_another_root_is_rejected() {
701        let all = slots(&[(key(1), 11)]);
702        let accounts = vec![(key(100), account(B256::repeat_byte(0xcc)))];
703
704        let client = rejecting_client(|peer| response(peer, 1, vec![wire_slots(&all)], Vec::new()));
705        let error = downloader(Arc::clone(&client), request(&accounts), &accounts)
706            .unwrap()
707            .await
708            .unwrap_err();
709
710        assert_eq!(error, RequestError::BadResponse);
711        assert_eq!(client.reported().len(), usize::from(MAX_RETRIES) + 1);
712    }
713
714    #[tokio::test]
715    async fn slots_that_are_not_strictly_ordered_penalize_the_peer_before_a_retry_succeeds() {
716        let all = slots(&[(key(1), 11), (key(2), 12)]);
717        let (root, _) = storage_root(&all, &[]);
718        let accounts = vec![(key(100), account(root))];
719        let bad_peer = PeerId::random();
720        let good_peer = PeerId::random();
721        let mut reversed = wire_slots(&all);
722        reversed.reverse();
723        let client = Arc::new(TestSnapClient::new([
724            response(bad_peer, 1, vec![reversed], Vec::new()),
725            response(good_peer, 1, vec![wire_slots(&all)], Vec::new()),
726        ]));
727
728        let outcome =
729            downloader(Arc::clone(&client), request(&accounts), &accounts).unwrap().await.unwrap();
730
731        let StorageRangeOutcome::Verified(verified) = outcome else { panic!("verified ranges") };
732        assert_eq!(verified.ranges[0].slots, all);
733        assert_eq!(*client.reported(), [bad_peer]);
734        assert_eq!(*client.priorities(), [Priority::Normal, Priority::High]);
735    }
736
737    #[test]
738    fn a_request_that_does_not_match_its_accounts_is_refused() {
739        let accounts = vec![(key(100), account(EMPTY_ROOT_HASH))];
740        let client = TestSnapClient::new([]);
741
742        let mut mismatched = request(&accounts);
743        mismatched.account_hashes = vec![key(999)];
744        let error = downloader(&client, mismatched, &accounts).unwrap_err();
745        assert_eq!(
746            error,
747            InvalidStorageRangeRequest::AccountMismatch {
748                index: 0,
749                requested: key(999),
750                supplied: key(100),
751            }
752        );
753
754        let mut empty = request(&accounts);
755        empty.account_hashes.clear();
756        assert_eq!(
757            downloader(&client, empty, &accounts).unwrap_err(),
758            InvalidStorageRangeRequest::NoAccounts
759        );
760
761        let mut reversed = request(&accounts);
762        reversed.starting_hash = key(9).into();
763        reversed.limit_hash = key(1).into();
764        assert_eq!(
765            downloader(&client, reversed, &accounts).unwrap_err(),
766            InvalidStorageRangeRequest::ReversedBounds { origin: key(9), limit: key(1) }
767        );
768    }
769
770    #[test]
771    fn a_request_for_another_state_root_is_refused() {
772        let accounts = vec![(key(100), account(EMPTY_ROOT_HASH))];
773        let client = TestSnapClient::new([]);
774
775        let mut other_root = request(&accounts);
776        other_root.root_hash = B256::repeat_byte(0xcc);
777        assert_eq!(
778            downloader(&client, other_root, &accounts).unwrap_err(),
779            InvalidStorageRangeRequest::StateRootMismatch {
780                requested: B256::repeat_byte(0xcc),
781                authenticated: STATE_ROOT,
782            }
783        );
784    }
785
786    #[tokio::test]
787    async fn a_trie_continuing_past_the_limit_needs_no_follow_up() {
788        let all = slots(&[(key(1), 11), (key(2), 12), (key(3), 13)]);
789        let (root, proof) = storage_root(&all, &[B256::ZERO, key(2)]);
790        let accounts = vec![(key(100), account(root))];
791        let client = Arc::new(TestSnapClient::new([response(
792            PeerId::random(),
793            1,
794            vec![wire_slots(&all[..2])],
795            proof,
796        )]));
797        let mut bounded = request(&accounts);
798        bounded.limit_hash = key(2).into();
799
800        let outcome = downloader(Arc::clone(&client), bounded, &accounts).unwrap().await.unwrap();
801
802        let StorageRangeOutcome::Verified(verified) = outcome else { panic!("verified ranges") };
803        let range = verified_range(&accounts);
804        assert_eq!(verified.follow_up(2, range.batch()).unwrap(), None);
805        assert_eq!(verified.into_ranges()[0].slots, all[..2]);
806    }
807
808    #[tokio::test]
809    async fn a_follow_up_keeps_the_limit_only_at_the_first_account() {
810        let all = slots(&[(key(1), 11), (key(2), 12), (key(3), 13)]);
811        let (root, proof) = storage_root(&all, &[B256::ZERO, key(1)]);
812        let accounts = vec![(key(100), account(root))];
813        let client = Arc::new(TestSnapClient::new([response(
814            PeerId::random(),
815            1,
816            vec![wire_slots(&all[..1])],
817            proof,
818        )]));
819        let mut bounded = request(&accounts);
820        bounded.limit_hash = key(5).into();
821
822        let outcome =
823            downloader(Arc::clone(&client), bounded.clone(), &accounts).unwrap().await.unwrap();
824
825        let StorageRangeOutcome::Verified(verified) = outcome else { panic!("verified ranges") };
826        let range = verified_range(&accounts);
827        let (follow_up, narrowed) = verified.follow_up(2, range.batch()).unwrap().unwrap();
828        assert_eq!(narrowed.accounts(), account_refs(&accounts));
829        assert_eq!(follow_up.request_id, 2);
830        assert_eq!(follow_up.root_hash, bounded.root_hash);
831        assert_eq!(follow_up.account_hashes, bounded.account_hashes);
832        assert_eq!(follow_up.starting_hash, key(2).into());
833        assert_eq!(follow_up.limit_hash, key(5).into());
834    }
835
836    // Every account after the first is authenticated as a whole trie, so its follow-up must drop
837    // the bounds the first account was requested with.
838    #[tokio::test]
839    async fn a_follow_up_past_the_first_account_is_unbounded() {
840        let first = slots(&[(key(1), 11)]);
841        let second = slots(&[(key(1), 21), (key(2), 22)]);
842        let (first_root, _) = storage_root(&first, &[]);
843        let (second_root, proof) = storage_root(&second, &[B256::ZERO, key(1)]);
844        let accounts = vec![(key(100), account(first_root)), (key(200), account(second_root))];
845        let client = Arc::new(TestSnapClient::new([response(
846            PeerId::random(),
847            1,
848            vec![wire_slots(&first), wire_slots(&second[..1])],
849            proof,
850        )]));
851
852        let outcome =
853            downloader(Arc::clone(&client), request(&accounts), &accounts).unwrap().await.unwrap();
854
855        let StorageRangeOutcome::Verified(verified) = outcome else { panic!("verified ranges") };
856        assert_eq!(
857            verified.continuation,
858            Some(StorageRangeContinuation::Partial {
859                account_index: 1,
860                account_hash: key(200),
861                starting_hash: key(2),
862            })
863        );
864
865        let range = verified_range(&accounts);
866        let (follow_up, narrowed) = verified.follow_up(2, range.batch()).unwrap().unwrap();
867        assert_eq!(narrowed.accounts(), account_refs(&accounts[1..]));
868        assert_eq!(follow_up.account_hashes, vec![key(200)]);
869        assert_eq!(follow_up.starting_hash, key(2).into());
870        assert_eq!(follow_up.limit_hash, RangeBound::default());
871    }
872
873    // Each follow-up must resume strictly past the last one, so the loop terminates.
874    #[tokio::test]
875    async fn resuming_a_partial_range_advances_until_the_trie_is_exhausted() {
876        let all = slots(&[(key(1), 11), (key(2), 12), (key(3), 13)]);
877        let (root, _) = storage_root(&all, &[]);
878        let accounts = vec![(key(100), account(root))];
879        let range = verified_range(&accounts);
880        let mut request = request(&accounts);
881        let mut origins = Vec::new();
882        let mut collected = Vec::new();
883
884        for served in 0..all.len() {
885            let origin = request.starting_hash.unwrap_or(B256::ZERO);
886            origins.push(origin);
887            let (_, proof) = storage_root(&all, &[origin, all[served].0]);
888            let client = Arc::new(TestSnapClient::new([response(
889                PeerId::random(),
890                request.request_id,
891                vec![wire_slots(&all[served..=served])],
892                proof,
893            )]));
894
895            let outcome =
896                downloader(Arc::clone(&client), request.clone(), &accounts).unwrap().await.unwrap();
897            let StorageRangeOutcome::Verified(verified) = outcome else {
898                panic!("verified ranges")
899            };
900            collected.extend(verified.ranges[0].slots.clone());
901
902            let Some((follow_up, _)) =
903                verified.follow_up(request.request_id + 1, range.batch()).unwrap()
904            else {
905                assert_eq!(served, all.len() - 1);
906                break
907            };
908            request = follow_up;
909        }
910
911        assert_eq!(collected, all);
912        assert_eq!(origins, vec![B256::ZERO, key(2), key(3)]);
913    }
914
915    // A non-zero origin makes the first account unambiguously partial, so servers stop there and
916    // the shared proof can still be assigned to it.
917    #[test]
918    fn a_zero_origin_limited_request_must_not_carry_several_accounts() {
919        let accounts = vec![
920            (key(100), account(B256::repeat_byte(0xaa))),
921            (key(200), account(B256::repeat_byte(0xbb))),
922        ];
923        // Accepted requests are submitted on construction, so the client must have replies ready.
924        let client =
925            TestSnapClient::new([response(PeerId::random(), 1, vec![Vec::new()], Vec::new())]);
926
927        let mut bounded = request(&accounts);
928        bounded.limit_hash = key(5).into();
929        assert_eq!(
930            downloader(&client, bounded.clone(), &accounts).unwrap_err(),
931            InvalidStorageRangeRequest::LimitedMultipleAccounts { accounts: 2 }
932        );
933
934        bounded.starting_hash = key(1).into();
935        assert!(downloader(&client, bounded, &accounts).is_ok());
936    }
937
938    // Consecutive account transitions must chain: each follow-up is driven by the batch the
939    // previous one returned, never by accounts the caller re-derived.
940    #[tokio::test]
941    async fn consecutive_account_transitions_carry_their_batch_forward() {
942        let served = slots(&[(key(1), 11)]);
943        let (root, _) = storage_root(&served, &[]);
944        let accounts =
945            vec![(key(100), account(root)), (key(200), account(root)), (key(300), account(root))];
946        let range = verified_range(&accounts);
947        let mut request = request(&accounts);
948        let mut batch = range.batch();
949
950        for remaining in (1..accounts.len()).rev() {
951            let client = Arc::new(TestSnapClient::new([response(
952                PeerId::random(),
953                request.request_id,
954                vec![wire_slots(&served)],
955                Vec::new(),
956            )]));
957            let outcome = StorageRangeDownloader::new(
958                Arc::clone(&client),
959                request.clone(),
960                &batch,
961                Runtime::test(),
962            )
963            .unwrap()
964            .await
965            .unwrap();
966
967            let StorageRangeOutcome::Verified(verified) = outcome else {
968                panic!("verified ranges")
969            };
970            let (follow_up, narrowed) =
971                verified.follow_up(request.request_id + 1, batch).unwrap().unwrap();
972            assert_eq!(follow_up.account_hashes.len(), remaining);
973            assert_eq!(narrowed.accounts(), account_refs(&accounts[accounts.len() - remaining..]));
974            request = follow_up;
975            batch = narrowed;
976        }
977
978        assert_eq!(request.account_hashes, vec![key(300)]);
979    }
980
981    #[tokio::test]
982    async fn a_follow_up_refuses_a_batch_from_another_request() {
983        let served = slots(&[(key(1), 11)]);
984        let (root, _) = storage_root(&served, &[]);
985        let accounts = vec![(key(100), account(root)), (key(200), account(root))];
986        let client = Arc::new(TestSnapClient::new([response(
987            PeerId::random(),
988            1,
989            vec![wire_slots(&served)],
990            Vec::new(),
991        )]));
992
993        let outcome =
994            downloader(Arc::clone(&client), request(&accounts), &accounts).unwrap().await.unwrap();
995        let StorageRangeOutcome::Verified(verified) = outcome else { panic!("verified ranges") };
996
997        let others = vec![(key(900), account(root)), (key(901), account(root))];
998        let other_range = verified_range(&others);
999        assert_eq!(
1000            verified.follow_up(2, other_range.batch()).unwrap_err(),
1001            InvalidStorageRangeRequest::AccountMismatch {
1002                index: 0,
1003                requested: key(100),
1004                supplied: key(900),
1005            }
1006        );
1007    }
1008}