mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-05-30 16:21:02 +00:00
Blocks carry full attestations for candidates (#42)
* statement table yields fully-attested candidates * attestation types in polkadot-primitives * propose block with fully-attested candidates in consensus * some signature-checking logic in the runtime * fix runtime compilation * ensure attestations are full and without duplicate when checking * fix consensus-service compilation * add some tests * use bitvec from crates.io now that it's published * sign statements based on primitive statement's encoding * remove some serialize bounds * Fix error message with duplicate availability attestations Co-Authored-By: rphmeier <rphmeier@gmail.com>
This commit is contained in:
committed by
GitHub
parent
c31f8168df
commit
7b46856159
Generated
+7
@@ -203,6 +203,11 @@ name = "bitflags"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "bitvec"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "blake2"
|
||||
version = "0.7.1"
|
||||
@@ -2023,6 +2028,7 @@ dependencies = [
|
||||
name = "polkadot-runtime"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bitvec 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"hex-literal 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"log 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"parity-codec 2.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
@@ -4099,6 +4105,7 @@ dependencies = [
|
||||
"checksum bitflags 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "aad18937a628ec6abcd26d1489012cc0e18c21798210f491af69ded9b881106d"
|
||||
"checksum bitflags 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4efd02e230a02e18f92fc2735f44597385ed02ad8f831e7c1c1156ee5e1ab3a5"
|
||||
"checksum bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "228047a76f468627ca71776ecdebd732a3423081fcf5125585bcd7c49886ce12"
|
||||
"checksum bitvec 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e37e2176261200377c7cde4c6de020394174df556c356f965e4bc239f5ce1c5a"
|
||||
"checksum blake2 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "73b77e29dbd0115e43938be2d5128ecf81c0353e00acaa65339a1242586951d9"
|
||||
"checksum blake2-rfc 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)" = "5d6d530bdd2d52966a6d03b7a964add7ae1a288d25214066fd4b600f0f796400"
|
||||
"checksum block-buffer 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a076c298b9ecdb530ed9d967e74a6027d6a7478924520acddcddc24c1c8ab3ab"
|
||||
|
||||
@@ -74,7 +74,7 @@ use parking_lot::Mutex;
|
||||
use polkadot_primitives::{Hash, Block, BlockId, BlockNumber, Header, Timestamp, SessionKey};
|
||||
use polkadot_primitives::{Compact, UncheckedExtrinsic};
|
||||
use polkadot_primitives::parachain::{Id as ParaId, Chain, DutyRoster, BlockData, Extrinsic as ParachainExtrinsic, CandidateReceipt, CandidateSignature};
|
||||
use polkadot_primitives::parachain::ParachainHost;
|
||||
use polkadot_primitives::parachain::{AttestedCandidate, ParachainHost, Statement as PrimitiveStatement};
|
||||
use primitives::{AuthorityId, ed25519};
|
||||
use runtime_primitives::traits::ProvideRuntimeApi;
|
||||
use tokio::runtime::TaskExecutor;
|
||||
@@ -156,7 +156,10 @@ pub struct GroupInfo {
|
||||
/// The actual message signed is the encoded statement concatenated with the
|
||||
/// parent hash.
|
||||
pub fn sign_table_statement(statement: &Statement, key: &ed25519::Pair, parent_hash: &Hash) -> CandidateSignature {
|
||||
let mut encoded = statement.encode();
|
||||
// we sign using the primitive statement type because that's what the runtime
|
||||
// expects. These types probably encode the same way so this clone could be optimized
|
||||
// out in the future.
|
||||
let mut encoded = PrimitiveStatement::from(statement.clone()).encode();
|
||||
encoded.extend(parent_hash.as_ref());
|
||||
|
||||
key.sign(&encoded).into()
|
||||
@@ -166,7 +169,7 @@ pub fn sign_table_statement(statement: &Statement, key: &ed25519::Pair, parent_h
|
||||
pub fn check_statement(statement: &Statement, signature: &CandidateSignature, signer: SessionKey, parent_hash: &Hash) -> bool {
|
||||
use runtime_primitives::traits::Verify;
|
||||
|
||||
let mut encoded = statement.encode();
|
||||
let mut encoded = PrimitiveStatement::from(statement.clone()).encode();
|
||||
encoded.extend(parent_hash.as_ref());
|
||||
|
||||
signature.verify(&encoded[..], &signer.into())
|
||||
@@ -640,7 +643,7 @@ impl<C, TxApi> CreateProposal<C, TxApi> where
|
||||
C: ProvideRuntimeApi + HeaderBackend<Block> + Send + Sync,
|
||||
C::Api: ParachainHost<Block> + BlockBuilder<Block>,
|
||||
{
|
||||
fn propose_with(&self, candidates: Vec<CandidateReceipt>) -> Result<Block, Error> {
|
||||
fn propose_with(&self, candidates: Vec<AttestedCandidate>) -> Result<Block, Error> {
|
||||
use client::block_builder::BlockBuilder;
|
||||
use runtime_primitives::traits::{Hash as HashT, BlakeTwo256};
|
||||
use polkadot_primitives::InherentData;
|
||||
@@ -726,9 +729,7 @@ impl<C, TxApi> Future for CreateProposal<C, TxApi> where
|
||||
try_ready!(self.timing.poll(included));
|
||||
|
||||
// 2. propose
|
||||
let proposed_candidates = self.table.with_proposal(|proposed_set| {
|
||||
proposed_set.into_iter().cloned().collect()
|
||||
});
|
||||
let proposed_candidates = self.table.proposed_set();
|
||||
|
||||
self.propose_with(proposed_candidates).map(Async::Ready)
|
||||
}
|
||||
@@ -738,6 +739,7 @@ impl<C, TxApi> Future for CreateProposal<C, TxApi> where
|
||||
mod tests {
|
||||
use super::*;
|
||||
use substrate_keyring::Keyring;
|
||||
use polkadot_primitives::parachain::Statement as PStatement;
|
||||
|
||||
#[test]
|
||||
fn sign_and_check_statement() {
|
||||
|
||||
@@ -94,7 +94,7 @@ fn prune_unneeded_availability<C>(client: Arc<C>, extrinsic_store: ExtrinsicStor
|
||||
.iter()
|
||||
.filter_map(|ex| match ex.function {
|
||||
Call::Parachains(ParachainsCall::set_heads(ref heads)) =>
|
||||
Some(heads.iter().map(|c| c.hash()).collect()),
|
||||
Some(heads.iter().map(|c| c.candidate.hash()).collect()),
|
||||
_ => None,
|
||||
})
|
||||
.next()
|
||||
|
||||
@@ -23,7 +23,10 @@ use std::sync::Arc;
|
||||
use extrinsic_store::{Data, Store as ExtrinsicStore};
|
||||
use table::{self, Table, Context as TableContextTrait};
|
||||
use polkadot_primitives::{Hash, SessionKey};
|
||||
use polkadot_primitives::parachain::{Id as ParaId, BlockData, Collation, Extrinsic, CandidateReceipt};
|
||||
use polkadot_primitives::parachain::{
|
||||
Id as ParaId, BlockData, Collation, Extrinsic, CandidateReceipt,
|
||||
AttestedCandidate,
|
||||
};
|
||||
|
||||
use parking_lot::Mutex;
|
||||
use futures::{future, prelude::*};
|
||||
@@ -439,14 +442,25 @@ impl SharedTable {
|
||||
f(inner.table.get_candidate(digest))
|
||||
}
|
||||
|
||||
/// Execute a closure using the current proposed set.
|
||||
///
|
||||
/// Deadlocks if called recursively.
|
||||
pub fn with_proposal<F, U>(&self, f: F) -> U
|
||||
where F: FnOnce(Vec<&CandidateReceipt>) -> U
|
||||
{
|
||||
let inner = self.inner.lock();
|
||||
f(inner.table.proposed_candidates(&*self.context))
|
||||
/// Get a set of candidates that can be proposed.
|
||||
pub fn proposed_set(&self) -> Vec<AttestedCandidate> {
|
||||
use table::generic::{ValidityAttestation as GAttestation};
|
||||
use polkadot_primitives::parachain::ValidityAttestation;
|
||||
|
||||
// we transform the types of the attestations gathered from the table
|
||||
// into the type expected by the runtime. This may do signature
|
||||
// aggregation in the future.
|
||||
let table_attestations = self.inner.lock().table.proposed_candidates(&*self.context);
|
||||
table_attestations.into_iter()
|
||||
.map(|attested| AttestedCandidate {
|
||||
candidate: attested.candidate,
|
||||
availability_votes: attested.availability_votes,
|
||||
validity_votes: attested.validity_votes.into_iter().map(|(a, v)| match v {
|
||||
GAttestation::Implicit(s) => (a, ValidityAttestation::Implicit(s)),
|
||||
GAttestation::Explicit(s) => (a, ValidityAttestation::Explicit(s)),
|
||||
}).collect(),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Get the number of total parachains.
|
||||
|
||||
@@ -113,6 +113,6 @@ impl Extrinsic for UncheckedExtrinsic {}
|
||||
pub struct InherentData {
|
||||
/// Current timestamp.
|
||||
pub timestamp: Timestamp,
|
||||
/// Parachain heads update.
|
||||
pub parachain_heads: Vec<::parachain::CandidateReceipt>,
|
||||
/// Parachain heads update. This contains fully-attested candidates.
|
||||
pub parachain_heads: Vec<::parachain::AttestedCandidate>,
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
use rstd::prelude::*;
|
||||
use rstd::cmp::Ordering;
|
||||
use super::Hash;
|
||||
use super::{Hash, SessionKey};
|
||||
|
||||
use {AccountId};
|
||||
|
||||
@@ -102,7 +102,6 @@ pub struct CandidateReceipt {
|
||||
|
||||
impl CandidateReceipt {
|
||||
/// Get the blake2_256 hash
|
||||
#[cfg(feature = "std")]
|
||||
pub fn hash(&self) -> Hash {
|
||||
use runtime_primitives::traits::{BlakeTwo256, Hash};
|
||||
BlakeTwo256::hash_of(self)
|
||||
@@ -189,25 +188,68 @@ pub struct HeadData(#[cfg_attr(feature = "std", serde(with="bytes"))] pub Vec<u8
|
||||
#[cfg_attr(feature = "std", derive(Serialize, Deserialize, Debug))]
|
||||
pub struct ValidationCode(#[cfg_attr(feature = "std", serde(with="bytes"))] pub Vec<u8>);
|
||||
|
||||
/// Activitiy bit field
|
||||
/// Activity bit field
|
||||
#[derive(PartialEq, Eq, Clone, Default, Encode, Decode)]
|
||||
#[cfg_attr(feature = "std", derive(Serialize, Deserialize, Debug))]
|
||||
pub struct Activity(#[cfg_attr(feature = "std", serde(with="bytes"))] pub Vec<u8>);
|
||||
|
||||
/// Statements which can be made about parachain candidates.
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "std", derive(Debug, Serialize, Deserialize))]
|
||||
#[derive(Clone, PartialEq, Eq, Encode)]
|
||||
#[cfg_attr(feature = "std", derive(Debug))]
|
||||
pub enum Statement {
|
||||
/// Proposal of a parachain candidate.
|
||||
#[codec(index = "1")]
|
||||
Candidate(CandidateReceipt),
|
||||
/// State that a parachain candidate is valid.
|
||||
#[codec(index = "2")]
|
||||
Valid(Hash),
|
||||
/// Vote to commit to a candidate.
|
||||
/// State a candidate is invalid.
|
||||
#[codec(index = "3")]
|
||||
Invalid(Hash),
|
||||
/// Vote to advance round after inactive primary.
|
||||
/// State a candidate's associated data is unavailable.
|
||||
#[codec(index = "4")]
|
||||
Available(Hash),
|
||||
}
|
||||
|
||||
/// An either implicit or explicit attestation to the validity of a parachain
|
||||
/// candidate.
|
||||
#[derive(Clone, PartialEq, Decode, Encode)]
|
||||
#[cfg_attr(feature = "std", derive(Debug))]
|
||||
pub enum ValidityAttestation {
|
||||
/// implicit validity attestation by issuing.
|
||||
/// This corresponds to issuance of a `Candidate` statement.
|
||||
#[codec(index = "1")]
|
||||
Implicit(CandidateSignature),
|
||||
/// An explicit attestation. This corresponds to issuance of a
|
||||
/// `Valid` statement.
|
||||
#[codec(index = "2")]
|
||||
Explicit(CandidateSignature),
|
||||
}
|
||||
|
||||
/// An attested candidate.
|
||||
#[derive(Clone, PartialEq, Decode, Encode)]
|
||||
#[cfg_attr(feature = "std", derive(Debug))]
|
||||
pub struct AttestedCandidate {
|
||||
/// The candidate data.
|
||||
pub candidate: CandidateReceipt,
|
||||
/// Validity attestations.
|
||||
pub validity_votes: Vec<(SessionKey, ValidityAttestation)>,
|
||||
/// Availability attestations.
|
||||
pub availability_votes: Vec<(SessionKey, CandidateSignature)>,
|
||||
}
|
||||
|
||||
impl AttestedCandidate {
|
||||
/// Get the candidate.
|
||||
pub fn candidate(&self) -> &CandidateReceipt {
|
||||
&self.candidate
|
||||
}
|
||||
|
||||
/// Get the group ID of the candidate.
|
||||
pub fn parachain_index(&self) -> Id {
|
||||
self.candidate.parachain_index
|
||||
}
|
||||
}
|
||||
|
||||
decl_runtime_apis! {
|
||||
/// The API for querying the state of parachains on-chain.
|
||||
pub trait ParachainHost {
|
||||
|
||||
@@ -4,6 +4,7 @@ version = "0.1.0"
|
||||
authors = ["Parity Technologies <admin@parity.io>"]
|
||||
|
||||
[dependencies]
|
||||
bitvec = { version = "0.8", default-features = false, features = ["alloc"] }
|
||||
rustc-hex = "1.0"
|
||||
log = { version = "0.3", optional = true }
|
||||
serde = { version = "1.0", default-features = false }
|
||||
@@ -17,7 +18,6 @@ sr-std = { git = "https://github.com/paritytech/substrate" }
|
||||
sr-io = { git = "https://github.com/paritytech/substrate" }
|
||||
srml-support = { git = "https://github.com/paritytech/substrate" }
|
||||
substrate-primitives = { git = "https://github.com/paritytech/substrate" }
|
||||
substrate-keyring = { git = "https://github.com/paritytech/substrate" }
|
||||
substrate-client = { git = "https://github.com/paritytech/substrate" }
|
||||
srml-balances = { git = "https://github.com/paritytech/substrate" }
|
||||
srml-consensus = { git = "https://github.com/paritytech/substrate" }
|
||||
@@ -34,10 +34,12 @@ sr-version = { git = "https://github.com/paritytech/substrate" }
|
||||
|
||||
[dev-dependencies]
|
||||
hex-literal = "0.1.0"
|
||||
substrate-keyring = { git = "https://github.com/paritytech/substrate" }
|
||||
|
||||
[features]
|
||||
default = ["std"]
|
||||
std = [
|
||||
"bitvec/std",
|
||||
"polkadot-primitives/std",
|
||||
"parity-codec/std",
|
||||
"parity-codec-derive/std",
|
||||
|
||||
@@ -20,6 +20,9 @@
|
||||
// `construct_runtime!` does a lot of recursion and requires us to increase the limit to 256.
|
||||
#![recursion_limit="256"]
|
||||
|
||||
#[macro_use]
|
||||
extern crate bitvec;
|
||||
|
||||
#[macro_use]
|
||||
extern crate parity_codec_derive;
|
||||
extern crate parity_codec as codec;
|
||||
@@ -52,6 +55,9 @@ extern crate srml_treasury as treasury;
|
||||
|
||||
extern crate polkadot_primitives as primitives;
|
||||
|
||||
#[cfg(test)]
|
||||
extern crate substrate_keyring as keyring;
|
||||
|
||||
mod parachains;
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
|
||||
@@ -19,9 +19,11 @@
|
||||
use rstd::prelude::*;
|
||||
use codec::Decode;
|
||||
|
||||
use sr_primitives::{RuntimeString, traits::{Extrinsic, Block as BlockT,
|
||||
Hash, BlakeTwo256, ProvideInherent}};
|
||||
use primitives::parachain::{Id, Chain, DutyRoster, CandidateReceipt};
|
||||
use bitvec::BigEndian;
|
||||
use sr_primitives::{RuntimeString, traits::{
|
||||
Extrinsic, Block as BlockT, Hash as HashT, BlakeTwo256, ProvideInherent,
|
||||
}};
|
||||
use primitives::parachain::{Id, Chain, DutyRoster, AttestedCandidate, Statement};
|
||||
use {system, session};
|
||||
|
||||
use srml_support::{StorageValue, StorageMap};
|
||||
@@ -77,7 +79,7 @@ decl_module! {
|
||||
/// Parachains module.
|
||||
pub struct Module<T: Trait> for enum Call where origin: T::Origin {
|
||||
/// Provide candidate receipts for parachains, in ascending order by id.
|
||||
fn set_heads(origin, heads: Vec<CandidateReceipt>) -> Result {
|
||||
fn set_heads(origin, heads: Vec<AttestedCandidate>) -> Result {
|
||||
ensure_inherent(origin)?;
|
||||
ensure!(!<DidUpdate<T>>::exists(), "Parachain heads must be updated only once in the block");
|
||||
ensure!(
|
||||
@@ -98,23 +100,26 @@ decl_module! {
|
||||
for head in &heads {
|
||||
// proposed heads must be ascending order by parachain ID without duplicate.
|
||||
ensure!(
|
||||
last_id.as_ref().map_or(true, |x| x < &head.parachain_index),
|
||||
last_id.as_ref().map_or(true, |x| x < &head.parachain_index()),
|
||||
"Parachain candidates out of order by ID"
|
||||
);
|
||||
|
||||
// must be unknown since active parachains are always sorted.
|
||||
ensure!(
|
||||
iter.find(|x| x == &&head.parachain_index).is_some(),
|
||||
iter.find(|x| x == &&head.parachain_index()).is_some(),
|
||||
"Submitted candidate for unregistered or out-of-order parachain {}"
|
||||
);
|
||||
|
||||
last_id = Some(head.parachain_index);
|
||||
last_id = Some(head.parachain_index());
|
||||
}
|
||||
}
|
||||
|
||||
Self::check_attestations(&heads)?;
|
||||
|
||||
|
||||
for head in heads {
|
||||
let id = head.parachain_index.clone();
|
||||
<Heads<T>>::insert(id, head.head_data.0);
|
||||
let id = head.parachain_index();
|
||||
<Heads<T>>::insert(id, head.candidate.head_data.0);
|
||||
}
|
||||
|
||||
<DidUpdate<T>>::put(true);
|
||||
@@ -158,6 +163,18 @@ decl_module! {
|
||||
}
|
||||
}
|
||||
|
||||
fn majority_of(list_len: usize) -> usize {
|
||||
list_len / 2 + list_len % 2
|
||||
}
|
||||
|
||||
fn localized_payload(statement: Statement, parent_hash: ::primitives::Hash) -> Vec<u8> {
|
||||
use codec::Encode;
|
||||
|
||||
let mut encoded = statement.encode();
|
||||
encoded.extend(parent_hash.as_ref());
|
||||
encoded
|
||||
}
|
||||
|
||||
impl<T: Trait> Module<T> {
|
||||
/// Calculate the current block's duty roster using system's random seed.
|
||||
pub fn calculate_duty_roster() -> DutyRoster {
|
||||
@@ -208,6 +225,179 @@ impl<T: Trait> Module<T> {
|
||||
}
|
||||
}
|
||||
|
||||
// check the attestations on these candidates. The candidates should have been checked
|
||||
// that each candidates' chain ID is valid.
|
||||
fn check_attestations(attested_candidates: &[AttestedCandidate]) -> Result {
|
||||
use primitives::parachain::ValidityAttestation;
|
||||
use sr_primitives::traits::Verify;
|
||||
|
||||
// returns groups of slices that have the same chain ID.
|
||||
// assumes the inner slice is sorted by id.
|
||||
struct GroupedDutyIter<'a> {
|
||||
next_idx: usize,
|
||||
inner: &'a [(usize, Id)],
|
||||
}
|
||||
|
||||
impl<'a> GroupedDutyIter<'a> {
|
||||
fn new(inner: &'a [(usize, Id)]) -> Self {
|
||||
GroupedDutyIter { next_idx: 0, inner }
|
||||
}
|
||||
|
||||
fn group_for(&mut self, wanted_id: Id) -> Option<&'a [(usize, Id)]> {
|
||||
while let Some((id, keys)) = self.next() {
|
||||
if wanted_id == id {
|
||||
return Some(keys)
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for GroupedDutyIter<'a> {
|
||||
type Item = (Id, &'a [(usize, Id)]);
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.next_idx == self.inner.len() { return None }
|
||||
let start_idx = self.next_idx;
|
||||
self.next_idx += 1;
|
||||
let start_id = self.inner[start_idx].1;
|
||||
|
||||
while self.inner.get(self.next_idx).map_or(false, |&(_, ref id)| id == &start_id) {
|
||||
self.next_idx += 1;
|
||||
}
|
||||
|
||||
Some((start_id, &self.inner[start_idx..self.next_idx]))
|
||||
}
|
||||
}
|
||||
|
||||
let authorities = super::Consensus::authorities();
|
||||
let duty_roster = Self::calculate_duty_roster();
|
||||
|
||||
// convert a duty roster, which is originally a Vec<Chain>, where each
|
||||
// item corresponds to the same position in the session keys, into
|
||||
// a list containing (index, parachain duty) where indices are into the session keys.
|
||||
// this list is sorted ascending by parachain duty, just like the
|
||||
// parachain candidates are.
|
||||
let make_sorted_duties = |duty: &[Chain]| {
|
||||
let mut sorted_duties = Vec::with_capacity(duty.len());
|
||||
for (val_idx, duty) in duty.iter().enumerate() {
|
||||
let id = match duty {
|
||||
Chain::Relay => continue,
|
||||
Chain::Parachain(id) => id,
|
||||
};
|
||||
|
||||
let idx = sorted_duties.binary_search_by_key(&id, |&(_, ref id)| id)
|
||||
.unwrap_or_else(|idx| idx);
|
||||
|
||||
sorted_duties.insert(idx, (val_idx, *id));
|
||||
}
|
||||
|
||||
sorted_duties
|
||||
};
|
||||
|
||||
let sorted_validators = make_sorted_duties(&duty_roster.validator_duty);
|
||||
let sorted_guarantors = make_sorted_duties(&duty_roster.guarantor_duty);
|
||||
|
||||
let parent_hash = super::System::parent_hash();
|
||||
let localized_payload = |statement: Statement| localized_payload(statement, parent_hash);
|
||||
|
||||
let mut validator_groups = GroupedDutyIter::new(&sorted_validators[..]);
|
||||
let mut guarantor_groups = GroupedDutyIter::new(&sorted_guarantors[..]);
|
||||
|
||||
for candidate in attested_candidates {
|
||||
let validator_group = validator_groups.group_for(candidate.parachain_index())
|
||||
.ok_or("no validator group for parachain")?;
|
||||
|
||||
let availability_group = guarantor_groups.group_for(candidate.parachain_index())
|
||||
.ok_or("no availability group for parachain")?;
|
||||
|
||||
ensure!(
|
||||
candidate.validity_votes.len() >= majority_of(validator_group.len()),
|
||||
"Not enough validity attestations"
|
||||
);
|
||||
|
||||
ensure!(
|
||||
candidate.availability_votes.len() >= majority_of(availability_group.len()),
|
||||
"Not enough availability attestations"
|
||||
);
|
||||
|
||||
let mut candidate_hash = None;
|
||||
let mut encoded_implicit = None;
|
||||
let mut encoded_explicit = None;
|
||||
|
||||
// track which voters have voted already. the first `authorities.len()`
|
||||
// bits is for validity, the next are for availability.
|
||||
let mut track_voters = bitvec![0; authorities.len() * 2];
|
||||
for (auth_id, validity_attestation) in &candidate.validity_votes {
|
||||
// protect against double-votes.
|
||||
match validator_group.iter().find(|&(idx, _)| &authorities[*idx] == auth_id) {
|
||||
None => return Err("Attesting validator not on this chain's validation duty."),
|
||||
Some(&(idx, _)) => {
|
||||
if track_voters.get(idx) {
|
||||
return Err("Voter already attested validity once")
|
||||
}
|
||||
track_voters.set(idx, true)
|
||||
}
|
||||
}
|
||||
|
||||
let (payload, sig) = match validity_attestation {
|
||||
ValidityAttestation::Implicit(sig) => {
|
||||
let payload = encoded_implicit.get_or_insert_with(|| localized_payload(
|
||||
Statement::Candidate(candidate.candidate.clone()),
|
||||
));
|
||||
|
||||
(payload, sig)
|
||||
}
|
||||
ValidityAttestation::Explicit(sig) => {
|
||||
let hash = candidate_hash
|
||||
.get_or_insert_with(|| candidate.candidate.hash())
|
||||
.clone();
|
||||
|
||||
let payload = encoded_explicit.get_or_insert_with(|| localized_payload(
|
||||
Statement::Valid(hash),
|
||||
));
|
||||
|
||||
(payload, sig)
|
||||
}
|
||||
};
|
||||
|
||||
ensure!(
|
||||
sig.verify(&payload[..], &auth_id.0.into()),
|
||||
"Candidate validity attestation signature is bad."
|
||||
);
|
||||
}
|
||||
|
||||
let mut encoded_available = None;
|
||||
for (auth_id, sig) in &candidate.availability_votes {
|
||||
match availability_group.iter().find(|&(idx, _)| &authorities[*idx] == auth_id) {
|
||||
None => return Err("Attesting validator not on this chain's availability duty."),
|
||||
Some(&(idx, _)) => {
|
||||
if track_voters.get(authorities.len() + idx) {
|
||||
return Err("Voter already attested availability once")
|
||||
}
|
||||
track_voters.set(authorities.len() + idx, true)
|
||||
}
|
||||
}
|
||||
|
||||
let hash = candidate_hash
|
||||
.get_or_insert_with(|| candidate.candidate.hash())
|
||||
.clone();
|
||||
|
||||
let payload = encoded_available.get_or_insert_with(|| localized_payload(
|
||||
Statement::Available(hash),
|
||||
));
|
||||
|
||||
ensure!(
|
||||
sig.verify(&payload[..], &auth_id.0.into()),
|
||||
"Candidate availability attestation signature is bad."
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/*
|
||||
// TODO: Consider integrating if needed.
|
||||
/// Extract the parachain heads from the block.
|
||||
@@ -226,7 +416,7 @@ impl<T: Trait> Module<T> {
|
||||
}
|
||||
|
||||
impl<T: Trait> ProvideInherent for Module<T> {
|
||||
type Inherent = Vec<CandidateReceipt>;
|
||||
type Inherent = Vec<AttestedCandidate>;
|
||||
type Call = Call<T>;
|
||||
type Error = RuntimeString;
|
||||
|
||||
@@ -259,9 +449,10 @@ mod tests {
|
||||
use rstd::marker::PhantomData;
|
||||
use sr_io::{TestExternalities, with_externalities};
|
||||
use substrate_primitives::{H256, Blake2Hasher};
|
||||
use sr_primitives::BuildStorage;
|
||||
use sr_primitives::traits::{Identity, BlakeTwo256};
|
||||
use sr_primitives::testing::{Digest, Header, DigestItem};
|
||||
use sr_primitives::{generic, BuildStorage};
|
||||
use sr_primitives::traits::BlakeTwo256;
|
||||
use primitives::{parachain::{CandidateReceipt, HeadData, ValidityAttestation}, SessionKey};
|
||||
use keyring::Keyring;
|
||||
use {consensus, timestamp};
|
||||
|
||||
impl_outer_origin! {
|
||||
@@ -272,24 +463,24 @@ mod tests {
|
||||
pub struct Test;
|
||||
impl consensus::Trait for Test {
|
||||
const NOTE_OFFLINE_POSITION: u32 = 1;
|
||||
type SessionKey = u64;
|
||||
type SessionKey = SessionKey;
|
||||
type OnOfflineValidator = ();
|
||||
type Log = DigestItem;
|
||||
type Log = ::Log;
|
||||
}
|
||||
impl system::Trait for Test {
|
||||
type Origin = Origin;
|
||||
type Index = u64;
|
||||
type Index = ::Index;
|
||||
type BlockNumber = u64;
|
||||
type Hash = H256;
|
||||
type Hashing = BlakeTwo256;
|
||||
type Digest = Digest;
|
||||
type AccountId = u64;
|
||||
type Header = Header;
|
||||
type Digest = generic::Digest<::Log>;
|
||||
type AccountId = ::AccountId;
|
||||
type Header = ::Header;
|
||||
type Event = ();
|
||||
type Log = DigestItem;
|
||||
type Log = ::Log;
|
||||
}
|
||||
impl session::Trait for Test {
|
||||
type ConvertAccountIdToSessionKey = Identity;
|
||||
type ConvertAccountIdToSessionKey = ::SessionKeyConversion;
|
||||
type OnSessionChange = ();
|
||||
type Event = ();
|
||||
}
|
||||
@@ -305,14 +496,25 @@ mod tests {
|
||||
|
||||
fn new_test_ext(parachains: Vec<(Id, Vec<u8>, Vec<u8>)>) -> TestExternalities<Blake2Hasher> {
|
||||
let mut t = system::GenesisConfig::<Test>::default().build_storage().unwrap().0;
|
||||
let authority_keys = [
|
||||
Keyring::Alice,
|
||||
Keyring::Bob,
|
||||
Keyring::Charlie,
|
||||
Keyring::Dave,
|
||||
Keyring::Eve,
|
||||
Keyring::Ferdie,
|
||||
Keyring::One,
|
||||
Keyring::Two,
|
||||
];
|
||||
|
||||
t.extend(consensus::GenesisConfig::<Test>{
|
||||
code: vec![],
|
||||
authorities: vec![1, 2, 3],
|
||||
authorities: authority_keys.iter().map(|k| k.to_raw_public().into()).collect(),
|
||||
_genesis_phantom_data: PhantomData,
|
||||
}.build_storage().unwrap().0);
|
||||
t.extend(session::GenesisConfig::<Test>{
|
||||
session_length: 1000,
|
||||
validators: vec![1, 2, 3, 4, 5, 6, 7, 8],
|
||||
validators: authority_keys.iter().map(|k| k.to_raw_public().into()).collect(),
|
||||
_genesis_phantom_data: PhantomData,
|
||||
}.build_storage().unwrap().0);
|
||||
t.extend(GenesisConfig::<Test>{
|
||||
@@ -322,6 +524,55 @@ mod tests {
|
||||
t.into()
|
||||
}
|
||||
|
||||
fn make_attestations(candidate: &mut AttestedCandidate) {
|
||||
let mut vote_implicit = false;
|
||||
let parent_hash = ::System::parent_hash();
|
||||
|
||||
let duty_roster = Parachains::calculate_duty_roster();
|
||||
let candidate_hash = candidate.candidate.hash();
|
||||
|
||||
let authorities = ::Consensus::authorities();
|
||||
let extract_key = |public: SessionKey| {
|
||||
Keyring::from_raw_public(public.0).unwrap()
|
||||
};
|
||||
|
||||
let validation_entries = duty_roster.validator_duty.iter()
|
||||
.enumerate()
|
||||
.map(|(i, d)| (i, d, true));
|
||||
|
||||
let availability_entries = duty_roster.guarantor_duty.iter()
|
||||
.enumerate()
|
||||
.map(|(i, d)| (i, d, false));
|
||||
|
||||
for (idx, &duty, is_validation) in validation_entries.chain(availability_entries) {
|
||||
if duty != Chain::Parachain(candidate.parachain_index()) { continue }
|
||||
if is_validation { vote_implicit = !vote_implicit };
|
||||
|
||||
let key = extract_key(authorities[idx]);
|
||||
|
||||
let statement = if is_validation && vote_implicit {
|
||||
Statement::Candidate(candidate.candidate.clone())
|
||||
} else if is_validation {
|
||||
Statement::Valid(candidate_hash.clone())
|
||||
} else {
|
||||
Statement::Available(candidate_hash.clone())
|
||||
};
|
||||
|
||||
let payload = localized_payload(statement, parent_hash);
|
||||
let signature = key.sign(&payload[..]).into();
|
||||
|
||||
if is_validation {
|
||||
candidate.validity_votes.push((authorities[idx], if vote_implicit {
|
||||
ValidityAttestation::Implicit(signature)
|
||||
} else {
|
||||
ValidityAttestation::Explicit(signature)
|
||||
}));
|
||||
} else {
|
||||
candidate.availability_votes.push((authorities[idx], signature));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn active_parachains_should_work() {
|
||||
let parachains = vec![
|
||||
@@ -397,4 +648,130 @@ mod tests {
|
||||
assert!(duty_roster_1 != duty_roster_2);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unattested_candidate_is_rejected() {
|
||||
let parachains = vec![
|
||||
(0u32.into(), vec![], vec![]),
|
||||
(1u32.into(), vec![], vec![]),
|
||||
];
|
||||
|
||||
with_externalities(&mut new_test_ext(parachains), || {
|
||||
system::Module::<Test>::set_random_seed([0u8; 32].into());
|
||||
let candidate = AttestedCandidate {
|
||||
validity_votes: vec![],
|
||||
availability_votes: vec![],
|
||||
candidate: CandidateReceipt {
|
||||
parachain_index: 0.into(),
|
||||
collator: Default::default(),
|
||||
signature: Default::default(),
|
||||
head_data: HeadData(vec![1, 2, 3]),
|
||||
balance_uploads: vec![],
|
||||
egress_queue_roots: vec![],
|
||||
fees: 0,
|
||||
block_data_hash: Default::default(),
|
||||
}
|
||||
};
|
||||
|
||||
assert!(Parachains::dispatch(Call::set_heads(vec![candidate]), Origin::INHERENT).is_err());
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attested_candidates_accepted_in_order() {
|
||||
let parachains = vec![
|
||||
(0u32.into(), vec![], vec![]),
|
||||
(1u32.into(), vec![], vec![]),
|
||||
];
|
||||
|
||||
with_externalities(&mut new_test_ext(parachains), || {
|
||||
system::Module::<Test>::set_random_seed([0u8; 32].into());
|
||||
let mut candidate_a = AttestedCandidate {
|
||||
validity_votes: vec![],
|
||||
availability_votes: vec![],
|
||||
candidate: CandidateReceipt {
|
||||
parachain_index: 0.into(),
|
||||
collator: Default::default(),
|
||||
signature: Default::default(),
|
||||
head_data: HeadData(vec![1, 2, 3]),
|
||||
balance_uploads: vec![],
|
||||
egress_queue_roots: vec![],
|
||||
fees: 0,
|
||||
block_data_hash: Default::default(),
|
||||
}
|
||||
};
|
||||
|
||||
let mut candidate_b = AttestedCandidate {
|
||||
validity_votes: vec![],
|
||||
availability_votes: vec![],
|
||||
candidate: CandidateReceipt {
|
||||
parachain_index: 1.into(),
|
||||
collator: Default::default(),
|
||||
signature: Default::default(),
|
||||
head_data: HeadData(vec![2, 3, 4]),
|
||||
balance_uploads: vec![],
|
||||
egress_queue_roots: vec![],
|
||||
fees: 0,
|
||||
block_data_hash: Default::default(),
|
||||
}
|
||||
};
|
||||
|
||||
make_attestations(&mut candidate_a);
|
||||
make_attestations(&mut candidate_b);
|
||||
|
||||
assert!(Parachains::dispatch(
|
||||
Call::set_heads(vec![candidate_b.clone(), candidate_a.clone()]),
|
||||
Origin::INHERENT,
|
||||
).is_err());
|
||||
|
||||
assert!(Parachains::dispatch(
|
||||
Call::set_heads(vec![candidate_a.clone(), candidate_b.clone()]),
|
||||
Origin::INHERENT,
|
||||
).is_ok());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duplicate_vote_is_rejected() {
|
||||
let parachains = vec![
|
||||
(0u32.into(), vec![], vec![]),
|
||||
(1u32.into(), vec![], vec![]),
|
||||
];
|
||||
|
||||
with_externalities(&mut new_test_ext(parachains), || {
|
||||
system::Module::<Test>::set_random_seed([0u8; 32].into());
|
||||
let mut candidate = AttestedCandidate {
|
||||
validity_votes: vec![],
|
||||
availability_votes: vec![],
|
||||
candidate: CandidateReceipt {
|
||||
parachain_index: 0.into(),
|
||||
collator: Default::default(),
|
||||
signature: Default::default(),
|
||||
head_data: HeadData(vec![1, 2, 3]),
|
||||
balance_uploads: vec![],
|
||||
egress_queue_roots: vec![],
|
||||
fees: 0,
|
||||
block_data_hash: Default::default(),
|
||||
}
|
||||
};
|
||||
|
||||
make_attestations(&mut candidate);
|
||||
|
||||
let mut double_validity = candidate.clone();
|
||||
double_validity.validity_votes.push(candidate.validity_votes[0].clone());
|
||||
|
||||
assert!(Parachains::dispatch(
|
||||
Call::set_heads(vec![double_validity]),
|
||||
Origin::INHERENT,
|
||||
).is_err());
|
||||
|
||||
let mut double_availability = candidate.clone();
|
||||
double_availability.availability_votes.push(candidate.availability_votes[0].clone());
|
||||
|
||||
assert!(Parachains::dispatch(
|
||||
Call::set_heads(vec![double_availability]),
|
||||
Origin::INHERENT,
|
||||
).is_err());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+7
@@ -6,6 +6,11 @@ dependencies = [
|
||||
"nodrop 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitvec"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
version = "1.2.7"
|
||||
@@ -133,6 +138,7 @@ dependencies = [
|
||||
name = "polkadot-runtime"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bitvec 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"integer-sqrt 0.1.0 (git+https://github.com/paritytech/integer-sqrt-rs.git)",
|
||||
"parity-codec 2.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"parity-codec-derive 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
@@ -543,6 +549,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[metadata]
|
||||
"checksum arrayvec 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)" = "a1e964f9e24d588183fcb43503abda40d288c8657dfc27311516ce2f05675aef"
|
||||
"checksum bitvec 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e37e2176261200377c7cde4c6de020394174df556c356f965e4bc239f5ce1c5a"
|
||||
"checksum byteorder 1.2.7 (registry+https://github.com/rust-lang/crates.io-index)" = "94f88df23a25417badc922ab0f5716cc1330e87f71ddd9203b3a3ccd9cedf75d"
|
||||
"checksum crunchy 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "a2f4a431c5c9f662e1200b7c7f02c34e91361150e382089a8f2dec3ba680cbda"
|
||||
"checksum crunchy 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c240f247c278fa08a6d4820a6a222bfc6e0d999e51ba67be94f44c905b2161f2"
|
||||
|
||||
@@ -7,6 +7,7 @@ authors = ["Parity Technologies <admin@parity.io>"]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
bitvec = { version = "0.8", default-features = false, features = ["alloc"] }
|
||||
integer-sqrt = { git = "https://github.com/paritytech/integer-sqrt-rs.git", branch = "master" }
|
||||
polkadot-primitives = { path = "../../primitives", default-features = false }
|
||||
safe-mix = { version = "1.0", default-features = false }
|
||||
|
||||
BIN
Binary file not shown.
Binary file not shown.
@@ -177,7 +177,7 @@ enum ValidityVote<S: Eq + Clone> {
|
||||
pub struct Summary<D, G> {
|
||||
/// The digest of the candidate referenced.
|
||||
pub candidate: D,
|
||||
/// The group that candidate is in.
|
||||
/// The group that the candidate is in.
|
||||
pub group_id: G,
|
||||
/// How many validity votes are currently witnessed.
|
||||
pub validity_votes: usize,
|
||||
@@ -187,6 +187,30 @@ pub struct Summary<D, G> {
|
||||
pub signalled_bad: bool,
|
||||
}
|
||||
|
||||
/// A validity attestation.
|
||||
#[derive(Clone, PartialEq, Decode, Encode)]
|
||||
pub enum ValidityAttestation<S> {
|
||||
/// implicit validity attestation by issuing.
|
||||
/// This corresponds to issuance of a `Candidate` statement.
|
||||
Implicit(S),
|
||||
/// An explicit attestation. This corresponds to issuance of a
|
||||
/// `Valid` statement.
|
||||
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>)>,
|
||||
/// Availability attestations.
|
||||
pub availability_votes: Vec<(AuthorityId, Signature)>
|
||||
}
|
||||
|
||||
/// Stores votes and data about a candidate.
|
||||
pub struct CandidateData<C: Context> {
|
||||
group_id: C::GroupId,
|
||||
@@ -202,6 +226,53 @@ impl<C: Context> CandidateData<C> {
|
||||
!self.indicated_bad_by.is_empty()
|
||||
}
|
||||
|
||||
/// Yield a full attestation for a candidate.
|
||||
/// If the candidate can be included, it will return `Some`.
|
||||
pub fn attested(&self, validity_threshold: usize, availability_threshold: usize)
|
||||
-> Option<AttestedCandidate<
|
||||
C::GroupId, C::Candidate, C::AuthorityId, C::Signature,
|
||||
>>
|
||||
{
|
||||
if self.can_be_included(validity_threshold, availability_threshold) {
|
||||
let validity_votes: Vec<_> = self.validity_votes.iter()
|
||||
.filter_map(|(a, v)| match *v {
|
||||
ValidityVote::Invalid(_) => None,
|
||||
|
||||
ValidityVote::Valid(ref s) =>
|
||||
Some((a, ValidityAttestation::Explicit(s.clone()))),
|
||||
ValidityVote::Issued(ref s) =>
|
||||
Some((a, ValidityAttestation::Implicit(s.clone()))),
|
||||
})
|
||||
.take(validity_threshold)
|
||||
.map(|(k, v)| (k.clone(), v.clone()))
|
||||
.collect();
|
||||
|
||||
assert!(
|
||||
validity_votes.len() == validity_threshold,
|
||||
"candidate is includable; therefore there are enough validity votes; qed",
|
||||
);
|
||||
|
||||
let availability_votes: Vec<_> = self.availability_votes.iter()
|
||||
.take(availability_threshold)
|
||||
.map(|(k, v)| (k.clone(), v.clone()))
|
||||
.collect();
|
||||
|
||||
assert!(
|
||||
availability_votes.len() == availability_threshold,
|
||||
"candidate is includable; therefore there are enough availability votes; qed",
|
||||
);
|
||||
|
||||
Some(AttestedCandidate {
|
||||
group_id: self.group_id.clone(),
|
||||
candidate: self.candidate.clone(),
|
||||
validity_votes,
|
||||
availability_votes,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
// Candidate data can be included in a proposal
|
||||
// if it has enough validity and availability votes
|
||||
// and no authorities have called it bad.
|
||||
@@ -267,7 +338,9 @@ impl<C: Context> Table<C> {
|
||||
/// best candidate for each group with requisite votes for inclusion.
|
||||
///
|
||||
/// The vector is sorted in ascending order by group id.
|
||||
pub fn proposed_candidates<'a>(&'a self, context: &C) -> Vec<&'a C::Candidate> {
|
||||
pub fn proposed_candidates(&self, context: &C) -> Vec<AttestedCandidate<
|
||||
C::GroupId, C::Candidate, C::AuthorityId, C::Signature,
|
||||
>> {
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::btree_map::Entry as BTreeEntry;
|
||||
|
||||
@@ -282,19 +355,26 @@ impl<C: Context> Table<C> {
|
||||
let (validity_t, availability_t) = context.requisite_votes(group_id);
|
||||
|
||||
if !candidate_data.can_be_included(validity_t, availability_t) { continue }
|
||||
let candidate = &candidate_data.candidate;
|
||||
match best_candidates.entry(group_id.clone()) {
|
||||
BTreeEntry::Vacant(vacant) => {
|
||||
vacant.insert((candidate_data, validity_t, availability_t));
|
||||
},
|
||||
BTreeEntry::Occupied(mut occ) => {
|
||||
let candidate_ref = occ.get_mut();
|
||||
if *candidate_ref > candidate {
|
||||
*candidate_ref = candidate;
|
||||
if candidate_ref.0.candidate > candidate_data.candidate {
|
||||
candidate_ref.0 = candidate_data;
|
||||
}
|
||||
}
|
||||
BTreeEntry::Vacant(vacant) => { vacant.insert(candidate); },
|
||||
}
|
||||
}
|
||||
|
||||
best_candidates.values().cloned().collect::<Vec<_>>()
|
||||
best_candidates.values()
|
||||
.map(|&(candidate_data, validity_t, availability_t)|
|
||||
candidate_data.attested(validity_t, availability_t)
|
||||
.expect("candidate has been checked includable; \
|
||||
therefore an attestation can be constructed; qed")
|
||||
)
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
|
||||
/// Whether a candidate can be included.
|
||||
|
||||
@@ -25,7 +25,9 @@ pub mod generic;
|
||||
|
||||
pub use generic::Table;
|
||||
|
||||
use primitives::parachain::{Id, CandidateReceipt, CandidateSignature as Signature};
|
||||
use primitives::parachain::{
|
||||
Id, CandidateReceipt, CandidateSignature as Signature, Statement as PrimitiveStatement,
|
||||
};
|
||||
use primitives::{SessionKey, Hash};
|
||||
|
||||
/// Statements about candidates on the network.
|
||||
@@ -86,3 +88,14 @@ impl<C: Context> generic::Context for C {
|
||||
Context::requisite_votes(self, group)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Statement> for PrimitiveStatement {
|
||||
fn from(s: Statement) -> PrimitiveStatement {
|
||||
match s {
|
||||
generic::Statement::Valid(s) => PrimitiveStatement::Valid(s),
|
||||
generic::Statement::Invalid(s) => PrimitiveStatement::Invalid(s),
|
||||
generic::Statement::Candidate(s) => PrimitiveStatement::Candidate(s),
|
||||
generic::Statement::Available(s) => PrimitiveStatement::Available(s),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user