mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-07-20 16:15:41 +00:00
sc-consensus-beefy: add peer reputation cost/benefit changes (#13881)
* add cost/benefit to gossip messages * report BEEFY gossip peer reputation changes * drop WorkerParams helper struct * add reputation costs to tests * add peer reputation cost/benefit to on-demand-requests protocol * include amount of signatures checked in invalid proof reputation cost Signed-off-by: Adrian Catangiu <adrian@parity.io>
This commit is contained in:
@@ -18,7 +18,7 @@
|
||||
|
||||
use std::{collections::BTreeMap, sync::Arc, time::Duration};
|
||||
|
||||
use sc_network::PeerId;
|
||||
use sc_network::{PeerId, ReputationChange};
|
||||
use sc_network_gossip::{MessageIntent, ValidationResult, Validator, ValidatorContext};
|
||||
use sp_core::hashing::twox_64;
|
||||
use sp_runtime::traits::{Block, Hash, Header, NumberFor};
|
||||
@@ -26,10 +26,14 @@ use sp_runtime::traits::{Block, Hash, Header, NumberFor};
|
||||
use codec::{Decode, Encode};
|
||||
use log::{debug, trace};
|
||||
use parking_lot::{Mutex, RwLock};
|
||||
use sc_utils::mpsc::{tracing_unbounded, TracingUnboundedReceiver, TracingUnboundedSender};
|
||||
use wasm_timer::Instant;
|
||||
|
||||
use crate::{
|
||||
communication::peers::KnownPeers,
|
||||
communication::{
|
||||
benefit, cost,
|
||||
peers::{KnownPeers, PeerReport},
|
||||
},
|
||||
justification::{
|
||||
proof_block_num_and_set_id, verify_with_validator_set, BeefyVersionedFinalityProof,
|
||||
},
|
||||
@@ -47,6 +51,27 @@ const REBROADCAST_AFTER: Duration = Duration::from_secs(60);
|
||||
#[cfg(test)]
|
||||
const REBROADCAST_AFTER: Duration = Duration::from_secs(5);
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub(super) enum Action<H> {
|
||||
// repropagate under given topic, to the given peers, applying cost/benefit to originator.
|
||||
Keep(H, ReputationChange),
|
||||
// discard, applying cost/benefit to originator.
|
||||
Discard(ReputationChange),
|
||||
}
|
||||
|
||||
/// An outcome of examining a message.
|
||||
#[derive(Debug, PartialEq, Clone, Copy)]
|
||||
enum Consider {
|
||||
/// Accept the message.
|
||||
Accept,
|
||||
/// Message is too early. Reject.
|
||||
RejectPast,
|
||||
/// Message is from the future. Reject.
|
||||
RejectFuture,
|
||||
/// Message cannot be evaluated. Reject.
|
||||
RejectOutOfScope,
|
||||
}
|
||||
|
||||
/// BEEFY gossip message type that gets encoded and sent on the network.
|
||||
#[derive(Debug, Encode, Decode)]
|
||||
pub(crate) enum GossipMessage<B: Block> {
|
||||
@@ -135,26 +160,47 @@ impl<B: Block> Filter<B> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Return true if `max(session_start, best_beefy) <= round <= best_grandpa`,
|
||||
/// Accept if `max(session_start, best_beefy) <= round <= best_grandpa`,
|
||||
/// and vote `set_id` matches session set id.
|
||||
///
|
||||
/// Latest concluded round is still considered alive to allow proper gossiping for it.
|
||||
fn is_vote_accepted(&self, round: NumberFor<B>, set_id: ValidatorSetId) -> bool {
|
||||
fn consider_vote(&self, round: NumberFor<B>, set_id: ValidatorSetId) -> Consider {
|
||||
self.inner
|
||||
.as_ref()
|
||||
.map(|f| set_id == f.validator_set.id() && round >= f.start && round <= f.end)
|
||||
.unwrap_or(false)
|
||||
.map(|f|
|
||||
// only from current set and only [filter.start, filter.end]
|
||||
if set_id < f.validator_set.id() {
|
||||
Consider::RejectPast
|
||||
} else if set_id > f.validator_set.id() {
|
||||
Consider::RejectFuture
|
||||
} else if round < f.start {
|
||||
Consider::RejectPast
|
||||
} else if round > f.end {
|
||||
Consider::RejectFuture
|
||||
} else {
|
||||
Consider::Accept
|
||||
})
|
||||
.unwrap_or(Consider::RejectOutOfScope)
|
||||
}
|
||||
|
||||
/// Return true if `round` is >= than `max(session_start, best_beefy)`,
|
||||
/// and proof `set_id` matches session set id.
|
||||
///
|
||||
/// Latest concluded round is still considered alive to allow proper gossiping for it.
|
||||
fn is_finality_proof_accepted(&self, round: NumberFor<B>, set_id: ValidatorSetId) -> bool {
|
||||
fn consider_finality_proof(&self, round: NumberFor<B>, set_id: ValidatorSetId) -> Consider {
|
||||
self.inner
|
||||
.as_ref()
|
||||
.map(|f| set_id == f.validator_set.id() && round >= f.start)
|
||||
.unwrap_or(false)
|
||||
.map(|f|
|
||||
// only from current set and only >= filter.start
|
||||
if round < f.start || set_id < f.validator_set.id() {
|
||||
Consider::RejectPast
|
||||
} else if set_id > f.validator_set.id() {
|
||||
Consider::RejectFuture
|
||||
} else {
|
||||
Consider::Accept
|
||||
}
|
||||
)
|
||||
.unwrap_or(Consider::RejectOutOfScope)
|
||||
}
|
||||
|
||||
/// Add new _known_ `hash` to the round's known votes.
|
||||
@@ -189,20 +235,26 @@ where
|
||||
gossip_filter: RwLock<Filter<B>>,
|
||||
next_rebroadcast: Mutex<Instant>,
|
||||
known_peers: Arc<Mutex<KnownPeers<B>>>,
|
||||
report_sender: TracingUnboundedSender<PeerReport>,
|
||||
}
|
||||
|
||||
impl<B> GossipValidator<B>
|
||||
where
|
||||
B: Block,
|
||||
{
|
||||
pub fn new(known_peers: Arc<Mutex<KnownPeers<B>>>) -> GossipValidator<B> {
|
||||
GossipValidator {
|
||||
pub(crate) fn new(
|
||||
known_peers: Arc<Mutex<KnownPeers<B>>>,
|
||||
) -> (GossipValidator<B>, TracingUnboundedReceiver<PeerReport>) {
|
||||
let (tx, rx) = tracing_unbounded("mpsc_beefy_gossip_validator", 10_000);
|
||||
let val = GossipValidator {
|
||||
votes_topic: votes_topic::<B>(),
|
||||
justifs_topic: proofs_topic::<B>(),
|
||||
gossip_filter: RwLock::new(Filter::new()),
|
||||
next_rebroadcast: Mutex::new(Instant::now() + REBROADCAST_AFTER),
|
||||
known_peers,
|
||||
}
|
||||
report_sender: tx,
|
||||
};
|
||||
(val, rx)
|
||||
}
|
||||
|
||||
/// Update gossip validator filter.
|
||||
@@ -213,12 +265,16 @@ where
|
||||
self.gossip_filter.write().update(filter);
|
||||
}
|
||||
|
||||
fn report(&self, who: PeerId, cost_benefit: ReputationChange) {
|
||||
let _ = self.report_sender.unbounded_send(PeerReport { who, cost_benefit });
|
||||
}
|
||||
|
||||
fn validate_vote(
|
||||
&self,
|
||||
vote: VoteMessage<NumberFor<B>, AuthorityId, Signature>,
|
||||
sender: &PeerId,
|
||||
data: &[u8],
|
||||
) -> ValidationResult<B::Hash> {
|
||||
) -> Action<B::Hash> {
|
||||
let msg_hash = twox_64(data);
|
||||
let round = vote.commitment.block_number;
|
||||
let set_id = vote.commitment.validator_set_id;
|
||||
@@ -230,25 +286,37 @@ where
|
||||
{
|
||||
let filter = self.gossip_filter.read();
|
||||
|
||||
if !filter.is_vote_accepted(round, set_id) {
|
||||
return ValidationResult::Discard
|
||||
match filter.consider_vote(round, set_id) {
|
||||
Consider::RejectPast => return Action::Discard(cost::OUTDATED_MESSAGE),
|
||||
Consider::RejectFuture => return Action::Discard(cost::FUTURE_MESSAGE),
|
||||
Consider::RejectOutOfScope => return Action::Discard(cost::OUT_OF_SCOPE_MESSAGE),
|
||||
Consider::Accept => {},
|
||||
}
|
||||
|
||||
if filter.is_known_vote(round, &msg_hash) {
|
||||
return ValidationResult::ProcessAndKeep(self.votes_topic)
|
||||
return Action::Keep(self.votes_topic, benefit::KNOWN_VOTE_MESSAGE)
|
||||
}
|
||||
|
||||
// ensure authority is part of the set.
|
||||
if !filter
|
||||
.validator_set()
|
||||
.map(|set| set.validators().contains(&vote.id))
|
||||
.unwrap_or(false)
|
||||
{
|
||||
debug!(target: LOG_TARGET, "Message from voter not in validator set: {}", vote.id);
|
||||
return Action::Discard(cost::UNKNOWN_VOTER)
|
||||
}
|
||||
}
|
||||
|
||||
if BeefyKeystore::verify(&vote.id, &vote.signature, &vote.commitment.encode()) {
|
||||
self.gossip_filter.write().add_known_vote(round, msg_hash);
|
||||
ValidationResult::ProcessAndKeep(self.votes_topic)
|
||||
Action::Keep(self.votes_topic, benefit::VOTE_MESSAGE)
|
||||
} else {
|
||||
// TODO: report peer
|
||||
debug!(
|
||||
target: LOG_TARGET,
|
||||
"🥩 Bad signature on message: {:?}, from: {:?}", vote, sender
|
||||
);
|
||||
ValidationResult::Discard
|
||||
Action::Discard(cost::BAD_SIGNATURE)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -256,31 +324,38 @@ where
|
||||
&self,
|
||||
proof: BeefyVersionedFinalityProof<B>,
|
||||
sender: &PeerId,
|
||||
) -> ValidationResult<B::Hash> {
|
||||
) -> Action<B::Hash> {
|
||||
let (round, set_id) = proof_block_num_and_set_id::<B>(&proof);
|
||||
self.known_peers.lock().note_vote_for(*sender, round);
|
||||
|
||||
let guard = self.gossip_filter.read();
|
||||
// Verify general usefulness of the justifications.
|
||||
if !guard.is_finality_proof_accepted(round, set_id) {
|
||||
return ValidationResult::Discard
|
||||
// Verify general usefulness of the justification.
|
||||
match guard.consider_finality_proof(round, set_id) {
|
||||
Consider::RejectPast => return Action::Discard(cost::OUTDATED_MESSAGE),
|
||||
Consider::RejectFuture => return Action::Discard(cost::FUTURE_MESSAGE),
|
||||
Consider::RejectOutOfScope => return Action::Discard(cost::OUT_OF_SCOPE_MESSAGE),
|
||||
Consider::Accept => {},
|
||||
}
|
||||
// Verify justification signatures.
|
||||
guard
|
||||
.validator_set()
|
||||
.map(|validator_set| {
|
||||
if let Ok(()) = verify_with_validator_set::<B>(round, validator_set, &proof) {
|
||||
ValidationResult::ProcessAndKeep(self.justifs_topic)
|
||||
} else {
|
||||
// TODO: report peer
|
||||
if let Err((_, signatures_checked)) =
|
||||
verify_with_validator_set::<B>(round, validator_set, &proof)
|
||||
{
|
||||
debug!(
|
||||
target: LOG_TARGET,
|
||||
"🥩 Bad signatures on message: {:?}, from: {:?}", proof, sender
|
||||
);
|
||||
ValidationResult::Discard
|
||||
let mut cost = cost::INVALID_PROOF;
|
||||
cost.value +=
|
||||
cost::PER_SIGNATURE_CHECKED.saturating_mul(signatures_checked as i32);
|
||||
Action::Discard(cost)
|
||||
} else {
|
||||
Action::Keep(self.justifs_topic, benefit::VALIDATED_PROOF)
|
||||
}
|
||||
})
|
||||
.unwrap_or(ValidationResult::Discard)
|
||||
.unwrap_or(Action::Discard(cost::OUT_OF_SCOPE_MESSAGE))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -294,15 +369,32 @@ where
|
||||
|
||||
fn validate(
|
||||
&self,
|
||||
_context: &mut dyn ValidatorContext<B>,
|
||||
context: &mut dyn ValidatorContext<B>,
|
||||
sender: &PeerId,
|
||||
mut data: &[u8],
|
||||
) -> ValidationResult<B::Hash> {
|
||||
match GossipMessage::<B>::decode(&mut data) {
|
||||
Ok(GossipMessage::Vote(msg)) => self.validate_vote(msg, sender, data),
|
||||
let raw = data;
|
||||
let action = match GossipMessage::<B>::decode(&mut data) {
|
||||
Ok(GossipMessage::Vote(msg)) => self.validate_vote(msg, sender, raw),
|
||||
Ok(GossipMessage::FinalityProof(proof)) => self.validate_finality_proof(proof, sender),
|
||||
Err(e) => {
|
||||
debug!(target: LOG_TARGET, "Error decoding message: {}", e);
|
||||
let bytes = raw.len().min(i32::MAX as usize) as i32;
|
||||
let cost = ReputationChange::new(
|
||||
bytes.saturating_mul(cost::PER_UNDECODABLE_BYTE),
|
||||
"BEEFY: Bad packet",
|
||||
);
|
||||
Action::Discard(cost)
|
||||
},
|
||||
};
|
||||
match action {
|
||||
Action::Keep(topic, cb) => {
|
||||
self.report(*sender, cb);
|
||||
context.broadcast_message(topic, data.to_vec(), false);
|
||||
ValidationResult::ProcessAndKeep(topic)
|
||||
},
|
||||
Action::Discard(cb) => {
|
||||
self.report(*sender, cb);
|
||||
ValidationResult::Discard
|
||||
},
|
||||
}
|
||||
@@ -314,13 +406,13 @@ where
|
||||
Ok(GossipMessage::Vote(msg)) => {
|
||||
let round = msg.commitment.block_number;
|
||||
let set_id = msg.commitment.validator_set_id;
|
||||
let expired = !filter.is_vote_accepted(round, set_id);
|
||||
let expired = filter.consider_vote(round, set_id) != Consider::Accept;
|
||||
trace!(target: LOG_TARGET, "🥩 Vote for round #{} expired: {}", round, expired);
|
||||
expired
|
||||
},
|
||||
Ok(GossipMessage::FinalityProof(proof)) => {
|
||||
let (round, set_id) = proof_block_num_and_set_id::<B>(&proof);
|
||||
let expired = !filter.is_finality_proof_accepted(round, set_id);
|
||||
let expired = filter.consider_finality_proof(round, set_id) != Consider::Accept;
|
||||
trace!(
|
||||
target: LOG_TARGET,
|
||||
"🥩 Finality proof for round #{} expired: {}",
|
||||
@@ -358,13 +450,13 @@ where
|
||||
Ok(GossipMessage::Vote(msg)) => {
|
||||
let round = msg.commitment.block_number;
|
||||
let set_id = msg.commitment.validator_set_id;
|
||||
let allowed = filter.is_vote_accepted(round, set_id);
|
||||
let allowed = filter.consider_vote(round, set_id) == Consider::Accept;
|
||||
trace!(target: LOG_TARGET, "🥩 Vote for round #{} allowed: {}", round, allowed);
|
||||
allowed
|
||||
},
|
||||
Ok(GossipMessage::FinalityProof(proof)) => {
|
||||
let (round, set_id) = proof_block_num_and_set_id::<B>(&proof);
|
||||
let allowed = filter.is_finality_proof_accepted(round, set_id);
|
||||
let allowed = filter.consider_finality_proof(round, set_id) == Consider::Accept;
|
||||
trace!(
|
||||
target: LOG_TARGET,
|
||||
"🥩 Finality proof for round #{} allowed: {}",
|
||||
@@ -409,15 +501,16 @@ pub(crate) mod tests {
|
||||
assert_eq!(filter.live_votes.len(), 3);
|
||||
|
||||
assert!(filter.inner.is_none());
|
||||
assert!(!filter.is_vote_accepted(1, 1));
|
||||
assert_eq!(filter.consider_vote(1, 1), Consider::RejectOutOfScope);
|
||||
|
||||
filter.update(GossipFilterCfg { start: 3, end: 10, validator_set: &validator_set });
|
||||
assert_eq!(filter.live_votes.len(), 1);
|
||||
assert!(filter.live_votes.contains_key(&3));
|
||||
assert!(!filter.is_vote_accepted(2, 1));
|
||||
assert!(filter.is_vote_accepted(3, 1));
|
||||
assert!(filter.is_vote_accepted(4, 1));
|
||||
assert!(!filter.is_vote_accepted(4, 2));
|
||||
assert_eq!(filter.consider_vote(2, 1), Consider::RejectPast);
|
||||
assert_eq!(filter.consider_vote(3, 1), Consider::Accept);
|
||||
assert_eq!(filter.consider_vote(4, 1), Consider::Accept);
|
||||
assert_eq!(filter.consider_vote(20, 1), Consider::RejectFuture);
|
||||
assert_eq!(filter.consider_vote(4, 2), Consider::RejectFuture);
|
||||
|
||||
let validator_set = ValidatorSet::<AuthorityId>::new(keys, 2).unwrap();
|
||||
filter.update(GossipFilterCfg { start: 5, end: 10, validator_set: &validator_set });
|
||||
@@ -430,9 +523,7 @@ pub(crate) mod tests {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn broadcast_message(&mut self, _topic: B::Hash, _message: Vec<u8>, _force: bool) {
|
||||
todo!()
|
||||
}
|
||||
fn broadcast_message(&mut self, _topic: B::Hash, _message: Vec<u8>, _force: bool) {}
|
||||
|
||||
fn send_message(&mut self, _who: &sc_network::PeerId, _message: Vec<u8>) {
|
||||
todo!()
|
||||
@@ -485,18 +576,39 @@ pub(crate) mod tests {
|
||||
fn should_validate_messages() {
|
||||
let keys = vec![Keyring::Alice.public()];
|
||||
let validator_set = ValidatorSet::<AuthorityId>::new(keys.clone(), 0).unwrap();
|
||||
let gv = GossipValidator::<Block>::new(Arc::new(Mutex::new(KnownPeers::new())));
|
||||
gv.update_filter(GossipFilterCfg { start: 0, end: 10, validator_set: &validator_set });
|
||||
let sender = sc_network::PeerId::random();
|
||||
let (gv, mut report_stream) =
|
||||
GossipValidator::<Block>::new(Arc::new(Mutex::new(KnownPeers::new())));
|
||||
let sender = PeerId::random();
|
||||
let mut context = TestContext;
|
||||
|
||||
// reject message, decoding error
|
||||
let bad_encoding = b"0000000000".as_slice();
|
||||
let expected_cost = ReputationChange::new(
|
||||
(bad_encoding.len() as i32).saturating_mul(cost::PER_UNDECODABLE_BYTE),
|
||||
"BEEFY: Bad packet",
|
||||
);
|
||||
let mut expected_report = PeerReport { who: sender, cost_benefit: expected_cost };
|
||||
let res = gv.validate(&mut context, &sender, bad_encoding);
|
||||
assert!(matches!(res, ValidationResult::Discard));
|
||||
assert_eq!(report_stream.try_recv().unwrap(), expected_report);
|
||||
|
||||
// verify votes validation
|
||||
|
||||
let vote = dummy_vote(3);
|
||||
let gossip_vote = GossipMessage::<Block>::Vote(vote.clone());
|
||||
let encoded = GossipMessage::<Block>::Vote(vote.clone()).encode();
|
||||
|
||||
// first time the cache should be populated
|
||||
let res = gv.validate(&mut context, &sender, &gossip_vote.encode());
|
||||
// filter not initialized
|
||||
let res = gv.validate(&mut context, &sender, &encoded);
|
||||
assert!(matches!(res, ValidationResult::Discard));
|
||||
expected_report.cost_benefit = cost::OUT_OF_SCOPE_MESSAGE;
|
||||
assert_eq!(report_stream.try_recv().unwrap(), expected_report);
|
||||
|
||||
gv.update_filter(GossipFilterCfg { start: 0, end: 10, validator_set: &validator_set });
|
||||
// nothing in cache first time
|
||||
let res = gv.validate(&mut context, &sender, &encoded);
|
||||
assert!(matches!(res, ValidationResult::ProcessAndKeep(_)));
|
||||
expected_report.cost_benefit = benefit::VOTE_MESSAGE;
|
||||
assert_eq!(report_stream.try_recv().unwrap(), expected_report);
|
||||
assert_eq!(
|
||||
gv.gossip_filter
|
||||
.read()
|
||||
@@ -507,43 +619,74 @@ pub(crate) mod tests {
|
||||
);
|
||||
|
||||
// second time we should hit the cache
|
||||
let res = gv.validate(&mut context, &sender, &gossip_vote.encode());
|
||||
let res = gv.validate(&mut context, &sender, &encoded);
|
||||
assert!(matches!(res, ValidationResult::ProcessAndKeep(_)));
|
||||
expected_report.cost_benefit = benefit::KNOWN_VOTE_MESSAGE;
|
||||
assert_eq!(report_stream.try_recv().unwrap(), expected_report);
|
||||
|
||||
// next we should quickly reject if the round is not live
|
||||
gv.update_filter(GossipFilterCfg { start: 7, end: 10, validator_set: &validator_set });
|
||||
// reject vote, voter not in validator set
|
||||
let mut bad_vote = vote.clone();
|
||||
bad_vote.id = Keyring::Bob.public();
|
||||
let bad_vote = GossipMessage::<Block>::Vote(bad_vote).encode();
|
||||
let res = gv.validate(&mut context, &sender, &bad_vote);
|
||||
assert!(matches!(res, ValidationResult::Discard));
|
||||
expected_report.cost_benefit = cost::UNKNOWN_VOTER;
|
||||
assert_eq!(report_stream.try_recv().unwrap(), expected_report);
|
||||
|
||||
// reject if the round is not GRANDPA finalized
|
||||
gv.update_filter(GossipFilterCfg { start: 1, end: 2, validator_set: &validator_set });
|
||||
let number = vote.commitment.block_number;
|
||||
let set_id = vote.commitment.validator_set_id;
|
||||
assert!(!gv.gossip_filter.read().is_vote_accepted(number, set_id));
|
||||
|
||||
let res = gv.validate(&mut context, &sender, &vote.encode());
|
||||
assert_eq!(gv.gossip_filter.read().consider_vote(number, set_id), Consider::RejectFuture);
|
||||
let res = gv.validate(&mut context, &sender, &encoded);
|
||||
assert!(matches!(res, ValidationResult::Discard));
|
||||
expected_report.cost_benefit = cost::FUTURE_MESSAGE;
|
||||
assert_eq!(report_stream.try_recv().unwrap(), expected_report);
|
||||
|
||||
// reject if the round is not live anymore
|
||||
gv.update_filter(GossipFilterCfg { start: 7, end: 10, validator_set: &validator_set });
|
||||
let number = vote.commitment.block_number;
|
||||
let set_id = vote.commitment.validator_set_id;
|
||||
assert_eq!(gv.gossip_filter.read().consider_vote(number, set_id), Consider::RejectPast);
|
||||
let res = gv.validate(&mut context, &sender, &encoded);
|
||||
assert!(matches!(res, ValidationResult::Discard));
|
||||
expected_report.cost_benefit = cost::OUTDATED_MESSAGE;
|
||||
assert_eq!(report_stream.try_recv().unwrap(), expected_report);
|
||||
|
||||
// now verify proofs validation
|
||||
|
||||
// reject old proof
|
||||
let proof = dummy_proof(5, &validator_set);
|
||||
let encoded_proof = GossipMessage::<Block>::FinalityProof(proof).encode();
|
||||
let res = gv.validate(&mut context, &sender, &encoded_proof);
|
||||
assert!(matches!(res, ValidationResult::Discard));
|
||||
expected_report.cost_benefit = cost::OUTDATED_MESSAGE;
|
||||
assert_eq!(report_stream.try_recv().unwrap(), expected_report);
|
||||
|
||||
// accept next proof with good set_id
|
||||
let proof = dummy_proof(7, &validator_set);
|
||||
let encoded_proof = GossipMessage::<Block>::FinalityProof(proof).encode();
|
||||
let res = gv.validate(&mut context, &sender, &encoded_proof);
|
||||
assert!(matches!(res, ValidationResult::ProcessAndKeep(_)));
|
||||
expected_report.cost_benefit = benefit::VALIDATED_PROOF;
|
||||
assert_eq!(report_stream.try_recv().unwrap(), expected_report);
|
||||
|
||||
// accept future proof with good set_id
|
||||
let proof = dummy_proof(20, &validator_set);
|
||||
let encoded_proof = GossipMessage::<Block>::FinalityProof(proof).encode();
|
||||
let res = gv.validate(&mut context, &sender, &encoded_proof);
|
||||
assert!(matches!(res, ValidationResult::ProcessAndKeep(_)));
|
||||
expected_report.cost_benefit = benefit::VALIDATED_PROOF;
|
||||
assert_eq!(report_stream.try_recv().unwrap(), expected_report);
|
||||
|
||||
// reject proof, wrong set_id
|
||||
// reject proof, future set_id
|
||||
let bad_validator_set = ValidatorSet::<AuthorityId>::new(keys, 1).unwrap();
|
||||
let proof = dummy_proof(20, &bad_validator_set);
|
||||
let encoded_proof = GossipMessage::<Block>::FinalityProof(proof).encode();
|
||||
let res = gv.validate(&mut context, &sender, &encoded_proof);
|
||||
assert!(matches!(res, ValidationResult::Discard));
|
||||
expected_report.cost_benefit = cost::FUTURE_MESSAGE;
|
||||
assert_eq!(report_stream.try_recv().unwrap(), expected_report);
|
||||
|
||||
// reject proof, bad signatures (Bob instead of Alice)
|
||||
let bad_validator_set =
|
||||
@@ -552,13 +695,16 @@ pub(crate) mod tests {
|
||||
let encoded_proof = GossipMessage::<Block>::FinalityProof(proof).encode();
|
||||
let res = gv.validate(&mut context, &sender, &encoded_proof);
|
||||
assert!(matches!(res, ValidationResult::Discard));
|
||||
expected_report.cost_benefit = cost::INVALID_PROOF;
|
||||
expected_report.cost_benefit.value += cost::PER_SIGNATURE_CHECKED;
|
||||
assert_eq!(report_stream.try_recv().unwrap(), expected_report);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn messages_allowed_and_expired() {
|
||||
let keys = vec![Keyring::Alice.public()];
|
||||
let validator_set = ValidatorSet::<AuthorityId>::new(keys.clone(), 0).unwrap();
|
||||
let gv = GossipValidator::<Block>::new(Arc::new(Mutex::new(KnownPeers::new())));
|
||||
let (gv, _) = GossipValidator::<Block>::new(Arc::new(Mutex::new(KnownPeers::new())));
|
||||
gv.update_filter(GossipFilterCfg { start: 0, end: 10, validator_set: &validator_set });
|
||||
let sender = sc_network::PeerId::random();
|
||||
let topic = Default::default();
|
||||
@@ -635,7 +781,7 @@ pub(crate) mod tests {
|
||||
fn messages_rebroadcast() {
|
||||
let keys = vec![Keyring::Alice.public()];
|
||||
let validator_set = ValidatorSet::<AuthorityId>::new(keys.clone(), 0).unwrap();
|
||||
let gv = GossipValidator::<Block>::new(Arc::new(Mutex::new(KnownPeers::new())));
|
||||
let (gv, _) = GossipValidator::<Block>::new(Arc::new(Mutex::new(KnownPeers::new())));
|
||||
gv.update_filter(GossipFilterCfg { start: 0, end: 10, validator_set: &validator_set });
|
||||
let sender = sc_network::PeerId::random();
|
||||
let topic = Default::default();
|
||||
|
||||
@@ -73,6 +73,39 @@ pub fn beefy_peers_set_config(
|
||||
cfg
|
||||
}
|
||||
|
||||
// cost scalars for reporting peers.
|
||||
mod cost {
|
||||
use sc_network::ReputationChange as Rep;
|
||||
// Message that's for an outdated round.
|
||||
pub(super) const OUTDATED_MESSAGE: Rep = Rep::new(-50, "BEEFY: Past message");
|
||||
// Message that's from the future relative to our current set-id.
|
||||
pub(super) const FUTURE_MESSAGE: Rep = Rep::new(-100, "BEEFY: Future message");
|
||||
// Vote message containing bad signature.
|
||||
pub(super) const BAD_SIGNATURE: Rep = Rep::new(-100, "BEEFY: Bad signature");
|
||||
// Message received with vote from voter not in validator set.
|
||||
pub(super) const UNKNOWN_VOTER: Rep = Rep::new(-150, "BEEFY: Unknown voter");
|
||||
// A message received that cannot be evaluated relative to our current state.
|
||||
pub(super) const OUT_OF_SCOPE_MESSAGE: Rep = Rep::new(-500, "BEEFY: Out-of-scope message");
|
||||
// Message containing invalid proof.
|
||||
pub(super) const INVALID_PROOF: Rep = Rep::new(-5000, "BEEFY: Invalid commit");
|
||||
// Reputation cost per signature checked for invalid proof.
|
||||
pub(super) const PER_SIGNATURE_CHECKED: i32 = -25;
|
||||
// Reputation cost per byte for un-decodable message.
|
||||
pub(super) const PER_UNDECODABLE_BYTE: i32 = -5;
|
||||
// On-demand request was refused by peer.
|
||||
pub(super) const REFUSAL_RESPONSE: Rep = Rep::new(-100, "BEEFY: Proof request refused");
|
||||
// On-demand request for a proof that can't be found in the backend.
|
||||
pub(super) const UNKOWN_PROOF_REQUEST: Rep = Rep::new(-150, "BEEFY: Unknown proof request");
|
||||
}
|
||||
|
||||
// benefit scalars for reporting peers.
|
||||
mod benefit {
|
||||
use sc_network::ReputationChange as Rep;
|
||||
pub(super) const VOTE_MESSAGE: Rep = Rep::new(100, "BEEFY: Round vote message");
|
||||
pub(super) const KNOWN_VOTE_MESSAGE: Rep = Rep::new(50, "BEEFY: Known vote");
|
||||
pub(super) const VALIDATED_PROOF: Rep = Rep::new(100, "BEEFY: Justification");
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -18,13 +18,17 @@
|
||||
|
||||
//! Logic for keeping track of BEEFY peers.
|
||||
|
||||
// TODO (issue #12296): replace this naive peer tracking with generic one that infers data
|
||||
// from multiple network protocols.
|
||||
|
||||
use sc_network::PeerId;
|
||||
use sc_network::{PeerId, ReputationChange};
|
||||
use sp_runtime::traits::{Block, NumberFor, Zero};
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
|
||||
/// Report specifying a reputation change for a given peer.
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub(crate) struct PeerReport {
|
||||
pub who: PeerId,
|
||||
pub cost_benefit: ReputationChange,
|
||||
}
|
||||
|
||||
struct PeerData<B: Block> {
|
||||
last_voted_on: NumberFor<B>,
|
||||
}
|
||||
|
||||
+37
-27
@@ -32,9 +32,12 @@ use sp_runtime::traits::Block;
|
||||
use std::{marker::PhantomData, sync::Arc};
|
||||
|
||||
use crate::{
|
||||
communication::request_response::{
|
||||
on_demand_justifications_protocol_config, Error, JustificationRequest,
|
||||
BEEFY_SYNC_LOG_TARGET,
|
||||
communication::{
|
||||
cost,
|
||||
request_response::{
|
||||
on_demand_justifications_protocol_config, Error, JustificationRequest,
|
||||
BEEFY_SYNC_LOG_TARGET,
|
||||
},
|
||||
},
|
||||
metric_inc,
|
||||
metrics::{register_metrics, OnDemandIncomingRequestsMetrics},
|
||||
@@ -69,17 +72,20 @@ impl<B: Block> IncomingRequest<B> {
|
||||
/// Params:
|
||||
/// - The raw request to decode
|
||||
/// - Reputation changes to apply for the peer in case decoding fails.
|
||||
pub fn try_from_raw(
|
||||
pub fn try_from_raw<F>(
|
||||
raw: netconfig::IncomingRequest,
|
||||
reputation_changes: Vec<ReputationChange>,
|
||||
) -> Result<Self, Error> {
|
||||
reputation_changes_on_err: F,
|
||||
) -> Result<Self, Error>
|
||||
where
|
||||
F: FnOnce(usize) -> Vec<ReputationChange>,
|
||||
{
|
||||
let netconfig::IncomingRequest { payload, peer, pending_response } = raw;
|
||||
let payload = match JustificationRequest::decode(&mut payload.as_ref()) {
|
||||
Ok(payload) => payload,
|
||||
Err(err) => {
|
||||
let response = netconfig::OutgoingResponse {
|
||||
result: Err(()),
|
||||
reputation_changes,
|
||||
reputation_changes: reputation_changes_on_err(payload.len()),
|
||||
sent_feedback: None,
|
||||
};
|
||||
if let Err(_) = pending_response.send(response) {
|
||||
@@ -111,11 +117,11 @@ impl IncomingRequestReceiver {
|
||||
pub async fn recv<B, F>(&mut self, reputation_changes: F) -> Result<IncomingRequest<B>, Error>
|
||||
where
|
||||
B: Block,
|
||||
F: FnOnce() -> Vec<ReputationChange>,
|
||||
F: FnOnce(usize) -> Vec<ReputationChange>,
|
||||
{
|
||||
let req = match self.raw.next().await {
|
||||
None => return Err(Error::RequestChannelExhausted),
|
||||
Some(raw) => IncomingRequest::<B>::try_from_raw(raw, reputation_changes())?,
|
||||
Some(raw) => IncomingRequest::<B>::try_from_raw(raw, reputation_changes)?,
|
||||
};
|
||||
Ok(req)
|
||||
}
|
||||
@@ -159,26 +165,20 @@ where
|
||||
|
||||
// Sends back justification response if justification found in client backend.
|
||||
fn handle_request(&self, request: IncomingRequest<B>) -> Result<(), Error> {
|
||||
// TODO (issue #12293): validate `request` and change peer reputation for invalid requests.
|
||||
|
||||
let maybe_encoded_proof = if let Some(hash) =
|
||||
self.client.block_hash(request.payload.begin).map_err(Error::Client)?
|
||||
{
|
||||
self.client
|
||||
.justifications(hash)
|
||||
.map_err(Error::Client)?
|
||||
.and_then(|justifs| justifs.get(BEEFY_ENGINE_ID).cloned())
|
||||
// No BEEFY justification present.
|
||||
.ok_or(())
|
||||
} else {
|
||||
Err(())
|
||||
};
|
||||
|
||||
let mut reputation_changes = vec![];
|
||||
let maybe_encoded_proof = self
|
||||
.client
|
||||
.block_hash(request.payload.begin)
|
||||
.ok()
|
||||
.flatten()
|
||||
.and_then(|hash| self.client.justifications(hash).ok().flatten())
|
||||
.and_then(|justifs| justifs.get(BEEFY_ENGINE_ID).cloned())
|
||||
.ok_or_else(|| reputation_changes.push(cost::UNKOWN_PROOF_REQUEST));
|
||||
request
|
||||
.pending_response
|
||||
.send(netconfig::OutgoingResponse {
|
||||
result: maybe_encoded_proof,
|
||||
reputation_changes: Vec::new(),
|
||||
reputation_changes,
|
||||
sent_feedback: None,
|
||||
})
|
||||
.map_err(|_| Error::SendResponse)
|
||||
@@ -188,7 +188,17 @@ where
|
||||
pub async fn run(mut self) {
|
||||
trace!(target: BEEFY_SYNC_LOG_TARGET, "🥩 Running BeefyJustifsRequestHandler");
|
||||
|
||||
while let Ok(request) = self.request_receiver.recv(|| vec![]).await {
|
||||
while let Ok(request) = self
|
||||
.request_receiver
|
||||
.recv(|bytes| {
|
||||
let bytes = bytes.min(i32::MAX as usize) as i32;
|
||||
vec![ReputationChange::new(
|
||||
bytes.saturating_mul(cost::PER_UNDECODABLE_BYTE),
|
||||
"BEEFY: Bad request payload",
|
||||
)]
|
||||
})
|
||||
.await
|
||||
{
|
||||
let peer = request.peer;
|
||||
match self.handle_request(request) {
|
||||
Ok(()) => {
|
||||
@@ -199,8 +209,8 @@ where
|
||||
)
|
||||
},
|
||||
Err(e) => {
|
||||
// peer reputation changes already applied in `self.handle_request()`
|
||||
metric_inc!(self, beefy_failed_justification_responses);
|
||||
// TODO (issue #12293): apply reputation changes here based on error type.
|
||||
debug!(
|
||||
target: BEEFY_SYNC_LOG_TARGET,
|
||||
"🥩 Failed to handle BEEFY justification request from {:?}: {}", peer, e,
|
||||
|
||||
@@ -30,7 +30,7 @@ use codec::{Decode, Encode, Error as CodecError};
|
||||
use sc_network::{config::RequestResponseConfig, PeerId};
|
||||
use sp_runtime::traits::{Block, NumberFor};
|
||||
|
||||
use crate::communication::beefy_protocol_name::justifications_protocol_name;
|
||||
use crate::communication::{beefy_protocol_name::justifications_protocol_name, peers::PeerReport};
|
||||
use incoming_requests_handler::IncomingRequestReceiver;
|
||||
|
||||
// 10 seems reasonable, considering justifs are explicitly requested only
|
||||
@@ -76,7 +76,7 @@ pub struct JustificationRequest<B: Block> {
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
pub(crate) enum Error {
|
||||
#[error(transparent)]
|
||||
Client(#[from] sp_blockchain::Error),
|
||||
|
||||
@@ -99,5 +99,8 @@ pub enum Error {
|
||||
SendResponse,
|
||||
|
||||
#[error("Received invalid response.")]
|
||||
InvalidResponse,
|
||||
InvalidResponse(PeerReport),
|
||||
|
||||
#[error("Internal error while getting response.")]
|
||||
ResponseError,
|
||||
}
|
||||
|
||||
+52
-24
@@ -31,7 +31,11 @@ use sp_runtime::traits::{Block, NumberFor};
|
||||
use std::{collections::VecDeque, result::Result, sync::Arc};
|
||||
|
||||
use crate::{
|
||||
communication::request_response::{Error, JustificationRequest, BEEFY_SYNC_LOG_TARGET},
|
||||
communication::{
|
||||
benefit, cost,
|
||||
peers::PeerReport,
|
||||
request_response::{Error, JustificationRequest, BEEFY_SYNC_LOG_TARGET},
|
||||
},
|
||||
justification::{decode_and_verify_finality_proof, BeefyVersionedFinalityProof},
|
||||
metric_inc,
|
||||
metrics::{register_metrics, OnDemandOutgoingRequestsMetrics},
|
||||
@@ -54,6 +58,16 @@ enum State<B: Block> {
|
||||
AwaitingResponse(PeerId, RequestInfo<B>, ResponseReceiver),
|
||||
}
|
||||
|
||||
/// Possible engine responses.
|
||||
pub(crate) enum ResponseInfo<B: Block> {
|
||||
/// No peer response available yet.
|
||||
Pending,
|
||||
/// Valid justification provided alongside peer reputation changes.
|
||||
ValidProof(BeefyVersionedFinalityProof<B>, PeerReport),
|
||||
/// No justification yet, only peer reputation changes.
|
||||
PeerReport(PeerReport),
|
||||
}
|
||||
|
||||
pub struct OnDemandJustificationsEngine<B: Block> {
|
||||
network: Arc<dyn NetworkRequest + Send + Sync>,
|
||||
protocol_name: ProtocolName,
|
||||
@@ -84,12 +98,10 @@ impl<B: Block> OnDemandJustificationsEngine<B> {
|
||||
}
|
||||
|
||||
fn reset_peers_cache_for_block(&mut self, block: NumberFor<B>) {
|
||||
// TODO (issue #12296): replace peer selection with generic one that involves all protocols.
|
||||
self.peers_cache = self.live_peers.lock().further_than(block);
|
||||
}
|
||||
|
||||
fn try_next_peer(&mut self) -> Option<PeerId> {
|
||||
// TODO (issue #12296): replace peer selection with generic one that involves all protocols.
|
||||
let live = self.live_peers.lock();
|
||||
while let Some(peer) = self.peers_cache.pop_front() {
|
||||
if live.contains(&peer) {
|
||||
@@ -159,24 +171,19 @@ impl<B: Block> OnDemandJustificationsEngine<B> {
|
||||
|
||||
fn process_response(
|
||||
&mut self,
|
||||
peer: PeerId,
|
||||
peer: &PeerId,
|
||||
req_info: &RequestInfo<B>,
|
||||
response: Result<Response, Canceled>,
|
||||
) -> Result<BeefyVersionedFinalityProof<B>, Error> {
|
||||
response
|
||||
.map_err(|e| {
|
||||
metric_inc!(self, beefy_on_demand_justification_peer_hang_up);
|
||||
debug!(
|
||||
target: BEEFY_SYNC_LOG_TARGET,
|
||||
"🥩 for on demand justification #{:?}, peer {:?} hung up: {:?}",
|
||||
req_info.block,
|
||||
peer,
|
||||
e
|
||||
"🥩 on-demand sc-network channel sender closed, err: {:?}", e
|
||||
);
|
||||
Error::InvalidResponse
|
||||
Error::ResponseError
|
||||
})?
|
||||
.map_err(|e| {
|
||||
metric_inc!(self, beefy_on_demand_justification_peer_error);
|
||||
debug!(
|
||||
target: BEEFY_SYNC_LOG_TARGET,
|
||||
"🥩 for on demand justification #{:?}, peer {:?} error: {:?}",
|
||||
@@ -184,7 +191,18 @@ impl<B: Block> OnDemandJustificationsEngine<B> {
|
||||
peer,
|
||||
e
|
||||
);
|
||||
Error::InvalidResponse
|
||||
match e {
|
||||
RequestFailure::Refused => {
|
||||
metric_inc!(self, beefy_on_demand_justification_peer_refused);
|
||||
let peer_report =
|
||||
PeerReport { who: *peer, cost_benefit: cost::REFUSAL_RESPONSE };
|
||||
Error::InvalidResponse(peer_report)
|
||||
},
|
||||
_ => {
|
||||
metric_inc!(self, beefy_on_demand_justification_peer_error);
|
||||
Error::ResponseError
|
||||
},
|
||||
}
|
||||
})
|
||||
.and_then(|encoded| {
|
||||
decode_and_verify_finality_proof::<B>(
|
||||
@@ -192,23 +210,26 @@ impl<B: Block> OnDemandJustificationsEngine<B> {
|
||||
req_info.block,
|
||||
&req_info.active_set,
|
||||
)
|
||||
.map_err(|e| {
|
||||
.map_err(|(err, signatures_checked)| {
|
||||
metric_inc!(self, beefy_on_demand_justification_invalid_proof);
|
||||
debug!(
|
||||
target: BEEFY_SYNC_LOG_TARGET,
|
||||
"🥩 for on demand justification #{:?}, peer {:?} responded with invalid proof: {:?}",
|
||||
req_info.block, peer, e
|
||||
req_info.block, peer, err
|
||||
);
|
||||
Error::InvalidResponse
|
||||
let mut cost = cost::INVALID_PROOF;
|
||||
cost.value +=
|
||||
cost::PER_SIGNATURE_CHECKED.saturating_mul(signatures_checked as i32);
|
||||
Error::InvalidResponse(PeerReport { who: *peer, cost_benefit: cost })
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn next(&mut self) -> Option<BeefyVersionedFinalityProof<B>> {
|
||||
pub(crate) async fn next(&mut self) -> ResponseInfo<B> {
|
||||
let (peer, req_info, resp) = match &mut self.state {
|
||||
State::Idle => {
|
||||
futures::future::pending::<()>().await;
|
||||
return None
|
||||
return ResponseInfo::Pending
|
||||
},
|
||||
State::AwaitingResponse(peer, req_info, receiver) => {
|
||||
let resp = receiver.await;
|
||||
@@ -220,8 +241,8 @@ impl<B: Block> OnDemandJustificationsEngine<B> {
|
||||
self.state = State::Idle;
|
||||
|
||||
let block = req_info.block;
|
||||
self.process_response(peer, &req_info, resp)
|
||||
.map_err(|_| {
|
||||
match self.process_response(&peer, &req_info, resp) {
|
||||
Err(err) => {
|
||||
// No valid justification received, try next peer in our set.
|
||||
if let Some(peer) = self.try_next_peer() {
|
||||
self.request_from_peer(peer, req_info);
|
||||
@@ -231,15 +252,22 @@ impl<B: Block> OnDemandJustificationsEngine<B> {
|
||||
"🥩 ran out of peers to request justif #{:?} from", block
|
||||
);
|
||||
}
|
||||
})
|
||||
.map(|proof| {
|
||||
// Report peer based on error type.
|
||||
if let Error::InvalidResponse(peer_report) = err {
|
||||
ResponseInfo::PeerReport(peer_report)
|
||||
} else {
|
||||
ResponseInfo::Pending
|
||||
}
|
||||
},
|
||||
Ok(proof) => {
|
||||
metric_inc!(self, beefy_on_demand_justification_good_proof);
|
||||
debug!(
|
||||
target: BEEFY_SYNC_LOG_TARGET,
|
||||
"🥩 received valid on-demand justif #{:?} from {:?}", block, peer
|
||||
);
|
||||
proof
|
||||
})
|
||||
.ok()
|
||||
let peer_report = PeerReport { who: peer, cost_benefit: benefit::VALIDATED_PROOF };
|
||||
ResponseInfo::ValidProof(proof, peer_report)
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user