polkadot_node_network_protocol/request_response/mod.rs
1// Copyright (C) Parity Technologies (UK) Ltd.
2// This file is part of Polkadot.
3
4// Polkadot is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8
9// Polkadot is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU General Public License for more details.
13
14// You should have received a copy of the GNU General Public License
15// along with Polkadot. If not, see <http://www.gnu.org/licenses/>.
16
17//! Overview over request/responses as used in `Polkadot`.
18//!
19//! `enum Protocol` .... List of all supported protocols.
20//!
21//! `enum Requests` .... List of all supported requests, each entry matches one in protocols, but
22//! has the actual request as payload.
23//!
24//! `struct IncomingRequest` .... wrapper for incoming requests, containing a sender for sending
25//! responses.
26//!
27//! `struct OutgoingRequest` .... wrapper for outgoing requests, containing a sender used by the
28//! networking code for delivering responses/delivery errors.
29//!
30//! `trait IsRequest` .... A trait describing a particular request. It is used for gathering meta
31//! data, like what is the corresponding response type.
32//!
33//! ## Versioning
34//!
35//! Versioning for request-response protocols can be done in multiple ways.
36//!
37//! If you're just changing the protocol name but the binary payloads are the same, just add a new
38//! `fallback_name` to the protocol config.
39//!
40//! One way in which versioning has historically been achieved for req-response protocols is to
41//! bundle the new req-resp version with an upgrade of a notifications protocol. The subsystem would
42//! then know which request version to use based on stored data about the peer's notifications
43//! protocol version.
44//!
45//! When bumping a notifications protocol version is not needed/desirable, you may add a new
46//! req-resp protocol and set the old request as a fallback (see
47//! `OutgoingRequest::new_with_fallback`). A request with the new version will be attempted and if
48//! the protocol is refused by the peer, the fallback protocol request will be used.
49//! Information about the actually used protocol will be returned alongside the raw response, so
50//! that you know how to decode it.
51
52use std::{collections::HashMap, time::Duration, u64};
53
54use sc_network::{NetworkBackend, MAX_RESPONSE_SIZE};
55use sp_runtime::traits::Block;
56use strum::{EnumIter, IntoEnumIterator};
57
58pub use sc_network::{config as network, config::RequestResponseConfig, ProtocolName};
59
60/// Everything related to handling of incoming requests.
61pub mod incoming;
62/// Everything related to handling of outgoing requests.
63pub mod outgoing;
64
65pub use incoming::{IncomingRequest, IncomingRequestReceiver};
66
67pub use outgoing::{OutgoingRequest, OutgoingResult, Recipient, Requests, ResponseSender};
68
69///// Multiplexer for incoming requests.
70// pub mod multiplexer;
71
72/// Actual versioned requests and responses that are sent over the wire.
73pub mod v1;
74
75/// Actual versioned requests and responses that are sent over the wire.
76pub mod v2;
77
78/// Actual versioned requests and responses that are sent over the wire.
79pub mod v3;
80
81/// A protocol per subsystem seems to make the most sense, this way we don't need any dispatching
82/// within protocols.
83#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, EnumIter)]
84pub enum Protocol {
85 /// Protocol for chunk fetching, used by availability distribution and availability recovery.
86 ChunkFetchingV1,
87 /// Protocol for fetching collations from collators.
88 CollationFetchingV1,
89 /// Protocol for fetching collations from collators when async backing is enabled.
90 CollationFetchingV2,
91 /// Protocol for fetching collations from collators by output head data hash.
92 CollationFetchingV3,
93 /// Protocol for fetching seconded PoVs from validators of the same group.
94 PoVFetchingV1,
95 /// Protocol for fetching available data.
96 AvailableDataFetchingV1,
97 /// Sending of dispute statements with application level confirmations.
98 DisputeSendingV1,
99
100 /// Protocol for requesting candidates with attestations in statement distribution
101 /// when async backing is enabled.
102 AttestedCandidateV2,
103
104 /// Protocol for chunk fetching version 2, used by availability distribution and availability
105 /// recovery.
106 ChunkFetchingV2,
107}
108
109/// Minimum bandwidth we expect for validators - 500Mbit/s is the recommendation, so approximately
110/// 50MB per second:
111const MIN_BANDWIDTH_BYTES: u64 = 50 * 1024 * 1024;
112
113/// Default request timeout in seconds.
114///
115/// When decreasing this value, take into account that the very first request might need to open a
116/// connection, which can be slow. If this causes problems, we should ensure connectivity via peer
117/// sets.
118#[allow(dead_code)]
119const DEFAULT_REQUEST_TIMEOUT: Duration = Duration::from_secs(3);
120
121/// Request timeout where we can assume the connection is already open (e.g. we have peers in a
122/// peer set as well).
123const DEFAULT_REQUEST_TIMEOUT_CONNECTED: Duration = Duration::from_secs(1);
124
125/// Timeout for requesting availability chunks.
126pub const CHUNK_REQUEST_TIMEOUT: Duration = DEFAULT_REQUEST_TIMEOUT_CONNECTED;
127
128/// This timeout is based on the following parameters, assuming we use asynchronous backing with no
129/// time budget within a relay block:
130/// - 500 Mbit/s networking speed
131/// - 10 MB PoV
132/// - 10 parallel executions
133const POV_REQUEST_TIMEOUT_CONNECTED: Duration = Duration::from_millis(2000);
134
135/// We want attested candidate requests to time out relatively fast,
136/// because slow requests will bottleneck the backing system. Ideally, we'd have
137/// an adaptive timeout based on the candidate size, because there will be a lot of variance
138/// in candidate sizes: candidates with no code and no messages vs candidates with code
139/// and messages.
140///
141/// We supply leniency because there are often large candidates and asynchronous
142/// backing allows them to be included over a longer window of time. Exponential back-off
143/// up to a maximum of 10 seconds would be ideal, but isn't supported by the
144/// infrastructure here yet: see https://github.com/paritytech/polkadot/issues/6009
145const ATTESTED_CANDIDATE_TIMEOUT: Duration = Duration::from_millis(2500);
146
147/// We don't want a slow peer to slow down all the others, at the same time we want to get out the
148/// data quickly in full to at least some peers (as this will reduce load on us as they then can
149/// start serving the data). So this value is a tradeoff. 5 seems to be sensible. So we would need
150/// to have 5 slow nodes connected, to delay transfer for others by `ATTESTED_CANDIDATE_TIMEOUT`.
151pub const MAX_PARALLEL_ATTESTED_CANDIDATE_REQUESTS: u32 = 5;
152
153/// Response size limit for responses of POV like data.
154///
155/// Same as what we use in substrate networking.
156const POV_RESPONSE_SIZE: u64 = MAX_RESPONSE_SIZE;
157
158/// Maximum response sizes for `AttestedCandidateV2`.
159///
160/// Chosen as a safe upper bound above the governance ceiling on validation code size
161/// (`polkadot_primitives::MAX_CODE_SIZE`), leaving headroom for backing statements and
162/// protocol overhead. This is a transport-level DoS cap only; the effective policy is
163/// enforced by the runtime and the node-side inclusion emulator against the on-chain
164/// `HostConfiguration.max_code_size`.
165const ATTESTED_CANDIDATE_RESPONSE_SIZE: u64 = 8 * 1024 * 1024;
166
167/// We can have relative large timeouts here, there is no value of hitting a
168/// timeout as we want to get statements through to each node in any case.
169pub const DISPUTE_REQUEST_TIMEOUT: Duration = Duration::from_secs(12);
170
171impl Protocol {
172 /// Get a configuration for a given Request response protocol.
173 ///
174 /// Returns a `ProtocolConfig` for this protocol.
175 /// Use this if you plan only to send requests for this protocol.
176 pub fn get_outbound_only_config<B: Block, N: NetworkBackend<B, <B as Block>::Hash>>(
177 self,
178 req_protocol_names: &ReqProtocolNames,
179 ) -> N::RequestResponseProtocolConfig {
180 self.create_config::<B, N>(req_protocol_names, None)
181 }
182
183 /// Get a configuration for a given Request response protocol.
184 ///
185 /// Returns a receiver for messages received on this protocol and the requested
186 /// `ProtocolConfig`.
187 pub fn get_config<B: Block, N: NetworkBackend<B, <B as Block>::Hash>>(
188 self,
189 req_protocol_names: &ReqProtocolNames,
190 ) -> (async_channel::Receiver<network::IncomingRequest>, N::RequestResponseProtocolConfig) {
191 let (tx, rx) = async_channel::bounded(self.get_channel_size());
192 let cfg = self.create_config::<B, N>(req_protocol_names, Some(tx));
193 (rx, cfg)
194 }
195
196 fn create_config<B: Block, N: NetworkBackend<B, <B as Block>::Hash>>(
197 self,
198 req_protocol_names: &ReqProtocolNames,
199 tx: Option<async_channel::Sender<network::IncomingRequest>>,
200 ) -> N::RequestResponseProtocolConfig {
201 let name = req_protocol_names.get_name(self);
202 let legacy_names = self.get_legacy_name().into_iter().map(Into::into).collect();
203 match self {
204 Protocol::ChunkFetchingV1 | Protocol::ChunkFetchingV2 => N::request_response_config(
205 name,
206 legacy_names,
207 1_000,
208 POV_RESPONSE_SIZE,
209 // We are connected to all validators:
210 CHUNK_REQUEST_TIMEOUT,
211 tx,
212 ),
213 Protocol::CollationFetchingV1 |
214 Protocol::CollationFetchingV2 |
215 Protocol::CollationFetchingV3 => {
216 N::request_response_config(
217 name,
218 legacy_names,
219 1_000,
220 POV_RESPONSE_SIZE,
221 // Taken from initial implementation in collator protocol:
222 POV_REQUEST_TIMEOUT_CONNECTED,
223 tx,
224 )
225 },
226 Protocol::PoVFetchingV1 => N::request_response_config(
227 name,
228 legacy_names,
229 1_000,
230 POV_RESPONSE_SIZE,
231 POV_REQUEST_TIMEOUT_CONNECTED,
232 tx,
233 ),
234 Protocol::AvailableDataFetchingV1 => N::request_response_config(
235 name,
236 legacy_names,
237 1_000,
238 // Available data size is dominated by the PoV size.
239 POV_RESPONSE_SIZE,
240 POV_REQUEST_TIMEOUT_CONNECTED,
241 tx,
242 ),
243 Protocol::DisputeSendingV1 => N::request_response_config(
244 name,
245 legacy_names,
246 1_000,
247 // Responses are just confirmation, in essence not even a bit. So 100 seems
248 // plenty.
249 100,
250 DISPUTE_REQUEST_TIMEOUT,
251 tx,
252 ),
253 Protocol::AttestedCandidateV2 => N::request_response_config(
254 name,
255 legacy_names,
256 1_000,
257 ATTESTED_CANDIDATE_RESPONSE_SIZE,
258 ATTESTED_CANDIDATE_TIMEOUT,
259 tx,
260 ),
261 }
262 }
263
264 // Channel sizes for the supported protocols.
265 fn get_channel_size(self) -> usize {
266 match self {
267 // Hundreds of validators will start requesting their chunks once they see a candidate
268 // awaiting availability on chain. Given that they will see that block at different
269 // times (due to network delays), 100 seems big enough to accommodate for "bursts",
270 // assuming we can service requests relatively quickly, which would need to be measured
271 // as well.
272 Protocol::ChunkFetchingV1 | Protocol::ChunkFetchingV2 => 100,
273 // 10 seems reasonable, considering group sizes of max 10 validators.
274 Protocol::CollationFetchingV1 |
275 Protocol::CollationFetchingV2 |
276 Protocol::CollationFetchingV3 => 10,
277 // 10 seems reasonable, considering group sizes of max 10 validators.
278 Protocol::PoVFetchingV1 => 10,
279 // Validators are constantly self-selecting to request available data which may lead
280 // to constant load and occasional burstiness.
281 Protocol::AvailableDataFetchingV1 => 100,
282 // Incoming requests can get bursty, we should also be able to handle them fast on
283 // average, so something in the ballpark of 100 should be fine. Nodes will retry on
284 // failure, so having a good value here is mostly about performance tuning.
285 Protocol::DisputeSendingV1 => 100,
286
287 Protocol::AttestedCandidateV2 => {
288 // We assume we can utilize up to 70% of the available bandwidth for statements.
289 // This is just a guess/estimate, with the following considerations: If we are
290 // faster than that, queue size will stay low anyway, even if not - requesters will
291 // get an immediate error, but if we are slower, requesters will run in a timeout -
292 // wasting precious time.
293 let available_bandwidth = 7 * MIN_BANDWIDTH_BYTES / 10;
294 let size = u64::saturating_sub(
295 ATTESTED_CANDIDATE_TIMEOUT.as_millis() as u64 * available_bandwidth /
296 (1000 * ATTESTED_CANDIDATE_RESPONSE_SIZE),
297 MAX_PARALLEL_ATTESTED_CANDIDATE_REQUESTS as u64,
298 );
299 debug_assert!(
300 size > 0,
301 "We should have a channel size greater zero, otherwise we won't accept any requests."
302 );
303 size as usize
304 },
305 }
306 }
307
308 /// Legacy protocol name associated with each peer set, if any.
309 /// The request will be tried on this legacy protocol name if the remote refuses to speak the
310 /// protocol.
311 const fn get_legacy_name(self) -> Option<&'static str> {
312 match self {
313 Protocol::ChunkFetchingV1 => Some("/polkadot/req_chunk/1"),
314 Protocol::CollationFetchingV1 => Some("/polkadot/req_collation/1"),
315 Protocol::PoVFetchingV1 => Some("/polkadot/req_pov/1"),
316 Protocol::AvailableDataFetchingV1 => Some("/polkadot/req_available_data/1"),
317 Protocol::DisputeSendingV1 => Some("/polkadot/send_dispute/1"),
318
319 // Introduced after legacy names became legacy.
320 Protocol::AttestedCandidateV2 => None,
321 Protocol::CollationFetchingV2 => None,
322 Protocol::ChunkFetchingV2 => None,
323 Protocol::CollationFetchingV3 => None,
324 }
325 }
326}
327
328/// Common properties of any `Request`.
329pub trait IsRequest {
330 /// Each request has a corresponding `Response`.
331 type Response;
332
333 /// What protocol this `Request` implements.
334 const PROTOCOL: Protocol;
335}
336
337/// Type for getting on the wire [`Protocol`] names using genesis hash & fork id.
338#[derive(Clone)]
339pub struct ReqProtocolNames {
340 names: HashMap<Protocol, ProtocolName>,
341}
342
343impl ReqProtocolNames {
344 /// Construct [`ReqProtocolNames`] from `genesis_hash` and `fork_id`.
345 pub fn new<Hash: AsRef<[u8]>>(genesis_hash: Hash, fork_id: Option<&str>) -> Self {
346 let mut names = HashMap::new();
347 for protocol in Protocol::iter() {
348 names.insert(protocol, Self::generate_name(protocol, &genesis_hash, fork_id));
349 }
350 Self { names }
351 }
352
353 /// Get on the wire [`Protocol`] name.
354 pub fn get_name(&self, protocol: Protocol) -> ProtocolName {
355 self.names
356 .get(&protocol)
357 .expect("All `Protocol` enum variants are added above via `strum`; qed")
358 .clone()
359 }
360
361 /// Protocol name of this protocol based on `genesis_hash` and `fork_id`.
362 fn generate_name<Hash: AsRef<[u8]>>(
363 protocol: Protocol,
364 genesis_hash: &Hash,
365 fork_id: Option<&str>,
366 ) -> ProtocolName {
367 let prefix = if let Some(fork_id) = fork_id {
368 format!("/{}/{}", hex::encode(genesis_hash), fork_id)
369 } else {
370 format!("/{}", hex::encode(genesis_hash))
371 };
372
373 let short_name = match protocol {
374 // V1:
375 Protocol::ChunkFetchingV1 => "/req_chunk/1",
376 Protocol::CollationFetchingV1 => "/req_collation/1",
377 Protocol::PoVFetchingV1 => "/req_pov/1",
378 Protocol::AvailableDataFetchingV1 => "/req_available_data/1",
379 Protocol::DisputeSendingV1 => "/send_dispute/1",
380
381 // V2:
382 Protocol::CollationFetchingV2 => "/req_collation/2",
383 Protocol::AttestedCandidateV2 => "/req_attested_candidate/2",
384 Protocol::ChunkFetchingV2 => "/req_chunk/2",
385 // V3:
386 Protocol::CollationFetchingV3 => "/req_collation/3",
387 };
388
389 format!("{}{}", prefix, short_name).into()
390 }
391}