mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-04-25 19:57:56 +00:00
60e537b95f
First step in implementing https://github.com/paritytech/polkadot-sdk/issues/3144 ### Summary of changes - switch statement `Table` candidate mapping from `ParaId` to `CoreIndex` - introduce experimental `InjectCoreIndex` node feature. - determine and assume a `CoreIndex` for a candidate based on statement validator index. If the signature is valid it means validator controls the validator that index and we can easily map it to a validator group/core. - introduce a temporary provisioner fix until we fully enable elastic scaling in the subystem. The fix ensures we don't fetch the same backable candidate when calling `get_backable_candidate` for each core. TODO: - [x] fix backing tests - [x] fix statement table tests - [x] add new test --------- Signed-off-by: Andrei Sandu <andrei-mihail@parity.io> Signed-off-by: alindima <alin@parity.io> Co-authored-by: alindima <alin@parity.io>
989 lines
29 KiB
Rust
989 lines
29 KiB
Rust
// Copyright (C) Parity Technologies (UK) Ltd.
|
|
// This file is part of Polkadot.
|
|
|
|
// 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 <http://www.gnu.org/licenses/>.
|
|
|
|
//! The statement table: generic implementation.
|
|
//!
|
|
//! This stores messages other authorities issue about candidates.
|
|
//!
|
|
//! These messages are used to create a proposal submitted to a BFT consensus process.
|
|
//!
|
|
//! Each parachain is associated with a committee of authorities, who issue statements
|
|
//! indicating whether the candidate is valid or invalid. Once a threshold of the committee
|
|
//! has signed validity statements, the candidate may be marked includable.
|
|
|
|
use std::{
|
|
collections::hash_map::{self, Entry, HashMap},
|
|
fmt::Debug,
|
|
hash::Hash,
|
|
};
|
|
|
|
use primitives::{
|
|
effective_minimum_backing_votes, ValidatorSignature,
|
|
ValidityAttestation as PrimitiveValidityAttestation,
|
|
};
|
|
|
|
use parity_scale_codec::{Decode, Encode};
|
|
const LOG_TARGET: &str = "parachain::statement-table";
|
|
|
|
/// Context for the statement table.
|
|
pub trait Context {
|
|
/// An authority ID
|
|
type AuthorityId: Debug + Hash + Eq + Clone;
|
|
/// The digest (hash or other unique attribute) of a candidate.
|
|
type Digest: Debug + Hash + Eq + Clone;
|
|
/// The group ID type
|
|
type GroupId: Debug + Hash + Ord + Eq + Clone;
|
|
/// A signature type.
|
|
type Signature: Debug + Eq + Clone;
|
|
/// Candidate type. In practice this will be a candidate receipt.
|
|
type Candidate: Debug + Ord + Eq + Clone;
|
|
|
|
/// get the digest of a candidate.
|
|
fn candidate_digest(candidate: &Self::Candidate) -> Self::Digest;
|
|
|
|
/// Whether a authority is a member of a group.
|
|
/// Members are meant to submit candidates and vote on validity.
|
|
fn is_member_of(&self, authority: &Self::AuthorityId, group: &Self::GroupId) -> bool;
|
|
|
|
/// Get a validator group size.
|
|
fn get_group_size(&self, group: &Self::GroupId) -> Option<usize>;
|
|
}
|
|
|
|
/// Table configuration.
|
|
pub struct Config {
|
|
/// When this is true, the table will allow multiple seconded candidates
|
|
/// per authority. This flag means that higher-level code is responsible for
|
|
/// bounding the number of candidates.
|
|
pub allow_multiple_seconded: bool,
|
|
}
|
|
|
|
/// Statements circulated among peers.
|
|
#[derive(PartialEq, Eq, Debug, Clone, Encode, Decode)]
|
|
pub enum Statement<Candidate, Digest> {
|
|
/// Broadcast by an authority to indicate that this is its candidate for inclusion.
|
|
///
|
|
/// Broadcasting two different candidate messages per round is not allowed.
|
|
#[codec(index = 1)]
|
|
Seconded(Candidate),
|
|
/// Broadcast by a authority to attest that the candidate with given digest is valid.
|
|
#[codec(index = 2)]
|
|
Valid(Digest),
|
|
}
|
|
|
|
/// A signed statement.
|
|
#[derive(PartialEq, Eq, Debug, Clone, Encode, Decode)]
|
|
pub struct SignedStatement<Candidate, Digest, AuthorityId, Signature> {
|
|
/// The statement.
|
|
pub statement: Statement<Candidate, Digest>,
|
|
/// The signature.
|
|
pub signature: Signature,
|
|
/// The sender.
|
|
pub sender: AuthorityId,
|
|
}
|
|
|
|
/// Misbehavior: voting more than one way on candidate validity.
|
|
///
|
|
/// Since there are three possible ways to vote, a double vote is possible in
|
|
/// three possible combinations (unordered)
|
|
#[derive(PartialEq, Eq, Debug, Clone)]
|
|
pub enum ValidityDoubleVote<Candidate, Digest, Signature> {
|
|
/// Implicit vote by issuing and explicitly voting validity.
|
|
IssuedAndValidity((Candidate, Signature), (Digest, Signature)),
|
|
}
|
|
|
|
impl<Candidate, Digest, Signature> ValidityDoubleVote<Candidate, Digest, Signature> {
|
|
/// Deconstruct this misbehavior into two `(Statement, Signature)` pairs, erasing the
|
|
/// information about precisely what the problem was.
|
|
pub fn deconstruct<Ctx>(
|
|
self,
|
|
) -> ((Statement<Candidate, Digest>, Signature), (Statement<Candidate, Digest>, Signature))
|
|
where
|
|
Ctx: Context<Candidate = Candidate, Digest = Digest, Signature = Signature>,
|
|
Candidate: Debug + Ord + Eq + Clone,
|
|
Digest: Debug + Hash + Eq + Clone,
|
|
Signature: Debug + Eq + Clone,
|
|
{
|
|
match self {
|
|
Self::IssuedAndValidity((c, s1), (d, s2)) =>
|
|
((Statement::Seconded(c), s1), (Statement::Valid(d), s2)),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Misbehavior: multiple signatures on same statement.
|
|
#[derive(PartialEq, Eq, Debug, Clone)]
|
|
pub enum DoubleSign<Candidate, Digest, Signature> {
|
|
/// On candidate.
|
|
Seconded(Candidate, Signature, Signature),
|
|
/// On validity.
|
|
Validity(Digest, Signature, Signature),
|
|
}
|
|
|
|
impl<Candidate, Digest, Signature> DoubleSign<Candidate, Digest, Signature> {
|
|
/// Deconstruct this misbehavior into a statement with two signatures, erasing the information
|
|
/// about precisely where in the process the issue was detected.
|
|
pub fn deconstruct(self) -> (Statement<Candidate, Digest>, Signature, Signature) {
|
|
match self {
|
|
Self::Seconded(candidate, a, b) => (Statement::Seconded(candidate), a, b),
|
|
Self::Validity(digest, a, b) => (Statement::Valid(digest), a, b),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Misbehavior: declaring multiple candidates.
|
|
#[derive(PartialEq, Eq, Debug, Clone)]
|
|
pub struct MultipleCandidates<Candidate, Signature> {
|
|
/// The first candidate seen.
|
|
pub first: (Candidate, Signature),
|
|
/// The second candidate seen.
|
|
pub second: (Candidate, Signature),
|
|
}
|
|
|
|
/// Misbehavior: submitted statement for wrong group.
|
|
#[derive(PartialEq, Eq, Debug, Clone)]
|
|
pub struct UnauthorizedStatement<Candidate, Digest, AuthorityId, Signature> {
|
|
/// A signed statement which was submitted without proper authority.
|
|
pub statement: SignedStatement<Candidate, Digest, AuthorityId, Signature>,
|
|
}
|
|
|
|
/// Different kinds of misbehavior. All of these kinds of malicious misbehavior
|
|
/// are easily provable and extremely disincentivized.
|
|
#[derive(PartialEq, Eq, Debug, Clone)]
|
|
pub enum Misbehavior<Candidate, Digest, AuthorityId, Signature> {
|
|
/// Voted invalid and valid on validity.
|
|
ValidityDoubleVote(ValidityDoubleVote<Candidate, Digest, Signature>),
|
|
/// Submitted multiple candidates.
|
|
MultipleCandidates(MultipleCandidates<Candidate, Signature>),
|
|
/// Submitted a message that was unauthorized.
|
|
UnauthorizedStatement(UnauthorizedStatement<Candidate, Digest, AuthorityId, Signature>),
|
|
/// Submitted two valid signatures for the same message.
|
|
DoubleSign(DoubleSign<Candidate, Digest, Signature>),
|
|
}
|
|
|
|
/// Type alias for misbehavior corresponding to context type.
|
|
pub type MisbehaviorFor<Ctx> = Misbehavior<
|
|
<Ctx as Context>::Candidate,
|
|
<Ctx as Context>::Digest,
|
|
<Ctx as Context>::AuthorityId,
|
|
<Ctx as Context>::Signature,
|
|
>;
|
|
|
|
// Kinds of votes for validity on a particular candidate.
|
|
#[derive(Clone, PartialEq, Eq)]
|
|
enum ValidityVote<Signature: Eq + Clone> {
|
|
// Implicit validity vote.
|
|
Issued(Signature),
|
|
// Direct validity vote.
|
|
Valid(Signature),
|
|
}
|
|
|
|
/// A summary of import of a statement.
|
|
#[derive(Clone, PartialEq, Eq, Debug)]
|
|
pub struct Summary<Digest, Group> {
|
|
/// The digest of the candidate referenced.
|
|
pub candidate: Digest,
|
|
/// The group that the candidate is in.
|
|
pub group_id: Group,
|
|
/// How many validity votes are currently witnessed.
|
|
pub validity_votes: usize,
|
|
}
|
|
|
|
/// A validity attestation.
|
|
#[derive(Clone, PartialEq, Decode, Encode)]
|
|
pub enum ValidityAttestation<Signature> {
|
|
/// implicit validity attestation by issuing.
|
|
/// This corresponds to issuance of a `Candidate` statement.
|
|
Implicit(Signature),
|
|
/// An explicit attestation. This corresponds to issuance of a
|
|
/// `Valid` statement.
|
|
Explicit(Signature),
|
|
}
|
|
|
|
impl Into<PrimitiveValidityAttestation> for ValidityAttestation<ValidatorSignature> {
|
|
fn into(self) -> PrimitiveValidityAttestation {
|
|
match self {
|
|
Self::Implicit(s) => PrimitiveValidityAttestation::Implicit(s),
|
|
Self::Explicit(s) => PrimitiveValidityAttestation::Explicit(s),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// An attested-to candidate.
|
|
#[derive(Clone, PartialEq, Decode, Encode)]
|
|
pub struct AttestedCandidate<Group, Candidate, AuthorityId, Signature> {
|
|
/// The group ID that the candidate is in.
|
|
pub group_id: Group,
|
|
/// The candidate data.
|
|
pub candidate: Candidate,
|
|
/// Validity attestations.
|
|
pub validity_votes: Vec<(AuthorityId, ValidityAttestation<Signature>)>,
|
|
}
|
|
|
|
/// Stores votes and data about a candidate.
|
|
pub struct CandidateData<Ctx: Context> {
|
|
group_id: Ctx::GroupId,
|
|
candidate: Ctx::Candidate,
|
|
validity_votes: HashMap<Ctx::AuthorityId, ValidityVote<Ctx::Signature>>,
|
|
}
|
|
|
|
impl<Ctx: Context> CandidateData<Ctx> {
|
|
/// Yield a full attestation for a candidate.
|
|
/// If the candidate can be included, it will return `Some`.
|
|
pub fn attested(
|
|
&self,
|
|
validity_threshold: usize,
|
|
) -> Option<AttestedCandidate<Ctx::GroupId, Ctx::Candidate, Ctx::AuthorityId, Ctx::Signature>> {
|
|
let valid_votes = self.validity_votes.len();
|
|
if valid_votes < validity_threshold {
|
|
return None
|
|
}
|
|
|
|
let validity_votes = self
|
|
.validity_votes
|
|
.iter()
|
|
.map(|(a, v)| match *v {
|
|
ValidityVote::Valid(ref s) => (a.clone(), ValidityAttestation::Explicit(s.clone())),
|
|
ValidityVote::Issued(ref s) =>
|
|
(a.clone(), ValidityAttestation::Implicit(s.clone())),
|
|
})
|
|
.collect();
|
|
|
|
Some(AttestedCandidate {
|
|
group_id: self.group_id.clone(),
|
|
candidate: self.candidate.clone(),
|
|
validity_votes,
|
|
})
|
|
}
|
|
|
|
fn summary(&self, digest: Ctx::Digest) -> Summary<Ctx::Digest, Ctx::GroupId> {
|
|
Summary {
|
|
candidate: digest,
|
|
group_id: self.group_id.clone(),
|
|
validity_votes: self.validity_votes.len(),
|
|
}
|
|
}
|
|
}
|
|
|
|
// authority metadata
|
|
struct AuthorityData<Ctx: Context> {
|
|
proposals: Vec<(Ctx::Digest, Ctx::Signature)>,
|
|
}
|
|
|
|
impl<Ctx: Context> Default for AuthorityData<Ctx> {
|
|
fn default() -> Self {
|
|
AuthorityData { proposals: Vec::new() }
|
|
}
|
|
}
|
|
|
|
/// Type alias for the result of a statement import.
|
|
pub type ImportResult<Ctx> = Result<
|
|
Option<Summary<<Ctx as Context>::Digest, <Ctx as Context>::GroupId>>,
|
|
MisbehaviorFor<Ctx>,
|
|
>;
|
|
|
|
/// Stores votes
|
|
pub struct Table<Ctx: Context> {
|
|
authority_data: HashMap<Ctx::AuthorityId, AuthorityData<Ctx>>,
|
|
detected_misbehavior: HashMap<Ctx::AuthorityId, Vec<MisbehaviorFor<Ctx>>>,
|
|
candidate_votes: HashMap<Ctx::Digest, CandidateData<Ctx>>,
|
|
config: Config,
|
|
}
|
|
|
|
impl<Ctx: Context> Table<Ctx> {
|
|
/// Create a new `Table` from a `Config`.
|
|
pub fn new(config: Config) -> Self {
|
|
Table {
|
|
authority_data: HashMap::default(),
|
|
detected_misbehavior: HashMap::default(),
|
|
candidate_votes: HashMap::default(),
|
|
config,
|
|
}
|
|
}
|
|
|
|
/// Get the attested candidate for `digest`.
|
|
///
|
|
/// Returns `Some(_)` if the candidate exists and is includable.
|
|
pub fn attested_candidate(
|
|
&self,
|
|
digest: &Ctx::Digest,
|
|
context: &Ctx,
|
|
minimum_backing_votes: u32,
|
|
) -> Option<AttestedCandidate<Ctx::GroupId, Ctx::Candidate, Ctx::AuthorityId, Ctx::Signature>> {
|
|
self.candidate_votes.get(digest).and_then(|data| {
|
|
let v_threshold = context.get_group_size(&data.group_id).map_or(usize::MAX, |len| {
|
|
effective_minimum_backing_votes(len, minimum_backing_votes)
|
|
});
|
|
data.attested(v_threshold)
|
|
})
|
|
}
|
|
|
|
/// Import a signed statement. Signatures should be checked for validity, and the
|
|
/// sender should be checked to actually be an authority.
|
|
///
|
|
/// Validity and invalidity statements are only valid if the corresponding
|
|
/// candidate has already been imported.
|
|
///
|
|
/// If this returns `None`, the statement was either duplicate or invalid.
|
|
pub fn import_statement(
|
|
&mut self,
|
|
context: &Ctx,
|
|
group_id: Ctx::GroupId,
|
|
statement: SignedStatement<Ctx::Candidate, Ctx::Digest, Ctx::AuthorityId, Ctx::Signature>,
|
|
) -> Option<Summary<Ctx::Digest, Ctx::GroupId>> {
|
|
let SignedStatement { statement, signature, sender: signer } = statement;
|
|
let res = match statement {
|
|
Statement::Seconded(candidate) =>
|
|
self.import_candidate(context, signer.clone(), candidate, signature, group_id),
|
|
Statement::Valid(digest) =>
|
|
self.validity_vote(context, signer.clone(), digest, ValidityVote::Valid(signature)),
|
|
};
|
|
|
|
match res {
|
|
Ok(maybe_summary) => maybe_summary,
|
|
Err(misbehavior) => {
|
|
// all misbehavior in agreement is provable and actively malicious.
|
|
// punishments may be cumulative.
|
|
self.detected_misbehavior.entry(signer).or_default().push(misbehavior);
|
|
None
|
|
},
|
|
}
|
|
}
|
|
|
|
/// Get a candidate by digest.
|
|
pub fn get_candidate(&self, digest: &Ctx::Digest) -> Option<&Ctx::Candidate> {
|
|
self.candidate_votes.get(digest).map(|d| &d.candidate)
|
|
}
|
|
|
|
/// Access all witnessed misbehavior.
|
|
pub fn get_misbehavior(&self) -> &HashMap<Ctx::AuthorityId, Vec<MisbehaviorFor<Ctx>>> {
|
|
&self.detected_misbehavior
|
|
}
|
|
|
|
/// Create a draining iterator of misbehaviors.
|
|
///
|
|
/// This consumes all detected misbehaviors, even if the iterator is not completely consumed.
|
|
pub fn drain_misbehaviors(&mut self) -> DrainMisbehaviors<'_, Ctx> {
|
|
self.detected_misbehavior.drain().into()
|
|
}
|
|
|
|
fn import_candidate(
|
|
&mut self,
|
|
context: &Ctx,
|
|
authority: Ctx::AuthorityId,
|
|
candidate: Ctx::Candidate,
|
|
signature: Ctx::Signature,
|
|
group: Ctx::GroupId,
|
|
) -> ImportResult<Ctx> {
|
|
if !context.is_member_of(&authority, &group) {
|
|
gum::debug!(target: LOG_TARGET, authority = ?authority, group = ?group, "New `Misbehavior::UnauthorizedStatement`, candidate backed by validator that doesn't belong to expected group" );
|
|
return Err(Misbehavior::UnauthorizedStatement(UnauthorizedStatement {
|
|
statement: SignedStatement {
|
|
signature,
|
|
statement: Statement::Seconded(candidate),
|
|
sender: authority,
|
|
},
|
|
}))
|
|
}
|
|
|
|
// check that authority hasn't already specified another candidate.
|
|
let digest = Ctx::candidate_digest(&candidate);
|
|
|
|
let new_proposal = match self.authority_data.entry(authority.clone()) {
|
|
Entry::Occupied(mut occ) => {
|
|
// if digest is different, fetch candidate and
|
|
// note misbehavior.
|
|
let existing = occ.get_mut();
|
|
|
|
if !self.config.allow_multiple_seconded && existing.proposals.len() == 1 {
|
|
let (old_digest, old_sig) = &existing.proposals[0];
|
|
|
|
if old_digest != &digest {
|
|
const EXISTENCE_PROOF: &str =
|
|
"when proposal first received from authority, candidate \
|
|
votes entry is created. proposal here is `Some`, therefore \
|
|
candidate votes entry exists; qed";
|
|
|
|
let old_candidate = self
|
|
.candidate_votes
|
|
.get(old_digest)
|
|
.expect(EXISTENCE_PROOF)
|
|
.candidate
|
|
.clone();
|
|
|
|
return Err(Misbehavior::MultipleCandidates(MultipleCandidates {
|
|
first: (old_candidate, old_sig.clone()),
|
|
second: (candidate, signature.clone()),
|
|
}))
|
|
}
|
|
|
|
false
|
|
} else if self.config.allow_multiple_seconded &&
|
|
existing.proposals.iter().any(|(ref od, _)| od == &digest)
|
|
{
|
|
false
|
|
} else {
|
|
existing.proposals.push((digest.clone(), signature.clone()));
|
|
true
|
|
}
|
|
},
|
|
Entry::Vacant(vacant) => {
|
|
vacant
|
|
.insert(AuthorityData { proposals: vec![(digest.clone(), signature.clone())] });
|
|
true
|
|
},
|
|
};
|
|
|
|
// NOTE: altering this code may affect the existence proof above. ensure it remains
|
|
// valid.
|
|
if new_proposal {
|
|
self.candidate_votes
|
|
.entry(digest.clone())
|
|
.or_insert_with(move || CandidateData {
|
|
group_id: group,
|
|
candidate,
|
|
validity_votes: HashMap::new(),
|
|
});
|
|
}
|
|
|
|
self.validity_vote(context, authority, digest, ValidityVote::Issued(signature))
|
|
}
|
|
|
|
fn validity_vote(
|
|
&mut self,
|
|
context: &Ctx,
|
|
from: Ctx::AuthorityId,
|
|
digest: Ctx::Digest,
|
|
vote: ValidityVote<Ctx::Signature>,
|
|
) -> ImportResult<Ctx> {
|
|
let votes = match self.candidate_votes.get_mut(&digest) {
|
|
None => return Ok(None),
|
|
Some(votes) => votes,
|
|
};
|
|
|
|
// check that this authority actually can vote in this group.
|
|
if !context.is_member_of(&from, &votes.group_id) {
|
|
let sig = match vote {
|
|
ValidityVote::Valid(s) => s,
|
|
ValidityVote::Issued(_) => panic!(
|
|
"implicit issuance vote only cast from `import_candidate` after \
|
|
checking group membership of issuer; qed"
|
|
),
|
|
};
|
|
|
|
return Err(Misbehavior::UnauthorizedStatement(UnauthorizedStatement {
|
|
statement: SignedStatement {
|
|
signature: sig,
|
|
sender: from,
|
|
statement: Statement::Valid(digest),
|
|
},
|
|
}))
|
|
}
|
|
|
|
// check for double votes.
|
|
match votes.validity_votes.entry(from.clone()) {
|
|
Entry::Occupied(occ) => {
|
|
let make_vdv = |v| Misbehavior::ValidityDoubleVote(v);
|
|
let make_ds = |ds| Misbehavior::DoubleSign(ds);
|
|
return if occ.get() != &vote {
|
|
Err(match (occ.get().clone(), vote) {
|
|
// valid vote conflicting with candidate statement
|
|
(ValidityVote::Issued(iss), ValidityVote::Valid(good)) |
|
|
(ValidityVote::Valid(good), ValidityVote::Issued(iss)) =>
|
|
make_vdv(ValidityDoubleVote::IssuedAndValidity(
|
|
(votes.candidate.clone(), iss),
|
|
(digest, good),
|
|
)),
|
|
|
|
// two signatures on same candidate
|
|
(ValidityVote::Issued(a), ValidityVote::Issued(b)) =>
|
|
make_ds(DoubleSign::Seconded(votes.candidate.clone(), a, b)),
|
|
|
|
// two signatures on same validity vote
|
|
(ValidityVote::Valid(a), ValidityVote::Valid(b)) =>
|
|
make_ds(DoubleSign::Validity(digest, a, b)),
|
|
})
|
|
} else {
|
|
Ok(None)
|
|
}
|
|
},
|
|
Entry::Vacant(vacant) => {
|
|
vacant.insert(vote);
|
|
},
|
|
}
|
|
|
|
Ok(Some(votes.summary(digest)))
|
|
}
|
|
}
|
|
|
|
type Drain<'a, Ctx> = hash_map::Drain<'a, <Ctx as Context>::AuthorityId, Vec<MisbehaviorFor<Ctx>>>;
|
|
|
|
struct MisbehaviorForAuthority<Ctx: Context> {
|
|
id: Ctx::AuthorityId,
|
|
misbehaviors: Vec<MisbehaviorFor<Ctx>>,
|
|
}
|
|
|
|
impl<Ctx: Context> From<(Ctx::AuthorityId, Vec<MisbehaviorFor<Ctx>>)>
|
|
for MisbehaviorForAuthority<Ctx>
|
|
{
|
|
fn from((id, mut misbehaviors): (Ctx::AuthorityId, Vec<MisbehaviorFor<Ctx>>)) -> Self {
|
|
// we're going to be popping items off this list in the iterator, so reverse it now to
|
|
// preserve the original ordering.
|
|
misbehaviors.reverse();
|
|
Self { id, misbehaviors }
|
|
}
|
|
}
|
|
|
|
impl<Ctx: Context> Iterator for MisbehaviorForAuthority<Ctx> {
|
|
type Item = (Ctx::AuthorityId, MisbehaviorFor<Ctx>);
|
|
|
|
fn next(&mut self) -> Option<Self::Item> {
|
|
self.misbehaviors.pop().map(|misbehavior| (self.id.clone(), misbehavior))
|
|
}
|
|
}
|
|
|
|
pub struct DrainMisbehaviors<'a, Ctx: Context> {
|
|
drain: Drain<'a, Ctx>,
|
|
in_progress: Option<MisbehaviorForAuthority<Ctx>>,
|
|
}
|
|
|
|
impl<'a, Ctx: Context> From<Drain<'a, Ctx>> for DrainMisbehaviors<'a, Ctx> {
|
|
fn from(drain: Drain<'a, Ctx>) -> Self {
|
|
Self { drain, in_progress: None }
|
|
}
|
|
}
|
|
|
|
impl<'a, Ctx: Context> DrainMisbehaviors<'a, Ctx> {
|
|
fn maybe_item(&mut self) -> Option<(Ctx::AuthorityId, MisbehaviorFor<Ctx>)> {
|
|
self.in_progress.as_mut().and_then(Iterator::next)
|
|
}
|
|
}
|
|
|
|
impl<'a, Ctx: Context> Iterator for DrainMisbehaviors<'a, Ctx> {
|
|
type Item = (Ctx::AuthorityId, MisbehaviorFor<Ctx>);
|
|
|
|
fn next(&mut self) -> Option<Self::Item> {
|
|
// Note: this implementation will prematurely return `None` if `self.drain.next()` ever
|
|
// returns a tuple whose vector is empty. That will never currently happen, as the only
|
|
// modification to the backing map is currently via `drain` and
|
|
// `entry(...).or_default().push(...)`. However, future code changes might change that
|
|
// property.
|
|
self.maybe_item().or_else(|| {
|
|
self.in_progress = self.drain.next().map(Into::into);
|
|
self.maybe_item()
|
|
})
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use std::collections::HashMap;
|
|
|
|
fn create_single_seconded<Candidate: Context>() -> Table<Candidate> {
|
|
Table::new(Config { allow_multiple_seconded: false })
|
|
}
|
|
|
|
fn create_many_seconded<Candidate: Context>() -> Table<Candidate> {
|
|
Table::new(Config { allow_multiple_seconded: true })
|
|
}
|
|
|
|
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)]
|
|
struct AuthorityId(usize);
|
|
|
|
#[derive(Debug, Copy, Clone, Hash, PartialOrd, Ord, PartialEq, Eq)]
|
|
struct GroupId(usize);
|
|
|
|
// group, body
|
|
#[derive(Debug, Copy, Clone, Hash, PartialOrd, Ord, PartialEq, Eq)]
|
|
struct Candidate(usize, usize);
|
|
|
|
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)]
|
|
struct Signature(usize);
|
|
|
|
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)]
|
|
struct Digest(usize);
|
|
|
|
#[derive(Debug, PartialEq, Eq)]
|
|
struct TestContext {
|
|
// v -> parachain group
|
|
authorities: HashMap<AuthorityId, GroupId>,
|
|
}
|
|
|
|
impl Context for TestContext {
|
|
type AuthorityId = AuthorityId;
|
|
type Digest = Digest;
|
|
type Candidate = Candidate;
|
|
type GroupId = GroupId;
|
|
type Signature = Signature;
|
|
|
|
fn candidate_digest(candidate: &Candidate) -> Digest {
|
|
Digest(candidate.1)
|
|
}
|
|
|
|
fn is_member_of(&self, authority: &AuthorityId, group: &GroupId) -> bool {
|
|
self.authorities.get(authority).map(|v| v == group).unwrap_or(false)
|
|
}
|
|
|
|
fn get_group_size(&self, group: &Self::GroupId) -> Option<usize> {
|
|
let count = self.authorities.values().filter(|g| *g == group).count();
|
|
if count == 0 {
|
|
None
|
|
} else {
|
|
Some(count)
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn submitting_two_candidates_can_be_misbehavior() {
|
|
let context = TestContext {
|
|
authorities: {
|
|
let mut map = HashMap::new();
|
|
map.insert(AuthorityId(1), GroupId(2));
|
|
map
|
|
},
|
|
};
|
|
|
|
let mut table = create_single_seconded();
|
|
let statement_a = SignedStatement {
|
|
statement: Statement::Seconded(Candidate(2, 100)),
|
|
signature: Signature(1),
|
|
sender: AuthorityId(1),
|
|
};
|
|
|
|
let statement_b = SignedStatement {
|
|
statement: Statement::Seconded(Candidate(2, 999)),
|
|
signature: Signature(1),
|
|
sender: AuthorityId(1),
|
|
};
|
|
|
|
table.import_statement(&context, GroupId(2), statement_a);
|
|
assert!(!table.detected_misbehavior.contains_key(&AuthorityId(1)));
|
|
|
|
table.import_statement(&context, GroupId(2), statement_b);
|
|
assert_eq!(
|
|
table.detected_misbehavior[&AuthorityId(1)][0],
|
|
Misbehavior::MultipleCandidates(MultipleCandidates {
|
|
first: (Candidate(2, 100), Signature(1)),
|
|
second: (Candidate(2, 999), Signature(1)),
|
|
})
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn submitting_two_candidates_can_be_allowed() {
|
|
let context = TestContext {
|
|
authorities: {
|
|
let mut map = HashMap::new();
|
|
map.insert(AuthorityId(1), GroupId(2));
|
|
map
|
|
},
|
|
};
|
|
|
|
let mut table = create_many_seconded();
|
|
let statement_a = SignedStatement {
|
|
statement: Statement::Seconded(Candidate(2, 100)),
|
|
signature: Signature(1),
|
|
sender: AuthorityId(1),
|
|
};
|
|
|
|
let statement_b = SignedStatement {
|
|
statement: Statement::Seconded(Candidate(2, 999)),
|
|
signature: Signature(1),
|
|
sender: AuthorityId(1),
|
|
};
|
|
|
|
table.import_statement(&context, GroupId(2), statement_a);
|
|
assert!(!table.detected_misbehavior.contains_key(&AuthorityId(1)));
|
|
|
|
table.import_statement(&context, GroupId(2), statement_b);
|
|
assert!(!table.detected_misbehavior.contains_key(&AuthorityId(1)));
|
|
}
|
|
|
|
#[test]
|
|
fn submitting_candidate_from_wrong_group_is_misbehavior() {
|
|
let context = TestContext {
|
|
authorities: {
|
|
let mut map = HashMap::new();
|
|
map.insert(AuthorityId(1), GroupId(3));
|
|
map
|
|
},
|
|
};
|
|
|
|
let mut table = create_single_seconded();
|
|
let statement = SignedStatement {
|
|
statement: Statement::Seconded(Candidate(2, 100)),
|
|
signature: Signature(1),
|
|
sender: AuthorityId(1),
|
|
};
|
|
|
|
table.import_statement(&context, GroupId(2), statement);
|
|
|
|
assert_eq!(
|
|
table.detected_misbehavior[&AuthorityId(1)][0],
|
|
Misbehavior::UnauthorizedStatement(UnauthorizedStatement {
|
|
statement: SignedStatement {
|
|
statement: Statement::Seconded(Candidate(2, 100)),
|
|
signature: Signature(1),
|
|
sender: AuthorityId(1),
|
|
},
|
|
})
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn unauthorized_votes() {
|
|
let context = TestContext {
|
|
authorities: {
|
|
let mut map = HashMap::new();
|
|
map.insert(AuthorityId(1), GroupId(2));
|
|
map.insert(AuthorityId(2), GroupId(3));
|
|
map
|
|
},
|
|
};
|
|
|
|
let mut table = create_single_seconded();
|
|
|
|
let candidate_a = SignedStatement {
|
|
statement: Statement::Seconded(Candidate(2, 100)),
|
|
signature: Signature(1),
|
|
sender: AuthorityId(1),
|
|
};
|
|
let candidate_a_digest = Digest(100);
|
|
|
|
table.import_statement(&context, GroupId(2), candidate_a);
|
|
assert!(!table.detected_misbehavior.contains_key(&AuthorityId(1)));
|
|
assert!(!table.detected_misbehavior.contains_key(&AuthorityId(2)));
|
|
|
|
// authority 2 votes for validity on 1's candidate.
|
|
let bad_validity_vote = SignedStatement {
|
|
statement: Statement::Valid(candidate_a_digest),
|
|
signature: Signature(2),
|
|
sender: AuthorityId(2),
|
|
};
|
|
table.import_statement(&context, GroupId(3), bad_validity_vote);
|
|
|
|
assert_eq!(
|
|
table.detected_misbehavior[&AuthorityId(2)][0],
|
|
Misbehavior::UnauthorizedStatement(UnauthorizedStatement {
|
|
statement: SignedStatement {
|
|
statement: Statement::Valid(candidate_a_digest),
|
|
signature: Signature(2),
|
|
sender: AuthorityId(2),
|
|
},
|
|
})
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn candidate_double_signature_is_misbehavior() {
|
|
let context = TestContext {
|
|
authorities: {
|
|
let mut map = HashMap::new();
|
|
map.insert(AuthorityId(1), GroupId(2));
|
|
map.insert(AuthorityId(2), GroupId(2));
|
|
map
|
|
},
|
|
};
|
|
|
|
let mut table = create_single_seconded();
|
|
let statement = SignedStatement {
|
|
statement: Statement::Seconded(Candidate(2, 100)),
|
|
signature: Signature(1),
|
|
sender: AuthorityId(1),
|
|
};
|
|
|
|
table.import_statement(&context, GroupId(2), statement);
|
|
assert!(!table.detected_misbehavior.contains_key(&AuthorityId(1)));
|
|
|
|
let invalid_statement = SignedStatement {
|
|
statement: Statement::Seconded(Candidate(2, 100)),
|
|
signature: Signature(999),
|
|
sender: AuthorityId(1),
|
|
};
|
|
|
|
table.import_statement(&context, GroupId(2), invalid_statement);
|
|
assert!(table.detected_misbehavior.contains_key(&AuthorityId(1)));
|
|
}
|
|
|
|
#[test]
|
|
fn issue_and_vote_is_misbehavior() {
|
|
let context = TestContext {
|
|
authorities: {
|
|
let mut map = HashMap::new();
|
|
map.insert(AuthorityId(1), GroupId(2));
|
|
map
|
|
},
|
|
};
|
|
|
|
let mut table = create_single_seconded();
|
|
let statement = SignedStatement {
|
|
statement: Statement::Seconded(Candidate(2, 100)),
|
|
signature: Signature(1),
|
|
sender: AuthorityId(1),
|
|
};
|
|
let candidate_digest = Digest(100);
|
|
|
|
table.import_statement(&context, GroupId(2), statement);
|
|
assert!(!table.detected_misbehavior.contains_key(&AuthorityId(1)));
|
|
|
|
let extra_vote = SignedStatement {
|
|
statement: Statement::Valid(candidate_digest),
|
|
signature: Signature(1),
|
|
sender: AuthorityId(1),
|
|
};
|
|
|
|
table.import_statement(&context, GroupId(2), extra_vote);
|
|
assert_eq!(
|
|
table.detected_misbehavior[&AuthorityId(1)][0],
|
|
Misbehavior::ValidityDoubleVote(ValidityDoubleVote::IssuedAndValidity(
|
|
(Candidate(2, 100), Signature(1)),
|
|
(Digest(100), Signature(1)),
|
|
))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn candidate_attested_works() {
|
|
let validity_threshold = 6;
|
|
|
|
let mut candidate = CandidateData::<TestContext> {
|
|
group_id: GroupId(4),
|
|
candidate: Candidate(4, 12345),
|
|
validity_votes: HashMap::new(),
|
|
};
|
|
|
|
assert!(candidate.attested(validity_threshold).is_none());
|
|
|
|
for i in 0..validity_threshold {
|
|
candidate
|
|
.validity_votes
|
|
.insert(AuthorityId(i + 100), ValidityVote::Valid(Signature(i + 100)));
|
|
}
|
|
|
|
assert!(candidate.attested(validity_threshold).is_some());
|
|
|
|
candidate.validity_votes.insert(
|
|
AuthorityId(validity_threshold + 100),
|
|
ValidityVote::Valid(Signature(validity_threshold + 100)),
|
|
);
|
|
|
|
assert!(candidate.attested(validity_threshold).is_some());
|
|
}
|
|
|
|
#[test]
|
|
fn includability_works() {
|
|
let context = TestContext {
|
|
authorities: {
|
|
let mut map = HashMap::new();
|
|
map.insert(AuthorityId(1), GroupId(2));
|
|
map.insert(AuthorityId(2), GroupId(2));
|
|
map.insert(AuthorityId(3), GroupId(2));
|
|
map
|
|
},
|
|
};
|
|
|
|
// have 2/3 validity guarantors note validity.
|
|
let mut table = create_single_seconded();
|
|
let statement = SignedStatement {
|
|
statement: Statement::Seconded(Candidate(2, 100)),
|
|
signature: Signature(1),
|
|
sender: AuthorityId(1),
|
|
};
|
|
let candidate_digest = Digest(100);
|
|
|
|
table.import_statement(&context, GroupId(2), statement);
|
|
|
|
assert!(!table.detected_misbehavior.contains_key(&AuthorityId(1)));
|
|
assert!(table.attested_candidate(&candidate_digest, &context, 2).is_none());
|
|
|
|
let vote = SignedStatement {
|
|
statement: Statement::Valid(candidate_digest),
|
|
signature: Signature(2),
|
|
sender: AuthorityId(2),
|
|
};
|
|
|
|
table.import_statement(&context, GroupId(2), vote);
|
|
assert!(!table.detected_misbehavior.contains_key(&AuthorityId(2)));
|
|
assert!(table.attested_candidate(&candidate_digest, &context, 2).is_some());
|
|
}
|
|
|
|
#[test]
|
|
fn candidate_import_gives_summary() {
|
|
let context = TestContext {
|
|
authorities: {
|
|
let mut map = HashMap::new();
|
|
map.insert(AuthorityId(1), GroupId(2));
|
|
map
|
|
},
|
|
};
|
|
|
|
let mut table = create_single_seconded();
|
|
let statement = SignedStatement {
|
|
statement: Statement::Seconded(Candidate(2, 100)),
|
|
signature: Signature(1),
|
|
sender: AuthorityId(1),
|
|
};
|
|
|
|
let summary = table
|
|
.import_statement(&context, GroupId(2), statement)
|
|
.expect("candidate import to give summary");
|
|
|
|
assert_eq!(summary.candidate, Digest(100));
|
|
assert_eq!(summary.group_id, GroupId(2));
|
|
assert_eq!(summary.validity_votes, 1);
|
|
}
|
|
|
|
#[test]
|
|
fn candidate_vote_gives_summary() {
|
|
let context = TestContext {
|
|
authorities: {
|
|
let mut map = HashMap::new();
|
|
map.insert(AuthorityId(1), GroupId(2));
|
|
map.insert(AuthorityId(2), GroupId(2));
|
|
map
|
|
},
|
|
};
|
|
|
|
let mut table = create_single_seconded();
|
|
let statement = SignedStatement {
|
|
statement: Statement::Seconded(Candidate(2, 100)),
|
|
signature: Signature(1),
|
|
sender: AuthorityId(1),
|
|
};
|
|
let candidate_digest = Digest(100);
|
|
|
|
table.import_statement(&context, GroupId(2), statement);
|
|
assert!(!table.detected_misbehavior.contains_key(&AuthorityId(1)));
|
|
|
|
let vote = SignedStatement {
|
|
statement: Statement::Valid(candidate_digest),
|
|
signature: Signature(2),
|
|
sender: AuthorityId(2),
|
|
};
|
|
|
|
let summary = table
|
|
.import_statement(&context, GroupId(2), vote)
|
|
.expect("candidate vote to give summary");
|
|
|
|
assert!(!table.detected_misbehavior.contains_key(&AuthorityId(2)));
|
|
|
|
assert_eq!(summary.candidate, Digest(100));
|
|
assert_eq!(summary.group_id, GroupId(2));
|
|
assert_eq!(summary.validity_votes, 2);
|
|
}
|
|
}
|