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;
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            origin: B256::ZERO,
496            accounts: accounts.to_vec(),
497            has_more: false,
498            next: None,
499        }
500    }
501
502    fn account(storage_root: B256) -> TrieAccount {
503        TrieAccount { nonce: 1, balance: U256::from(2), storage_root, ..Default::default() }
504    }
505
506    fn wire_slots(slots: &[(B256, U256)]) -> Vec<StorageData> {
507        slots.iter().map(|(hash, value)| StorageData::from_value(*hash, *value)).collect()
508    }
509
510    fn account_refs(accounts: &[(B256, TrieAccount)]) -> Vec<(B256, &TrieAccount)> {
511        accounts.iter().map(|(hash, account)| (*hash, account)).collect()
512    }
513
514    fn request(accounts: &[(B256, TrieAccount)]) -> GetStorageRangesMessage {
515        GetStorageRangesMessage {
516            request_id: 1,
517            root_hash: STATE_ROOT,
518            account_hashes: accounts.iter().map(|(hash, _)| *hash).collect(),
519            starting_hash: B256::ZERO.into(),
520            limit_hash: MAX_HASH.into(),
521            response_bytes: 512 * 1024,
522        }
523    }
524
525    fn response(
526        peer: PeerId,
527        request_id: u64,
528        slots: Vec<Vec<StorageData>>,
529        proof: Vec<Bytes>,
530    ) -> PeerRequestResult<SnapResponse> {
531        Ok(WithPeerId::new(
532            peer,
533            SnapResponse::StorageRanges(StorageRangesMessage { request_id, slots, proof }),
534        ))
535    }
536
537    fn rejecting_client(
538        response: impl Fn(PeerId) -> PeerRequestResult<SnapResponse>,
539    ) -> Arc<TestSnapClient> {
540        let peer = PeerId::random();
541        Arc::new(TestSnapClient::new((0..=MAX_RETRIES).map(|_| response(peer))))
542    }
543
544    fn downloader<C: SnapClient>(
545        client: C,
546        request: GetStorageRangesMessage,
547        accounts: &[(B256, TrieAccount)],
548    ) -> Result<StorageRangeDownloader<C>, InvalidStorageRangeRequest> {
549        let range = verified_range(accounts);
550        StorageRangeDownloader::new(client, request, &range.batch(), Runtime::test())
551    }
552
553    #[tokio::test]
554    async fn complete_ranges_for_multiple_accounts_are_verified() {
555        let first = slots(&[(key(1), 11), (key(2), 12)]);
556        let second = slots(&[(key(3), 13)]);
557        let (first_root, _) = storage_root(&first, &[]);
558        let (second_root, proof) = storage_root(&second, &[key(3)]);
559        let accounts = vec![(key(100), account(first_root)), (key(200), account(second_root))];
560        let client = Arc::new(TestSnapClient::new([response(
561            PeerId::random(),
562            1,
563            vec![wire_slots(&first), wire_slots(&second)],
564            proof,
565        )]));
566
567        let outcome =
568            downloader(Arc::clone(&client), request(&accounts), &accounts).unwrap().await.unwrap();
569
570        let StorageRangeOutcome::Verified(verified) = outcome else { panic!("verified ranges") };
571        assert_eq!(verified.ranges.len(), 2);
572        assert_eq!(verified.ranges[0].slots, first);
573        assert_eq!(verified.ranges[1].slots, second);
574        assert_eq!(verified.continuation, None);
575        assert!(client.reported().is_empty());
576    }
577
578    #[tokio::test]
579    async fn partial_final_range_reports_its_slot_continuation() {
580        let all = slots(&[(key(1), 11), (key(2), 12), (key(3), 13)]);
581        let (root, proof) = storage_root(&all, &[B256::ZERO, key(1)]);
582        let accounts = vec![(key(100), account(root))];
583        let client = Arc::new(TestSnapClient::new([response(
584            PeerId::random(),
585            1,
586            vec![wire_slots(&all[..1])],
587            proof,
588        )]));
589
590        let outcome =
591            downloader(Arc::clone(&client), request(&accounts), &accounts).unwrap().await.unwrap();
592
593        let StorageRangeOutcome::Verified(verified) = outcome else { panic!("verified ranges") };
594        assert_eq!(verified.ranges[0].slots, all[..1]);
595        assert_eq!(
596            verified.continuation,
597            Some(StorageRangeContinuation::Partial {
598                account_index: 0,
599                account_hash: key(100),
600                starting_hash: key(2),
601            })
602        );
603        assert!(client.reported().is_empty());
604    }
605
606    #[tokio::test]
607    async fn an_omitted_account_resumes_at_the_next_position() {
608        let first = slots(&[(key(1), 11)]);
609        let (first_root, _) = storage_root(&first, &[]);
610        let accounts =
611            vec![(key(100), account(first_root)), (key(200), account(B256::repeat_byte(0xbb)))];
612        let client = Arc::new(TestSnapClient::new([response(
613            PeerId::random(),
614            1,
615            vec![wire_slots(&first)],
616            Vec::new(),
617        )]));
618
619        let outcome =
620            downloader(Arc::clone(&client), request(&accounts), &accounts).unwrap().await.unwrap();
621
622        let StorageRangeOutcome::Verified(verified) = outcome else { panic!("verified ranges") };
623        assert_eq!(verified.ranges.len(), 1);
624        assert_eq!(
625            verified.continuation,
626            Some(StorageRangeContinuation::NextAccount {
627                account_index: 1,
628                account_hash: key(200),
629            })
630        );
631    }
632
633    #[test]
634    fn empty_storage_roots_must_be_filtered_before_submission() {
635        let accounts = vec![
636            (key(100), account(B256::repeat_byte(0xaa))),
637            (key(200), account(EMPTY_ROOT_HASH)),
638            (key(300), account(B256::repeat_byte(0xbb))),
639        ];
640        let client = TestSnapClient::new([response(PeerId::random(), 1, Vec::new(), Vec::new())]);
641
642        assert_eq!(
643            downloader(&client, request(&accounts), &accounts).unwrap_err(),
644            InvalidStorageRangeRequest::EmptyStorageRoot { index: 1, account_hash: key(200) }
645        );
646        assert!(client.priorities().is_empty());
647
648        let range = verified_range(&accounts);
649        let batch = range.storage_batch();
650        let mut storage_request = request(&accounts);
651        storage_request.account_hashes = batch.accounts().iter().map(|(hash, _)| *hash).collect();
652        assert_eq!(storage_request.account_hashes, vec![key(100), key(300)]);
653        assert!(
654            StorageRangeDownloader::new(&client, storage_request, &batch, Runtime::test()).is_ok()
655        );
656        assert_eq!(*client.priorities(), [Priority::Normal]);
657    }
658
659    #[tokio::test]
660    async fn a_response_without_slots_or_proof_reports_the_state_unavailable() {
661        let batches = [
662            vec![(key(100), account(B256::repeat_byte(0xbb)))],
663            vec![
664                (key(100), account(B256::repeat_byte(0xaa))),
665                (key(200), account(B256::repeat_byte(0xbb))),
666            ],
667        ];
668        for accounts in batches {
669            let peer = PeerId::random();
670            let client = Arc::new(TestSnapClient::new([response(peer, 1, Vec::new(), Vec::new())]));
671
672            let outcome = downloader(Arc::clone(&client), request(&accounts), &accounts)
673                .unwrap()
674                .await
675                .unwrap();
676
677            assert_eq!(outcome, StorageRangeOutcome::Unavailable { peer_id: peer });
678            // Lacking the state is not misbehaviour.
679            assert!(client.reported().is_empty());
680        }
681    }
682
683    #[tokio::test]
684    async fn a_mismatched_request_id_exhausts_the_retry_budget() {
685        let all = slots(&[(key(1), 11)]);
686        let (root, _) = storage_root(&all, &[]);
687        let accounts = vec![(key(100), account(root))];
688
689        let client = rejecting_client(|peer| response(peer, 9, vec![wire_slots(&all)], Vec::new()));
690        let error = downloader(Arc::clone(&client), request(&accounts), &accounts)
691            .unwrap()
692            .await
693            .unwrap_err();
694
695        assert_eq!(error, RequestError::BadResponse);
696        // One penalty per attempt, then the budget is spent.
697        assert_eq!(client.reported().len(), usize::from(MAX_RETRIES) + 1);
698    }
699
700    #[tokio::test]
701    async fn a_range_proved_against_another_root_is_rejected() {
702        let all = slots(&[(key(1), 11)]);
703        let accounts = vec![(key(100), account(B256::repeat_byte(0xcc)))];
704
705        let client = rejecting_client(|peer| response(peer, 1, vec![wire_slots(&all)], Vec::new()));
706        let error = downloader(Arc::clone(&client), request(&accounts), &accounts)
707            .unwrap()
708            .await
709            .unwrap_err();
710
711        assert_eq!(error, RequestError::BadResponse);
712        assert_eq!(client.reported().len(), usize::from(MAX_RETRIES) + 1);
713    }
714
715    #[tokio::test]
716    async fn slots_that_are_not_strictly_ordered_penalize_the_peer_before_a_retry_succeeds() {
717        let all = slots(&[(key(1), 11), (key(2), 12)]);
718        let (root, _) = storage_root(&all, &[]);
719        let accounts = vec![(key(100), account(root))];
720        let bad_peer = PeerId::random();
721        let good_peer = PeerId::random();
722        let mut reversed = wire_slots(&all);
723        reversed.reverse();
724        let client = Arc::new(TestSnapClient::new([
725            response(bad_peer, 1, vec![reversed], Vec::new()),
726            response(good_peer, 1, vec![wire_slots(&all)], Vec::new()),
727        ]));
728
729        let outcome =
730            downloader(Arc::clone(&client), request(&accounts), &accounts).unwrap().await.unwrap();
731
732        let StorageRangeOutcome::Verified(verified) = outcome else { panic!("verified ranges") };
733        assert_eq!(verified.ranges[0].slots, all);
734        assert_eq!(*client.reported(), [bad_peer]);
735        assert_eq!(*client.priorities(), [Priority::Normal, Priority::High]);
736    }
737
738    #[test]
739    fn a_request_that_does_not_match_its_accounts_is_refused() {
740        let accounts = vec![(key(100), account(EMPTY_ROOT_HASH))];
741        let client = TestSnapClient::new([]);
742
743        let mut mismatched = request(&accounts);
744        mismatched.account_hashes = vec![key(999)];
745        let error = downloader(&client, mismatched, &accounts).unwrap_err();
746        assert_eq!(
747            error,
748            InvalidStorageRangeRequest::AccountMismatch {
749                index: 0,
750                requested: key(999),
751                supplied: key(100),
752            }
753        );
754
755        let mut empty = request(&accounts);
756        empty.account_hashes.clear();
757        assert_eq!(
758            downloader(&client, empty, &accounts).unwrap_err(),
759            InvalidStorageRangeRequest::NoAccounts
760        );
761
762        let mut reversed = request(&accounts);
763        reversed.starting_hash = key(9).into();
764        reversed.limit_hash = key(1).into();
765        assert_eq!(
766            downloader(&client, reversed, &accounts).unwrap_err(),
767            InvalidStorageRangeRequest::ReversedBounds { origin: key(9), limit: key(1) }
768        );
769    }
770
771    #[test]
772    fn a_request_for_another_state_root_is_refused() {
773        let accounts = vec![(key(100), account(EMPTY_ROOT_HASH))];
774        let client = TestSnapClient::new([]);
775
776        let mut other_root = request(&accounts);
777        other_root.root_hash = B256::repeat_byte(0xcc);
778        assert_eq!(
779            downloader(&client, other_root, &accounts).unwrap_err(),
780            InvalidStorageRangeRequest::StateRootMismatch {
781                requested: B256::repeat_byte(0xcc),
782                authenticated: STATE_ROOT,
783            }
784        );
785    }
786
787    #[tokio::test]
788    async fn a_trie_continuing_past_the_limit_needs_no_follow_up() {
789        let all = slots(&[(key(1), 11), (key(2), 12), (key(3), 13)]);
790        let (root, proof) = storage_root(&all, &[B256::ZERO, key(2)]);
791        let accounts = vec![(key(100), account(root))];
792        let client = Arc::new(TestSnapClient::new([response(
793            PeerId::random(),
794            1,
795            vec![wire_slots(&all[..2])],
796            proof,
797        )]));
798        let mut bounded = request(&accounts);
799        bounded.limit_hash = key(2).into();
800
801        let outcome = downloader(Arc::clone(&client), bounded, &accounts).unwrap().await.unwrap();
802
803        let StorageRangeOutcome::Verified(verified) = outcome else { panic!("verified ranges") };
804        let range = verified_range(&accounts);
805        assert_eq!(verified.follow_up(2, range.batch()).unwrap(), None);
806        assert_eq!(verified.into_ranges()[0].slots, all[..2]);
807    }
808
809    #[tokio::test]
810    async fn a_follow_up_keeps_the_limit_only_at_the_first_account() {
811        let all = slots(&[(key(1), 11), (key(2), 12), (key(3), 13)]);
812        let (root, proof) = storage_root(&all, &[B256::ZERO, key(1)]);
813        let accounts = vec![(key(100), account(root))];
814        let client = Arc::new(TestSnapClient::new([response(
815            PeerId::random(),
816            1,
817            vec![wire_slots(&all[..1])],
818            proof,
819        )]));
820        let mut bounded = request(&accounts);
821        bounded.limit_hash = key(5).into();
822
823        let outcome =
824            downloader(Arc::clone(&client), bounded.clone(), &accounts).unwrap().await.unwrap();
825
826        let StorageRangeOutcome::Verified(verified) = outcome else { panic!("verified ranges") };
827        let range = verified_range(&accounts);
828        let (follow_up, narrowed) = verified.follow_up(2, range.batch()).unwrap().unwrap();
829        assert_eq!(narrowed.accounts(), account_refs(&accounts));
830        assert_eq!(follow_up.request_id, 2);
831        assert_eq!(follow_up.root_hash, bounded.root_hash);
832        assert_eq!(follow_up.account_hashes, bounded.account_hashes);
833        assert_eq!(follow_up.starting_hash, key(2).into());
834        assert_eq!(follow_up.limit_hash, key(5).into());
835    }
836
837    // Every account after the first is authenticated as a whole trie, so its follow-up must drop
838    // the bounds the first account was requested with.
839    #[tokio::test]
840    async fn a_follow_up_past_the_first_account_is_unbounded() {
841        let first = slots(&[(key(1), 11)]);
842        let second = slots(&[(key(1), 21), (key(2), 22)]);
843        let (first_root, _) = storage_root(&first, &[]);
844        let (second_root, proof) = storage_root(&second, &[B256::ZERO, key(1)]);
845        let accounts = vec![(key(100), account(first_root)), (key(200), account(second_root))];
846        let client = Arc::new(TestSnapClient::new([response(
847            PeerId::random(),
848            1,
849            vec![wire_slots(&first), wire_slots(&second[..1])],
850            proof,
851        )]));
852
853        let outcome =
854            downloader(Arc::clone(&client), request(&accounts), &accounts).unwrap().await.unwrap();
855
856        let StorageRangeOutcome::Verified(verified) = outcome else { panic!("verified ranges") };
857        assert_eq!(
858            verified.continuation,
859            Some(StorageRangeContinuation::Partial {
860                account_index: 1,
861                account_hash: key(200),
862                starting_hash: key(2),
863            })
864        );
865
866        let range = verified_range(&accounts);
867        let (follow_up, narrowed) = verified.follow_up(2, range.batch()).unwrap().unwrap();
868        assert_eq!(narrowed.accounts(), account_refs(&accounts[1..]));
869        assert_eq!(follow_up.account_hashes, vec![key(200)]);
870        assert_eq!(follow_up.starting_hash, key(2).into());
871        assert_eq!(follow_up.limit_hash, RangeBound::default());
872    }
873
874    // Each follow-up must resume strictly past the last one, so the loop terminates.
875    #[tokio::test]
876    async fn resuming_a_partial_range_advances_until_the_trie_is_exhausted() {
877        let all = slots(&[(key(1), 11), (key(2), 12), (key(3), 13)]);
878        let (root, _) = storage_root(&all, &[]);
879        let accounts = vec![(key(100), account(root))];
880        let range = verified_range(&accounts);
881        let mut request = request(&accounts);
882        let mut origins = Vec::new();
883        let mut collected = Vec::new();
884
885        for served in 0..all.len() {
886            let origin = request.starting_hash.unwrap_or(B256::ZERO);
887            origins.push(origin);
888            let (_, proof) = storage_root(&all, &[origin, all[served].0]);
889            let client = Arc::new(TestSnapClient::new([response(
890                PeerId::random(),
891                request.request_id,
892                vec![wire_slots(&all[served..=served])],
893                proof,
894            )]));
895
896            let outcome =
897                downloader(Arc::clone(&client), request.clone(), &accounts).unwrap().await.unwrap();
898            let StorageRangeOutcome::Verified(verified) = outcome else {
899                panic!("verified ranges")
900            };
901            collected.extend(verified.ranges[0].slots.clone());
902
903            let Some((follow_up, _)) =
904                verified.follow_up(request.request_id + 1, range.batch()).unwrap()
905            else {
906                assert_eq!(served, all.len() - 1);
907                break
908            };
909            request = follow_up;
910        }
911
912        assert_eq!(collected, all);
913        assert_eq!(origins, vec![B256::ZERO, key(2), key(3)]);
914    }
915
916    // A non-zero origin makes the first account unambiguously partial, so servers stop there and
917    // the shared proof can still be assigned to it.
918    #[test]
919    fn a_zero_origin_limited_request_must_not_carry_several_accounts() {
920        let accounts = vec![
921            (key(100), account(B256::repeat_byte(0xaa))),
922            (key(200), account(B256::repeat_byte(0xbb))),
923        ];
924        // Accepted requests are submitted on construction, so the client must have replies ready.
925        let client =
926            TestSnapClient::new([response(PeerId::random(), 1, vec![Vec::new()], Vec::new())]);
927
928        let mut bounded = request(&accounts);
929        bounded.limit_hash = key(5).into();
930        assert_eq!(
931            downloader(&client, bounded.clone(), &accounts).unwrap_err(),
932            InvalidStorageRangeRequest::LimitedMultipleAccounts { accounts: 2 }
933        );
934
935        bounded.starting_hash = key(1).into();
936        assert!(downloader(&client, bounded, &accounts).is_ok());
937    }
938
939    // Consecutive account transitions must chain: each follow-up is driven by the batch the
940    // previous one returned, never by accounts the caller re-derived.
941    #[tokio::test]
942    async fn consecutive_account_transitions_carry_their_batch_forward() {
943        let served = slots(&[(key(1), 11)]);
944        let (root, _) = storage_root(&served, &[]);
945        let accounts =
946            vec![(key(100), account(root)), (key(200), account(root)), (key(300), account(root))];
947        let range = verified_range(&accounts);
948        let mut request = request(&accounts);
949        let mut batch = range.batch();
950
951        for remaining in (1..accounts.len()).rev() {
952            let client = Arc::new(TestSnapClient::new([response(
953                PeerId::random(),
954                request.request_id,
955                vec![wire_slots(&served)],
956                Vec::new(),
957            )]));
958            let outcome = StorageRangeDownloader::new(
959                Arc::clone(&client),
960                request.clone(),
961                &batch,
962                Runtime::test(),
963            )
964            .unwrap()
965            .await
966            .unwrap();
967
968            let StorageRangeOutcome::Verified(verified) = outcome else {
969                panic!("verified ranges")
970            };
971            let (follow_up, narrowed) =
972                verified.follow_up(request.request_id + 1, batch).unwrap().unwrap();
973            assert_eq!(follow_up.account_hashes.len(), remaining);
974            assert_eq!(narrowed.accounts(), account_refs(&accounts[accounts.len() - remaining..]));
975            request = follow_up;
976            batch = narrowed;
977        }
978
979        assert_eq!(request.account_hashes, vec![key(300)]);
980    }
981
982    #[tokio::test]
983    async fn a_follow_up_refuses_a_batch_from_another_request() {
984        let served = slots(&[(key(1), 11)]);
985        let (root, _) = storage_root(&served, &[]);
986        let accounts = vec![(key(100), account(root)), (key(200), account(root))];
987        let client = Arc::new(TestSnapClient::new([response(
988            PeerId::random(),
989            1,
990            vec![wire_slots(&served)],
991            Vec::new(),
992        )]));
993
994        let outcome =
995            downloader(Arc::clone(&client), request(&accounts), &accounts).unwrap().await.unwrap();
996        let StorageRangeOutcome::Verified(verified) = outcome else { panic!("verified ranges") };
997
998        let others = vec![(key(900), account(root)), (key(901), account(root))];
999        let other_range = verified_range(&others);
1000        assert_eq!(
1001            verified.follow_up(2, other_range.batch()).unwrap_err(),
1002            InvalidStorageRangeRequest::AccountMismatch {
1003                index: 0,
1004                requested: key(100),
1005                supplied: key(900),
1006            }
1007        );
1008    }
1009}