Skip to main content

reth_downloaders/snap/
bytecode.rs

1//! Downloads contract bytecode and authenticates it by hash.
2//!
3//! A peer may omit code it does not have, so a response is an ordered subsequence of the
4//! requested hashes, as defined by
5//! [snap](https://github.com/ethereum/devp2p/blob/master/caps/snap.md#bytecodes-0x05).
6
7use super::request::{SnapVerifier, VerifyingRequest};
8use alloy_primitives::{keccak256, Bytes, B256};
9use futures::Future;
10use reth_eth_wire_types::snap::GetByteCodesMessage;
11use reth_network_p2p::{
12    error::RequestError,
13    snap::client::{SnapClient, SnapResponse},
14};
15use reth_network_peers::PeerId;
16use reth_tasks::Runtime;
17use std::{
18    pin::Pin,
19    task::{Context, Poll},
20};
21use tracing::debug;
22
23/// Downloads contract bytecode and authenticates each blob against a requested hash.
24///
25/// Invalid responses penalize their peer and retry. Hashing runs on the blocking pool.
26#[derive(Debug)]
27pub struct BytecodeDownloader<C: SnapClient>(VerifyingRequest<C, BytecodeVerifier>);
28
29impl<C: SnapClient> BytecodeDownloader<C> {
30    /// Submits `request`, rejecting one that asks for no code.
31    pub fn new(
32        client: C,
33        request: GetByteCodesMessage,
34        runtime: Runtime,
35    ) -> Result<Self, InvalidBytecodeRequest> {
36        if request.hashes.is_empty() {
37            return Err(InvalidBytecodeRequest::NoHashes)
38        }
39
40        let verifier =
41            BytecodeVerifier { request_id: request.request_id, hashes: request.hashes.clone() };
42        Ok(Self(VerifyingRequest::new(client, request, verifier, runtime)))
43    }
44}
45
46impl<C> Future for BytecodeDownloader<C>
47where
48    C: SnapClient + Unpin,
49{
50    type Output = Result<BytecodeOutcome, RequestError>;
51
52    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
53        self.get_mut().0.poll_verified(cx)
54    }
55}
56
57/// Result of an authenticated bytecode request.
58#[derive(Clone, Debug, PartialEq, Eq)]
59pub enum BytecodeOutcome {
60    /// The peer holds none of the requested code, and was not penalized.
61    Unavailable {
62        /// The peer that answered.
63        peer_id: PeerId,
64    },
65    /// Code authenticated against the requested hashes.
66    Verified(VerifiedBytecode),
67}
68
69/// Contract code authenticated against the hashes that were requested for it.
70#[derive(Clone, Debug, PartialEq, Eq)]
71pub struct VerifiedBytecode {
72    // Identifies the peer that omitted any missing code.
73    peer_id: PeerId,
74    // Code paired with the requested hash it answers, in requested order. Private so a blob
75    // cannot be relabelled with a hash that did not authenticate it.
76    codes: Vec<(B256, Bytes)>,
77}
78
79impl VerifiedBytecode {
80    /// Peer that returned this code.
81    pub const fn peer_id(&self) -> PeerId {
82        self.peer_id
83    }
84
85    /// Code with the hash it authenticated against, in requested order.
86    pub fn codes(&self) -> &[(B256, Bytes)] {
87        &self.codes
88    }
89
90    /// Consumes the result and returns the authenticated code.
91    pub fn into_codes(self) -> Vec<(B256, Bytes)> {
92        self.codes
93    }
94
95    /// Requested hashes the response did not answer, in requested order.
96    ///
97    /// Omissions are not a fault: a peer serves what it has, and cuts the response at its own
98    /// soft byte limit. These hashes can be asked for again.
99    pub fn missing<'a>(&'a self, requested: &'a [B256]) -> impl Iterator<Item = B256> + 'a {
100        requested.iter().copied().filter(|hash| !self.codes.iter().any(|(got, _)| got == hash))
101    }
102}
103
104/// A bytecode request that can never be answered.
105#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
106pub enum InvalidBytecodeRequest {
107    /// The request asks for no code hashes.
108    #[error("bytecode request has no code hashes")]
109    NoHashes,
110}
111
112// Authenticates returned code by hashing it against the requested hashes.
113//
114// Holds only what blocking verification needs, so nothing else crosses onto the blocking pool.
115#[derive(Clone, Debug)]
116struct BytecodeVerifier {
117    // Matches the response to the request that asked for it.
118    request_id: u64,
119    // Requested hashes, in the order a response must follow.
120    hashes: Vec<B256>,
121}
122
123impl SnapVerifier for BytecodeVerifier {
124    type Request = GetByteCodesMessage;
125    type Output = BytecodeOutcome;
126
127    fn verify(self, peer_id: PeerId, response: SnapResponse) -> Result<Self::Output, RequestError> {
128        let SnapResponse::ByteCodes(response) = response else {
129            debug!(target: "downloaders::snap", "Expected byte codes response");
130            return Err(RequestError::BadResponse)
131        };
132        if response.request_id != self.request_id {
133            debug!(
134                target: "downloaders::snap",
135                expected = self.request_id,
136                got = response.request_id,
137                "Byte codes response id mismatch"
138            );
139            return Err(RequestError::BadResponse)
140        }
141        if response.codes.len() > self.hashes.len() {
142            debug!(
143                target: "downloaders::snap",
144                requested = self.hashes.len(),
145                got = response.codes.len(),
146                "Byte codes response is longer than the request"
147            );
148            return Err(RequestError::BadResponse)
149        }
150        // Serving nothing is a valid answer from a peer that has none of this code.
151        if response.codes.is_empty() {
152            return Ok(BytecodeOutcome::Unavailable { peer_id })
153        }
154
155        // The cursor only moves forward, so code that repeats, reorders, or was never requested
156        // finds no hash left to match.
157        let mut remaining = self.hashes.as_slice();
158        let mut codes = Vec::with_capacity(response.codes.len());
159        for code in response.codes {
160            let hash = keccak256(&code);
161            let Some(offset) = remaining.iter().position(|requested| *requested == hash) else {
162                debug!(target: "downloaders::snap", %hash, "Unrequested or out-of-order bytecode");
163                return Err(RequestError::BadResponse)
164            };
165            remaining = &remaining[offset + 1..];
166            codes.push((hash, code));
167        }
168
169        Ok(BytecodeOutcome::Verified(VerifiedBytecode { peer_id, codes }))
170    }
171}
172
173#[cfg(test)]
174mod tests {
175    use super::{
176        super::{request::MAX_RETRIES, test_utils::TestSnapClient},
177        *,
178    };
179    use reth_eth_wire_types::snap::{AccountRangeMessage, ByteCodesMessage};
180    use reth_network_p2p::{error::PeerRequestResult, priority::Priority};
181    use reth_network_peers::WithPeerId;
182    use std::sync::Arc;
183
184    fn code(byte: u8) -> Bytes {
185        Bytes::from(vec![byte; 4])
186    }
187
188    fn request(codes: &[Bytes]) -> GetByteCodesMessage {
189        GetByteCodesMessage {
190            request_id: 1,
191            hashes: codes.iter().map(keccak256).collect(),
192            response_bytes: 512 * 1024,
193        }
194    }
195
196    fn response(
197        peer: PeerId,
198        request_id: u64,
199        codes: Vec<Bytes>,
200    ) -> PeerRequestResult<SnapResponse> {
201        Ok(WithPeerId::new(peer, SnapResponse::ByteCodes(ByteCodesMessage { request_id, codes })))
202    }
203
204    // Every attempt gets the same answer, so a rejected response exhausts the retry budget.
205    fn always(
206        peer: PeerId,
207        request_id: u64,
208        codes: Vec<Bytes>,
209    ) -> impl Iterator<Item = PeerRequestResult<SnapResponse>> {
210        std::iter::repeat_with(move || response(peer, request_id, codes.clone()))
211            .take(usize::from(MAX_RETRIES) + 1)
212    }
213
214    fn downloader(
215        client: Arc<TestSnapClient>,
216        request: GetByteCodesMessage,
217    ) -> Result<BytecodeDownloader<Arc<TestSnapClient>>, InvalidBytecodeRequest> {
218        BytecodeDownloader::new(client, request, Runtime::test())
219    }
220
221    fn verified(outcome: BytecodeOutcome) -> VerifiedBytecode {
222        match outcome {
223            BytecodeOutcome::Verified(verified) => verified,
224            BytecodeOutcome::Unavailable { .. } => panic!("expected verified code"),
225        }
226    }
227
228    #[tokio::test]
229    async fn every_requested_code_is_authenticated_by_its_hash() {
230        let codes = vec![code(1), code(2), code(3)];
231        let client = Arc::new(TestSnapClient::new([response(PeerId::random(), 1, codes.clone())]));
232
233        let outcome = downloader(Arc::clone(&client), request(&codes)).unwrap().await.unwrap();
234
235        let verified = verified(outcome);
236        assert_eq!(
237            verified.codes(),
238            codes.iter().map(|code| (keccak256(code), code.clone())).collect::<Vec<_>>()
239        );
240        assert!(verified.missing(&request(&codes).hashes).next().is_none());
241        assert!(client.reported().is_empty());
242    }
243
244    #[tokio::test]
245    async fn an_ordered_subsequence_reports_the_omitted_hashes() {
246        let codes = vec![code(1), code(2), code(3)];
247        let request = request(&codes);
248        // The peer serves only the first and last of the three.
249        let served = vec![codes[0].clone(), codes[2].clone()];
250        let peer = PeerId::random();
251        let client = Arc::new(TestSnapClient::new([response(peer, 1, served)]));
252
253        let outcome = downloader(Arc::clone(&client), request.clone()).unwrap().await.unwrap();
254
255        let verified = verified(outcome);
256        assert_eq!(verified.peer_id(), peer);
257        assert_eq!(
258            verified.codes().iter().map(|(hash, _)| *hash).collect::<Vec<_>>(),
259            [keccak256(&codes[0]), keccak256(&codes[2])]
260        );
261        assert_eq!(verified.missing(&request.hashes).collect::<Vec<_>>(), [keccak256(&codes[1])]);
262        assert!(client.reported().is_empty());
263    }
264
265    #[tokio::test]
266    async fn an_empty_response_is_unavailable_and_not_a_peer_fault() {
267        let codes = vec![code(1)];
268        let peer = PeerId::random();
269        let client = Arc::new(TestSnapClient::new([response(peer, 1, Vec::new())]));
270
271        let outcome = downloader(Arc::clone(&client), request(&codes)).unwrap().await.unwrap();
272
273        assert_eq!(outcome, BytecodeOutcome::Unavailable { peer_id: peer });
274        assert!(client.reported().is_empty());
275    }
276
277    #[tokio::test]
278    async fn unrequested_code_is_reported_and_retried_against_another_peer() {
279        let codes = vec![code(1)];
280        let bad_peer = PeerId::random();
281        let client = Arc::new(TestSnapClient::new([
282            response(bad_peer, 1, vec![code(9)]),
283            response(PeerId::random(), 1, codes.clone()),
284        ]));
285
286        let outcome = downloader(Arc::clone(&client), request(&codes)).unwrap().await.unwrap();
287
288        assert_eq!(verified(outcome).codes().len(), 1);
289        assert_eq!(*client.reported(), [bad_peer]);
290        assert_eq!(*client.priorities(), [Priority::Normal, Priority::High]);
291    }
292
293    #[tokio::test]
294    async fn out_of_order_repeated_and_oversized_responses_are_rejected() {
295        let codes = vec![code(1), code(2)];
296        let peer = PeerId::random();
297        for served in [
298            // reversed, so the second hash is already behind the cursor
299            vec![codes[1].clone(), codes[0].clone()],
300            // the same code twice, which only one hash was requested for
301            vec![codes[0].clone(), codes[0].clone()],
302            // more blobs than hashes requested
303            vec![codes[0].clone(), codes[1].clone(), code(3)],
304        ] {
305            let client = Arc::new(TestSnapClient::new(always(peer, 1, served)));
306
307            let error =
308                downloader(Arc::clone(&client), request(&codes)).unwrap().await.unwrap_err();
309
310            assert_eq!(error, RequestError::BadResponse);
311            assert_eq!(client.reported().len(), usize::from(MAX_RETRIES) + 1);
312        }
313    }
314
315    #[tokio::test]
316    async fn a_response_for_another_request_id_is_rejected() {
317        let codes = vec![code(1)];
318        let peer = PeerId::random();
319        let client = Arc::new(TestSnapClient::new(always(peer, 7, codes.clone())));
320
321        let error = downloader(Arc::clone(&client), request(&codes)).unwrap().await.unwrap_err();
322
323        assert_eq!(error, RequestError::BadResponse);
324        assert_eq!(client.reported().len(), usize::from(MAX_RETRIES) + 1);
325    }
326
327    #[tokio::test]
328    async fn a_wrong_response_type_exhausts_the_retry_budget() {
329        let codes = vec![code(1)];
330        let peers = [PeerId::random(), PeerId::random(), PeerId::random()];
331        let responses = peers.map(|peer| {
332            Ok(WithPeerId::new(
333                peer,
334                SnapResponse::AccountRange(AccountRangeMessage {
335                    request_id: 1,
336                    accounts: Vec::new(),
337                    proof: Vec::new(),
338                }),
339            ))
340        });
341        let client = Arc::new(TestSnapClient::new(responses));
342
343        let error = downloader(Arc::clone(&client), request(&codes)).unwrap().await.unwrap_err();
344
345        assert_eq!(error, RequestError::BadResponse);
346        assert_eq!(*client.reported(), peers);
347        assert_eq!(*client.priorities(), [Priority::Normal, Priority::High, Priority::High]);
348    }
349
350    #[test]
351    fn a_request_without_hashes_is_rejected_before_submission() {
352        let client = Arc::new(TestSnapClient::new(std::iter::empty()));
353        let mut empty = request(&[code(1)]);
354        empty.hashes.clear();
355
356        assert_eq!(
357            downloader(Arc::clone(&client), empty).unwrap_err(),
358            InvalidBytecodeRequest::NoHashes
359        );
360        assert!(client.priorities().is_empty());
361    }
362}