// Copyright (C) Parity Technologies (UK) Ltd.
// Polkadot is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Polkadot is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Polkadot. If not, see .
//! Large statement requesting background task logic.
use std::time::Duration;
use futures::{
channel::{mpsc, oneshot},
SinkExt,
};
use polkadot_node_network_protocol::{
request_response::{
v1::{StatementFetchingRequest, StatementFetchingResponse},
OutgoingRequest, Recipient, Requests,
},
PeerId, UnifiedReputationChange,
};
use polkadot_node_subsystem::{Span, Stage};
use polkadot_node_subsystem_util::TimeoutExt;
use polkadot_primitives::{CandidateHash, CommittedCandidateReceipt, Hash};
use crate::{metrics::Metrics, COST_WRONG_HASH, LOG_TARGET};
// In case we failed fetching from our known peers, how long we should wait before attempting a
// retry, even though we have not yet discovered any new peers. Or in other words how long to
// wait before retrying peers that already failed.
const RETRY_TIMEOUT: Duration = Duration::from_millis(500);
/// Messages coming from a background task.
pub enum RequesterMessage {
/// Get an update of available peers to try for fetching a given statement.
GetMorePeers {
relay_parent: Hash,
candidate_hash: CandidateHash,
tx: oneshot::Sender>,
},
/// Fetching finished, ask for verification. If verification fails, task will continue asking
/// peers for data.
Finished {
/// Relay parent this candidate is in the context of.
relay_parent: Hash,
/// The candidate we fetched data for.
candidate_hash: CandidateHash,
/// Data was fetched from this peer.
from_peer: PeerId,
/// Response we received from above peer.
response: CommittedCandidateReceipt,
/// Peers which failed providing the data.
bad_peers: Vec,
},
/// Report a peer which behaved worse than just not providing data:
ReportPeer(PeerId, UnifiedReputationChange),
/// Ask subsystem to send a request for us.
SendRequest(Requests),
}
/// A fetching task, taking care of fetching large statements via request/response.
///
/// A fetch task does not know about a particular `Statement` instead it just tries fetching a
/// `CommittedCandidateReceipt` from peers, whether this can be used to re-assemble one ore
/// many `SignedFullStatement`s needs to be verified by the caller.
pub async fn fetch(
relay_parent: Hash,
candidate_hash: CandidateHash,
peers: Vec,
mut sender: mpsc::Sender,
metrics: Metrics,
) {
let span = Span::new(candidate_hash, "fetch-large-statement")
.with_relay_parent(relay_parent)
.with_stage(Stage::StatementDistribution);
gum::debug!(
target: LOG_TARGET,
?candidate_hash,
?relay_parent,
"Fetch for large statement started",
);
// Peers we already tried (and failed).
let mut tried_peers = Vec::new();
// Peers left for trying out.
let mut new_peers = peers;
let req = StatementFetchingRequest { relay_parent, candidate_hash };
// We retry endlessly (with sleep periods), and rely on the subsystem to kill us eventually.
loop {
let span = span.child("try-available-peers");
while let Some(peer) = new_peers.pop() {
let _span = span.child("try-peer").with_peer_id(&peer);
let (outgoing, pending_response) =
OutgoingRequest::new(Recipient::Peer(peer), req.clone());
if let Err(err) = sender
.feed(RequesterMessage::SendRequest(Requests::StatementFetchingV1(outgoing)))
.await
{
gum::info!(
target: LOG_TARGET,
?err,
"Sending request failed, node might be shutting down - exiting."
);
return
}
metrics.on_sent_request();
match pending_response.await {
Ok(StatementFetchingResponse::Statement(statement)) => {
if statement.hash() != candidate_hash {
metrics.on_received_response(false);
metrics.on_unexpected_statement_large();
if let Err(err) =
sender.feed(RequesterMessage::ReportPeer(peer, COST_WRONG_HASH)).await
{
gum::warn!(
target: LOG_TARGET,
?err,
"Sending reputation change failed: This should not happen."
);
}
// We want to get rid of this peer:
continue
}
if let Err(err) = sender
.feed(RequesterMessage::Finished {
relay_parent,
candidate_hash,
from_peer: peer,
response: statement,
bad_peers: tried_peers,
})
.await
{
gum::warn!(
target: LOG_TARGET,
?err,
"Sending task response failed: This should not happen."
);
}
metrics.on_received_response(true);
// We are done now.
return
},
Err(err) => {
gum::debug!(
target: LOG_TARGET,
?err,
"Receiving response failed with error - trying next peer."
);
metrics.on_received_response(false);
metrics.on_unexpected_statement_large();
},
}
tried_peers.push(peer);
}
new_peers = std::mem::take(&mut tried_peers);
// All our peers failed us - try getting new ones before trying again:
match try_get_new_peers(relay_parent, candidate_hash, &mut sender, &span).await {
Ok(Some(mut peers)) => {
gum::trace!(target: LOG_TARGET, ?peers, "Received new peers.");
// New arrivals will be tried first:
new_peers.append(&mut peers);
},
// No new peers, try the old ones again (if we have any):
Ok(None) => {
// Note: In case we don't have any more peers, we will just keep asking for new
// peers, which is exactly what we want.
},
Err(()) => return,
}
}
}
/// Try getting new peers from subsystem.
///
/// If there are non, we will return after a timeout with `None`.
async fn try_get_new_peers(
relay_parent: Hash,
candidate_hash: CandidateHash,
sender: &mut mpsc::Sender,
span: &Span,
) -> Result