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