Skip to main content

reth_downloaders/snap/
request.rs

1//! Shared request execution for authenticated snap responses.
2//!
3//! Retries, peer attribution and blocking verification live here so every range downloader
4//! penalizes and reissues in exactly the same way.
5
6use futures::FutureExt;
7use reth_eth_wire_types::snap::{
8    GetAccountRangeMessage, GetBlockAccessListsMessage, GetByteCodesMessage,
9    GetStorageRangesMessage,
10};
11use reth_network_p2p::{
12    error::RequestError,
13    priority::Priority,
14    snap::client::{SnapClient, SnapResponse},
15};
16use reth_network_peers::PeerId;
17use reth_tasks::Runtime;
18use std::{
19    fmt,
20    task::{ready, Context, Poll},
21};
22use tracing::debug;
23
24/// Number of retries allowed after the initial request fails.
25pub(super) const MAX_RETRIES: u8 = 2;
26
27/// Drives one snap request until its response is verified or retries are exhausted.
28pub(super) struct VerifyingRequest<C: SnapClient, V: SnapVerifier> {
29    // Sends each attempt and receives the penalty for an invalid response.
30    client: C,
31    // Verification runs here so peer-controlled proof work stays off the async worker.
32    runtime: Runtime,
33    // Retained so a retry reissues the identical request.
34    request: V::Request,
35    // Cloned per attempt, because verification moves onto the blocking pool.
36    verifier: V,
37    // The response currently in flight.
38    fut: C::Output,
39    // Present only while a response is being authenticated.
40    verification: Option<VerificationTask<V::Output>>,
41    // Attempts already spent against `MAX_RETRIES`.
42    retries: u8,
43}
44
45impl<C, V> VerifyingRequest<C, V>
46where
47    C: SnapClient,
48    V: SnapVerifier,
49{
50    /// Submits `request` and prepares to authenticate its response with `verifier`.
51    pub(super) fn new(client: C, request: V::Request, verifier: V, runtime: Runtime) -> Self {
52        let fut = request.send(&client, Priority::Normal);
53        Self { client, runtime, request, verifier, fut, verification: None, retries: 0 }
54    }
55
56    /// Polls until the request yields a verified response or a terminal error.
57    ///
58    /// An active verification finishes before another response is accepted, so a peer stays
59    /// attributable for the work done on its behalf.
60    pub(super) fn poll_verified(
61        &mut self,
62        cx: &mut Context<'_>,
63    ) -> Poll<Result<V::Output, RequestError>> {
64        loop {
65            if self.verification.is_some() {
66                match ready!(self.poll_verification(cx)) {
67                    Ok(Some(output)) => return Poll::Ready(Ok(output)),
68                    Ok(None) => {}
69                    Err(error) => return Poll::Ready(Err(error)),
70                }
71            }
72
73            match ready!(self.fut.poll_unpin(cx)) {
74                Ok(response) => {
75                    let (peer_id, response) = response.split();
76                    let verifier = self.verifier.clone();
77                    let fut =
78                        self.runtime.spawn_blocking(move || verifier.verify(peer_id, response));
79                    self.verification = Some(VerificationTask { peer_id, fut });
80                }
81                // A wrong wire response is already penalized by the session.
82                Err(error) if error.is_retryable() || error == RequestError::BadResponse => {
83                    debug!(target: "downloaders::snap", %error, "Snap request failed, retrying");
84                    if !self.retry() {
85                        return Poll::Ready(Err(error))
86                    }
87                }
88                Err(error) => return Poll::Ready(Err(error)),
89            }
90        }
91    }
92
93    // Raise retry priority so transient failures cannot leave range progress behind new work.
94    fn retry(&mut self) -> bool {
95        if self.retries >= MAX_RETRIES {
96            return false
97        }
98        self.retries += 1;
99        self.fut = self.request.send(&self.client, Priority::High);
100        true
101    }
102
103    // The responder stays attached until blocking verification completes.
104    fn poll_verification(
105        &mut self,
106        cx: &mut Context<'_>,
107    ) -> Poll<Result<Option<V::Output>, RequestError>> {
108        let verification = self.verification.as_mut().expect("verification task is present");
109        let result = ready!(verification.fut.poll_unpin(cx));
110        let peer_id = verification.peer_id;
111        self.verification = None;
112
113        match result {
114            Ok(Ok(output)) => Poll::Ready(Ok(Some(output))),
115            Ok(Err(error)) => {
116                debug!(target: "downloaders::snap", ?peer_id, %error, "Invalid snap response");
117                self.client.report_bad_message(peer_id);
118                Poll::Ready(self.retry().then_some(None).ok_or(error))
119            }
120            // A panic or a shutting-down runtime is local, so it must not penalize the responder.
121            Err(error) => {
122                debug!(target: "downloaders::snap", %error, "Snap verification task failed");
123                Poll::Ready(Err(RequestError::ChannelClosed))
124            }
125        }
126    }
127}
128
129// The opaque client future cannot be printed without imposing an unnecessary bound.
130impl<C, V> fmt::Debug for VerifyingRequest<C, V>
131where
132    C: SnapClient,
133    V: SnapVerifier + fmt::Debug,
134    V::Request: fmt::Debug,
135{
136    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
137        f.debug_struct("VerifyingRequest")
138            .field("client", &self.client)
139            .field("request", &self.request)
140            .field("verifier", &self.verifier)
141            .field("verifying", &self.verification.is_some())
142            .field("retries", &self.retries)
143            .finish_non_exhaustive()
144    }
145}
146
147/// A snap request that can be reissued at a chosen priority.
148pub(super) trait SnapRequest {
149    /// Sends this request through `client`.
150    fn send<C: SnapClient>(&self, client: &C, priority: Priority) -> C::Output;
151}
152
153impl SnapRequest for GetAccountRangeMessage {
154    fn send<C: SnapClient>(&self, client: &C, priority: Priority) -> C::Output {
155        client.get_account_range_with_priority(self.clone(), priority)
156    }
157}
158
159impl SnapRequest for GetStorageRangesMessage {
160    fn send<C: SnapClient>(&self, client: &C, priority: Priority) -> C::Output {
161        client.get_storage_ranges_with_priority(self.clone(), priority)
162    }
163}
164
165impl SnapRequest for GetBlockAccessListsMessage {
166    fn send<C: SnapClient>(&self, client: &C, priority: Priority) -> C::Output {
167        client.get_block_access_lists_with_priority(self.clone(), priority)
168    }
169}
170
171impl SnapRequest for GetByteCodesMessage {
172    fn send<C: SnapClient>(&self, client: &C, priority: Priority) -> C::Output {
173        client.get_byte_codes_with_priority(self.clone(), priority)
174    }
175}
176
177/// Authenticates a snap response against the request that asked for it.
178pub(super) trait SnapVerifier: Clone + Send + 'static {
179    /// Request type this verifier authenticates responses for.
180    type Request: SnapRequest;
181    /// Verified output returned to the downloader.
182    type Output: Send + 'static;
183
184    /// Verifies `response`, retaining the responder for non-error outcomes.
185    fn verify(self, peer_id: PeerId, response: SnapResponse) -> Result<Self::Output, RequestError>;
186}
187
188// Proof failures remain attributable after leaving the async worker.
189struct VerificationTask<O> {
190    // The responder to penalize if verification rejects its response.
191    peer_id: PeerId,
192    // Returns the verified output without blocking the async worker.
193    fut: tokio::task::JoinHandle<Result<O, RequestError>>,
194}