Skip to main content

reth_downloaders/snap/
block_access_list.rs

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