Skip to main content

reth_downloaders/snap/
block_access_list.rs

1//! Downloads block access lists and authenticates them against header commitments.
2//!
3//! Responses are positional: entry `i` answers the `i`th requested block hash, and is accepted
4//! only if it hashes to the commitment carried by that block's header, as defined by
5//! [EIP-8189](https://eips.ethereum.org/EIPS/eip-8189).
6
7use super::request::{SnapVerifier, VerifyingRequest};
8use alloy_consensus::BlockHeader;
9use alloy_eips::eip7928::bal::{DecodedBal, RawBal};
10use alloy_primitives::{Bytes, Sealable, B256};
11use futures::Future;
12use reth_eth_wire_types::snap::{BlockAccessListsMessage, GetBlockAccessListsMessage};
13use reth_network_p2p::{
14    error::RequestError,
15    snap::client::{SnapClient, SnapResponse},
16};
17use reth_network_peers::PeerId;
18use reth_primitives_traits::SealedHeader;
19use reth_tasks::Runtime;
20use std::{
21    pin::Pin,
22    task::{Context, Poll},
23};
24use tracing::debug;
25
26/// Downloads block access lists and authenticates each against its header commitment.
27///
28/// Invalid responses penalize their peer and retry. Decoding and hashing run on the blocking
29/// pool.
30#[derive(Debug)]
31pub struct BlockAccessListDownloader<C: SnapClient>(VerifyingRequest<C, BlockAccessListVerifier>);
32
33impl<C: SnapClient> BlockAccessListDownloader<C> {
34    /// Creates a downloader that verifies responses against `headers`.
35    ///
36    /// Headers must match the requested block hashes in order and carry their block-access-list
37    /// commitments.
38    pub fn new<H: BlockHeader + Sealable>(
39        client: C,
40        request: GetBlockAccessListsMessage,
41        headers: &[SealedHeader<H>],
42        runtime: Runtime,
43    ) -> Result<Self, InvalidBlockAccessListRequest> {
44        if request.block_hashes.is_empty() {
45            return Err(InvalidBlockAccessListRequest::NoBlocks)
46        }
47        if request.block_hashes.len() != headers.len() {
48            return Err(InvalidBlockAccessListRequest::HeaderCount {
49                requested: request.block_hashes.len(),
50                supplied: headers.len(),
51            })
52        }
53
54        // Only authenticated block identities cross into blocking work, so the verifier stays
55        // free of the caller's header type.
56        let mut blocks = Vec::with_capacity(headers.len());
57        for (index, (requested, header)) in request.block_hashes.iter().zip(headers).enumerate() {
58            if *requested != header.hash() {
59                return Err(InvalidBlockAccessListRequest::HashMismatch {
60                    index,
61                    requested: *requested,
62                    supplied: header.hash(),
63                })
64            }
65            let Some(commitment) = header.block_access_list_hash() else {
66                return Err(InvalidBlockAccessListRequest::MissingCommitment {
67                    index,
68                    block_hash: *requested,
69                })
70            };
71            blocks.push((*requested, commitment));
72        }
73
74        let verifier = BlockAccessListVerifier {
75            request_id: request.request_id,
76            response_bytes: request.response_bytes,
77            blocks,
78        };
79        Ok(Self(VerifyingRequest::new(client, request, verifier, runtime)))
80    }
81}
82
83impl<C> Future for BlockAccessListDownloader<C>
84where
85    C: SnapClient + Unpin,
86{
87    type Output = Result<BlockAccessListOutcome, 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 block-access-lists request.
95#[derive(Clone, Debug, PartialEq, Eq)]
96pub enum BlockAccessListOutcome {
97    /// The peer serves snap but holds none of the requested lists, and was not penalized.
98    Unavailable {
99        /// The peer that answered.
100        peer_id: PeerId,
101    },
102    /// Lists authenticated against their header commitments.
103    Verified(VerifiedBlockAccessLists),
104}
105
106/// Positional block access lists authenticated against their header commitments.
107#[derive(Clone, Debug, PartialEq, Eq)]
108pub struct VerifiedBlockAccessLists {
109    // Needed to avoid retrying unavailable entries against the peer that omitted them.
110    peer_id: PeerId,
111    // Each list stays bound to the block whose header commitment authenticated it.
112    block_access_lists: Vec<(B256, Option<DecodedBal>)>,
113    // Requested blocks the response left unanswered, in request order.
114    missing: Vec<B256>,
115    // Soft byte limit of the answered request, so a follow-up is bounded the same way.
116    response_bytes: u64,
117}
118
119impl VerifiedBlockAccessLists {
120    /// Peer that returned these lists.
121    pub const fn peer_id(&self) -> PeerId {
122        self.peer_id
123    }
124
125    /// Block hashes and lists in requested order, with `None` where the peer had none.
126    pub fn block_access_lists(&self) -> &[(B256, Option<DecodedBal>)] {
127        &self.block_access_lists
128    }
129
130    /// Consumes the result without separating lists from their authenticated block hashes.
131    pub fn into_block_access_lists(self) -> Vec<(B256, Option<DecodedBal>)> {
132        self.block_access_lists
133    }
134
135    /// Request the blocks left unanswered, or `None` once every list is authenticated.
136    /// The returned request contains only unanswered hashes, preserving authenticated lists.
137    pub fn follow_up(&self, request_id: u64) -> Option<GetBlockAccessListsMessage> {
138        (!self.missing.is_empty()).then(|| GetBlockAccessListsMessage {
139            request_id,
140            block_hashes: self.missing.clone(),
141            response_bytes: self.response_bytes,
142        })
143    }
144
145    /// Requested blocks this response left unanswered, in request order.
146    /// This includes in-place omissions and any truncated suffix.
147    pub fn missing(&self) -> &[B256] {
148        &self.missing
149    }
150}
151
152/// A block-access-lists request that cannot be authenticated by the headers supplied with it.
153#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
154pub enum InvalidBlockAccessListRequest {
155    /// The request asks for no blocks.
156    #[error("block access list request has no block hashes")]
157    NoBlocks,
158    /// A different number of headers was supplied than blocks requested.
159    #[error("requested {requested} block access lists but supplied {supplied} headers")]
160    HeaderCount {
161        /// Blocks the request asks for.
162        requested: usize,
163        /// Headers supplied to authenticate them.
164        supplied: usize,
165    },
166    /// A requested hash does not match the header at the same position.
167    #[error("requested block {requested} at index {index} but header is for {supplied}")]
168    HashMismatch {
169        /// Position the mismatch was found at.
170        index: usize,
171        /// Hash the request asks for.
172        requested: B256,
173        /// Hash of the header supplied for it.
174        supplied: B256,
175    },
176    /// A supplied header carries no commitment, so its list could never be authenticated.
177    #[error("header for block {block_hash} at index {index} has no block access list commitment")]
178    MissingCommitment {
179        /// Position of the header without a commitment.
180        index: usize,
181        /// Block the header belongs to.
182        block_hash: B256,
183    },
184}
185
186// Authenticates each returned list against the commitment of the block it answers.
187//
188// Keeps only response identity and authenticated block pairs, avoiding the caller's generic
189// header type on the blocking pool.
190#[derive(Clone, Debug)]
191struct BlockAccessListVerifier {
192    // Matches the response to the request that asked for it.
193    request_id: u64,
194    // Soft byte limit the request was sent with, carried into any follow-up.
195    response_bytes: u64,
196    // Requested hashes paired with their header commitments, in wire order.
197    blocks: Vec<(B256, B256)>,
198}
199
200impl BlockAccessListVerifier {
201    // Checks that the response can be paired with this request before its entries are decoded.
202    fn validate_response(
203        &self,
204        response: SnapResponse,
205    ) -> Result<BlockAccessListsMessage, RequestError> {
206        let SnapResponse::BlockAccessLists(response) = response else {
207            debug!(target: "downloaders::snap", "Expected block access lists response");
208            return Err(RequestError::BadResponse)
209        };
210        if response.request_id != self.request_id {
211            debug!(
212                target: "downloaders::snap",
213                expected = self.request_id,
214                got = response.request_id,
215                "Block access lists response id mismatch"
216            );
217            return Err(RequestError::BadResponse)
218        }
219        if response.block_access_lists.0.len() > self.blocks.len() {
220            debug!(
221                target: "downloaders::snap",
222                requested = self.blocks.len(),
223                got = response.block_access_lists.0.len(),
224                "Block access lists response is longer than the request"
225            );
226            return Err(RequestError::BadResponse)
227        }
228
229        Ok(response)
230    }
231
232    // Keeps each decoded entry tied to the commitment at the same request position.
233    fn authenticate_entries(
234        &self,
235        entries: Vec<Option<Bytes>>,
236    ) -> Result<Vec<(B256, Option<DecodedBal>)>, RequestError> {
237        let mut block_access_lists = Vec::with_capacity(entries.len());
238        for (index, entry) in entries.into_iter().enumerate() {
239            let (block_hash, commitment) = self.blocks[index];
240            // An omitted list stays in place, so every later entry keeps the commitment it
241            // answers.
242            let Some(raw) = entry else {
243                block_access_lists.push((block_hash, None));
244                continue
245            };
246            // Hashing the raw bytes settles authenticity without decoding, so a peer cannot
247            // charge us the decode of a list it was never able to serve.
248            let raw = RawBal::new(raw);
249            raw.ensure_hash(commitment).map_err(|error| {
250                debug!(
251                    target: "downloaders::snap",
252                    %block_hash,
253                    expected = %error.expected,
254                    got = %error.computed,
255                    "Block access list does not match its header commitment"
256                );
257                RequestError::BadResponse
258            })?;
259            let decoded = DecodedBal::from_raw_bal(raw).map_err(|error| {
260                debug!(target: "downloaders::snap", %block_hash, %error, "Invalid block access list");
261                RequestError::BadResponse
262            })?;
263            block_access_lists.push((block_hash, Some(decoded)));
264        }
265
266        Ok(block_access_lists)
267    }
268}
269
270impl SnapVerifier for BlockAccessListVerifier {
271    type Request = GetBlockAccessListsMessage;
272    type Output = BlockAccessListOutcome;
273
274    // Validates the response identity and authenticates every supplied block access list.
275    fn verify(self, peer_id: PeerId, response: SnapResponse) -> Result<Self::Output, RequestError> {
276        let entries = self.validate_response(response)?.block_access_lists.0;
277        // An empty response is the peer's explicit statement that it has none of these lists.
278        if entries.is_empty() {
279            return Ok(BlockAccessListOutcome::Unavailable { peer_id })
280        }
281
282        let block_access_lists = self.authenticate_entries(entries)?;
283        // Omitted entries and a cut at the peer's soft byte limit both leave blocks unanswered,
284        // and neither invalidates the entries around them.
285        let missing = block_access_lists
286            .iter()
287            .filter_map(|(block_hash, list)| list.is_none().then_some(*block_hash))
288            .chain(
289                self.blocks[block_access_lists.len()..].iter().map(|(block_hash, _)| *block_hash),
290            )
291            .collect::<Vec<_>>();
292        // A full response of omitted entries is unavailable, a shorter one can be byte-limited.
293        // Preserve the latter's omitted suffix for resumption.
294        if block_access_lists.len() == self.blocks.len() && missing.len() == self.blocks.len() {
295            return Ok(BlockAccessListOutcome::Unavailable { peer_id })
296        }
297
298        Ok(BlockAccessListOutcome::Verified(VerifiedBlockAccessLists {
299            peer_id,
300            block_access_lists,
301            missing,
302            response_bytes: self.response_bytes,
303        }))
304    }
305}
306
307#[cfg(test)]
308mod tests {
309    use super::{
310        super::{request::MAX_RETRIES, test_utils::TestSnapClient},
311        *,
312    };
313    use alloy_consensus::Header;
314    use alloy_eips::eip7928::bal::Bal;
315    use alloy_primitives::Bytes;
316    use reth_eth_wire_types::{
317        snap::{AccountRangeMessage, BlockAccessListsMessage, ByteCodesMessage},
318        BlockAccessLists,
319    };
320    use reth_network_p2p::{error::PeerRequestResult, priority::Priority};
321    use reth_network_peers::WithPeerId;
322    use std::sync::Arc;
323
324    fn bal() -> Bytes {
325        Bytes::from(alloy_rlp::encode(Bal::default()))
326    }
327
328    fn commitment(raw: Bytes) -> B256 {
329        DecodedBal::from_rlp_bytes(raw).expect("test bal decodes").hash()
330    }
331
332    // Headers committing to `entries`, sealed with distinct block hashes. A `None` entry gets a
333    // commitment no list can match, since nothing authenticates against it.
334    fn headers(entries: &[Option<Bytes>]) -> Vec<SealedHeader<Header>> {
335        entries
336            .iter()
337            .enumerate()
338            .map(|(index, raw)| {
339                let header = Header {
340                    block_access_list_hash: Some(
341                        raw.clone().map_or(B256::repeat_byte(0xee), commitment),
342                    ),
343                    ..Default::default()
344                };
345                SealedHeader::new(header, B256::repeat_byte(index as u8 + 1))
346            })
347            .collect()
348    }
349
350    fn request(headers: &[SealedHeader<Header>]) -> GetBlockAccessListsMessage {
351        GetBlockAccessListsMessage {
352            request_id: 1,
353            block_hashes: headers.iter().map(SealedHeader::hash).collect(),
354            response_bytes: 512 * 1024,
355        }
356    }
357
358    fn response(
359        peer: PeerId,
360        request_id: u64,
361        entries: Vec<Option<Bytes>>,
362    ) -> PeerRequestResult<SnapResponse> {
363        Ok(WithPeerId::new(
364            peer,
365            SnapResponse::BlockAccessLists(BlockAccessListsMessage {
366                request_id,
367                block_access_lists: BlockAccessLists(entries),
368            }),
369        ))
370    }
371
372    // A response that can never authenticate a block access list.
373    fn unverifiable(peer: PeerId) -> PeerRequestResult<SnapResponse> {
374        Ok(WithPeerId::new(
375            peer,
376            SnapResponse::ByteCodes(ByteCodesMessage { request_id: 1, codes: Vec::new() }),
377        ))
378    }
379
380    // Every attempt gets the same answer, so a rejected response exhausts the retry budget.
381    fn always(
382        peer: PeerId,
383        request_id: u64,
384        entries: Vec<Option<Bytes>>,
385    ) -> impl Iterator<Item = PeerRequestResult<SnapResponse>> {
386        std::iter::repeat_with(move || response(peer, request_id, entries.clone()))
387            .take(usize::from(MAX_RETRIES) + 1)
388    }
389
390    fn downloader(
391        client: Arc<TestSnapClient>,
392        request: GetBlockAccessListsMessage,
393        headers: &[SealedHeader<Header>],
394    ) -> Result<BlockAccessListDownloader<Arc<TestSnapClient>>, InvalidBlockAccessListRequest> {
395        BlockAccessListDownloader::new(client, request, headers, Runtime::test())
396    }
397
398    fn verified(outcome: BlockAccessListOutcome) -> VerifiedBlockAccessLists {
399        match outcome {
400            BlockAccessListOutcome::Verified(verified) => verified,
401            BlockAccessListOutcome::Unavailable { .. } => panic!("expected verified lists"),
402        }
403    }
404
405    #[tokio::test]
406    async fn present_and_omitted_entries_keep_their_positions() {
407        let entries = vec![Some(bal()), None, Some(bal())];
408        let headers = headers(&entries);
409        let peer = PeerId::random();
410        let client = Arc::new(TestSnapClient::new([response(peer, 1, entries)]));
411
412        let outcome =
413            downloader(Arc::clone(&client), request(&headers), &headers).unwrap().await.unwrap();
414
415        let verified = verified(outcome);
416        assert_eq!(verified.peer_id(), peer);
417        assert_eq!(
418            verified
419                .block_access_lists()
420                .iter()
421                .map(|(block_hash, entry)| (*block_hash, entry.as_ref().map(DecodedBal::hash)))
422                .collect::<Vec<_>>(),
423            [
424                (headers[0].hash(), Some(commitment(bal()))),
425                (headers[1].hash(), None),
426                (headers[2].hash(), Some(commitment(bal()))),
427            ]
428        );
429        // The list after the omission stays authenticated, so only the gap is asked for again.
430        assert_eq!(verified.missing(), [headers[1].hash()]);
431        assert_eq!(
432            verified.follow_up(2),
433            Some(GetBlockAccessListsMessage {
434                request_id: 2,
435                block_hashes: vec![headers[1].hash()],
436                response_bytes: request(&headers).response_bytes,
437            })
438        );
439        assert!(client.reported().is_empty());
440    }
441
442    #[tokio::test]
443    async fn a_complete_response_needs_no_follow_up() {
444        let entries = vec![Some(bal()), Some(bal())];
445        let headers = headers(&entries);
446        let client = Arc::new(TestSnapClient::new([response(PeerId::random(), 1, entries)]));
447
448        let outcome =
449            downloader(Arc::clone(&client), request(&headers), &headers).unwrap().await.unwrap();
450
451        let verified = verified(outcome);
452        assert!(verified.missing().is_empty());
453        assert_eq!(verified.follow_up(2), None);
454    }
455
456    #[tokio::test]
457    async fn a_truncated_response_leaves_the_rest_of_the_request_unanswered() {
458        let entries = vec![Some(bal()), Some(bal()), Some(bal())];
459        let headers = headers(&entries);
460        let client =
461            Arc::new(TestSnapClient::new([response(PeerId::random(), 1, entries[..2].to_vec())]));
462
463        let outcome =
464            downloader(Arc::clone(&client), request(&headers), &headers).unwrap().await.unwrap();
465
466        let verified = verified(outcome);
467        assert_eq!(verified.missing(), [headers[2].hash()]);
468        assert_eq!(verified.block_access_lists().len(), 2);
469        assert_eq!(verified.block_access_lists()[0].0, headers[0].hash());
470        assert_eq!(verified.block_access_lists()[1].0, headers[1].hash());
471        assert!(client.reported().is_empty());
472    }
473
474    #[tokio::test]
475    async fn an_empty_or_complete_response_holding_no_lists_is_unavailable() {
476        let headers = headers(&[Some(bal()), Some(bal())]);
477        let peer = PeerId::random();
478        for entries in [Vec::new(), vec![None, None]] {
479            let client = Arc::new(TestSnapClient::new([response(peer, 1, entries)]));
480
481            let outcome = downloader(Arc::clone(&client), request(&headers), &headers)
482                .unwrap()
483                .await
484                .unwrap();
485
486            assert_eq!(outcome, BlockAccessListOutcome::Unavailable { peer_id: peer });
487            assert!(client.reported().is_empty());
488        }
489    }
490
491    #[tokio::test]
492    async fn a_truncated_response_after_an_omission_stays_resumable() {
493        let headers = headers(&[Some(bal()), Some(bal())]);
494        let peer = PeerId::random();
495        let client = Arc::new(TestSnapClient::new([response(peer, 1, vec![None])]));
496
497        let outcome =
498            downloader(Arc::clone(&client), request(&headers), &headers).unwrap().await.unwrap();
499
500        let verified = verified(outcome);
501        assert_eq!(verified.missing(), [headers[0].hash(), headers[1].hash()]);
502        assert_eq!(
503            verified.follow_up(2),
504            Some(GetBlockAccessListsMessage {
505                request_id: 2,
506                block_hashes: headers.iter().map(SealedHeader::hash).collect(),
507                response_bytes: request(&headers).response_bytes,
508            })
509        );
510        assert!(client.reported().is_empty());
511    }
512
513    #[tokio::test]
514    async fn an_invalid_list_is_reported_and_retried() {
515        let entries = vec![Some(bal())];
516        let headers = headers(&entries);
517        let bad_peer = PeerId::random();
518        let client = Arc::new(TestSnapClient::new([
519            response(bad_peer, 1, vec![Some(Bytes::from_static(&[0xff, 0xff]))]),
520            response(PeerId::random(), 1, entries),
521        ]));
522
523        let outcome =
524            downloader(Arc::clone(&client), request(&headers), &headers).unwrap().await.unwrap();
525
526        assert_eq!(verified(outcome).block_access_lists().len(), 1);
527        assert_eq!(*client.reported(), [bad_peer]);
528        assert_eq!(*client.priorities(), [Priority::Normal, Priority::High]);
529    }
530
531    #[tokio::test]
532    async fn a_list_that_misses_its_header_commitment_is_rejected() {
533        // The header commits to something this list cannot hash to.
534        let headers = vec![SealedHeader::new(
535            Header { block_access_list_hash: Some(B256::repeat_byte(0xab)), ..Default::default() },
536            B256::repeat_byte(1),
537        )];
538        let peer = PeerId::random();
539        let client = Arc::new(TestSnapClient::new(always(peer, 1, vec![Some(bal())])));
540
541        let error = downloader(Arc::clone(&client), request(&headers), &headers)
542            .unwrap()
543            .await
544            .unwrap_err();
545
546        assert_eq!(error, RequestError::BadResponse);
547        assert_eq!(client.reported().len(), usize::from(MAX_RETRIES) + 1);
548    }
549
550    #[tokio::test]
551    async fn dropping_an_entry_instead_of_omitting_it_in_place_is_rejected() {
552        // The peer holds no list for the first block and drops the slot rather than sending
553        // `None`, so the second block's list lands on the first block's commitment.
554        let headers = headers(&[None, Some(bal())]);
555        let peer = PeerId::random();
556        let client = Arc::new(TestSnapClient::new(always(peer, 1, vec![Some(bal())])));
557
558        let error = downloader(Arc::clone(&client), request(&headers), &headers)
559            .unwrap()
560            .await
561            .unwrap_err();
562
563        assert_eq!(error, RequestError::BadResponse);
564        assert_eq!(client.reported().len(), usize::from(MAX_RETRIES) + 1);
565    }
566
567    #[tokio::test]
568    async fn malformed_wrong_id_and_oversized_responses_are_rejected() {
569        let headers = headers(&[Some(bal())]);
570        let peer = PeerId::random();
571        for (request_id, entries) in [
572            // undecodable payload
573            (1, vec![Some(Bytes::from_static(&[0xff, 0xff]))]),
574            // answers a different request
575            (7, vec![Some(bal())]),
576            // more entries than blocks requested
577            (1, vec![Some(bal()), Some(bal())]),
578        ] {
579            let client = Arc::new(TestSnapClient::new(always(peer, request_id, entries)));
580
581            let error = downloader(Arc::clone(&client), request(&headers), &headers)
582                .unwrap()
583                .await
584                .unwrap_err();
585
586            assert_eq!(error, RequestError::BadResponse);
587            assert_eq!(client.reported().len(), usize::from(MAX_RETRIES) + 1);
588        }
589    }
590
591    #[tokio::test]
592    async fn a_wrong_response_type_exhausts_the_retry_budget() {
593        let headers = headers(&[Some(bal())]);
594        let peers = [PeerId::random(), PeerId::random(), PeerId::random()];
595        let client = Arc::new(TestSnapClient::new(peers.map(unverifiable)));
596
597        let error = downloader(Arc::clone(&client), request(&headers), &headers)
598            .unwrap()
599            .await
600            .unwrap_err();
601
602        assert_eq!(error, RequestError::BadResponse);
603        assert_eq!(*client.reported(), peers);
604        assert_eq!(*client.priorities(), [Priority::Normal, Priority::High, Priority::High]);
605    }
606
607    #[test]
608    fn requests_that_cannot_be_authenticated_are_rejected_before_submission() {
609        let headers = headers(&[Some(bal())]);
610        let client = Arc::new(TestSnapClient::new(std::iter::empty()));
611
612        let mut empty = request(&headers);
613        empty.block_hashes.clear();
614        assert_eq!(
615            downloader(Arc::clone(&client), empty, &[]).unwrap_err(),
616            InvalidBlockAccessListRequest::NoBlocks
617        );
618
619        assert!(matches!(
620            downloader(Arc::clone(&client), request(&headers), &[]).unwrap_err(),
621            InvalidBlockAccessListRequest::HeaderCount { requested: 1, supplied: 0 }
622        ));
623
624        let mut wrong_block = request(&headers);
625        wrong_block.block_hashes[0] = B256::repeat_byte(0x99);
626        assert!(matches!(
627            downloader(Arc::clone(&client), wrong_block, &headers).unwrap_err(),
628            InvalidBlockAccessListRequest::HashMismatch { index: 0, .. }
629        ));
630
631        let uncommitted = vec![SealedHeader::new(Header::default(), headers[0].hash())];
632        assert!(matches!(
633            downloader(Arc::clone(&client), request(&headers), &uncommitted).unwrap_err(),
634            InvalidBlockAccessListRequest::MissingCommitment { index: 0, .. }
635        ));
636
637        // Nothing reached the network.
638        assert!(client.priorities().is_empty());
639    }
640
641    #[test]
642    fn a_response_of_another_kind_is_rejected() {
643        let verifier = BlockAccessListVerifier {
644            request_id: 1,
645            response_bytes: 512 * 1024,
646            blocks: vec![(B256::repeat_byte(1), commitment(bal()))],
647        };
648        let wrong = SnapResponse::AccountRange(AccountRangeMessage {
649            request_id: 1,
650            accounts: Vec::new(),
651            proof: Vec::new(),
652        });
653
654        assert_eq!(verifier.verify(PeerId::random(), wrong), Err(RequestError::BadResponse));
655    }
656}