diff --git a/polkadot/Cargo.lock b/polkadot/Cargo.lock index e48886a2b08ece37783d31b3f9e264413e2fe8ad..031f60f6eb6ef5168dad9e91c368c0edeae5eb93 100644 --- a/polkadot/Cargo.lock +++ b/polkadot/Cargo.lock @@ -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" diff --git a/polkadot/consensus/src/lib.rs b/polkadot/consensus/src/lib.rs index 4601b5b30d428cbcbbd76f2826e908ecaff98c5f..ccf330856b18b2cbe0277f9371c0b92a7d73a115 100644 --- a/polkadot/consensus/src/lib.rs +++ b/polkadot/consensus/src/lib.rs @@ -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() { diff --git a/polkadot/consensus/src/service.rs b/polkadot/consensus/src/service.rs index a4ad40a80afd121dee48eb444a49891283983005..a9a848e14b5efd51a789d7649e768a77de7f7b5b 100644 --- a/polkadot/consensus/src/service.rs +++ b/polkadot/consensus/src/service.rs @@ -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() diff --git a/polkadot/consensus/src/shared_table/mod.rs b/polkadot/consensus/src/shared_table/mod.rs index 7c0914ecf95bc352d7b34dfcbac73d56a5d8b6ba..34087566214a67738fcdbbdf2f266f26542c2bd5 100644 --- a/polkadot/consensus/src/shared_table/mod.rs +++ b/polkadot/consensus/src/shared_table/mod.rs @@ -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. diff --git a/polkadot/primitives/src/lib.rs b/polkadot/primitives/src/lib.rs index 01acdc4564b5b0090ddd1d31c43996d74364df6d..321060db3fb6a325c13dbe91ed480cd398601d7f 100644 --- a/polkadot/primitives/src/lib.rs +++ b/polkadot/primitives/src/lib.rs @@ -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>, } diff --git a/polkadot/primitives/src/parachain.rs b/polkadot/primitives/src/parachain.rs index 29fecaae735979bea15ee95f3b1574bc26743181..2e209011ab73504b89a9e287e67fcb25789653c0 100644 --- a/polkadot/primitives/src/parachain.rs +++ b/polkadot/primitives/src/parachain.rs @@ -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 { diff --git a/polkadot/runtime/Cargo.toml b/polkadot/runtime/Cargo.toml index 241e7c4450df1c408f34d576595f25010445a1a6..2398d27e73a74e2c3907d9bc6b7f793ebbde7a2f 100644 --- a/polkadot/runtime/Cargo.toml +++ b/polkadot/runtime/Cargo.toml @@ -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", diff --git a/polkadot/runtime/src/lib.rs b/polkadot/runtime/src/lib.rs index 133ce258dabfff3394f219e6acc6bb94d583cf21..4db8767dceb25fb26f12a21f5e84390daa644bf0 100644 --- a/polkadot/runtime/src/lib.rs +++ b/polkadot/runtime/src/lib.rs @@ -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")] diff --git a/polkadot/runtime/src/parachains.rs b/polkadot/runtime/src/parachains.rs index 0426ea7d5b5f12a11013ed90f7f517b6f418e96c..0f5e781e49939a9a7138df5c555c0ddc4cf59678 100644 --- a/polkadot/runtime/src/parachains.rs +++ b/polkadot/runtime/src/parachains.rs @@ -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()); + }); + } } diff --git a/polkadot/runtime/wasm/Cargo.lock b/polkadot/runtime/wasm/Cargo.lock index 5f6afdfae853734e17fa21b28171a8e22c972e01..0c7200296ab533faa6aff0344d27c3092bef354e 100644 --- a/polkadot/runtime/wasm/Cargo.lock +++ b/polkadot/runtime/wasm/Cargo.lock @@ -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" diff --git a/polkadot/runtime/wasm/Cargo.toml b/polkadot/runtime/wasm/Cargo.toml index a718d695a2d3e71c0dd6f58ab7771c194d2c34da..4282d32cea825cb2b06e171851a90f4fa554aa5c 100644 --- a/polkadot/runtime/wasm/Cargo.toml +++ b/polkadot/runtime/wasm/Cargo.toml @@ -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 } diff --git a/polkadot/runtime/wasm/target/wasm32-unknown-unknown/release/polkadot_runtime.compact.wasm b/polkadot/runtime/wasm/target/wasm32-unknown-unknown/release/polkadot_runtime.compact.wasm index e70997545ae5be376d36c4f0470da48828fe3d50..cd5656aa78794e8e182d60bf220736079c6fdb4d 100644 Binary files a/polkadot/runtime/wasm/target/wasm32-unknown-unknown/release/polkadot_runtime.compact.wasm and b/polkadot/runtime/wasm/target/wasm32-unknown-unknown/release/polkadot_runtime.compact.wasm differ diff --git a/polkadot/runtime/wasm/target/wasm32-unknown-unknown/release/polkadot_runtime.wasm b/polkadot/runtime/wasm/target/wasm32-unknown-unknown/release/polkadot_runtime.wasm index 665b570b511bc15ff96c2e413391cd88ab79ba7a..c5486a58a6db3fb32c497f79794b9730b080657f 100755 Binary files a/polkadot/runtime/wasm/target/wasm32-unknown-unknown/release/polkadot_runtime.wasm and b/polkadot/runtime/wasm/target/wasm32-unknown-unknown/release/polkadot_runtime.wasm differ diff --git a/polkadot/statement-table/src/generic.rs b/polkadot/statement-table/src/generic.rs index 2a6c828f5b3254fd902614bd5ad3769157c16801..6deece66df4a5c53e86f7bcd8443c6967c5b1eb0 100644 --- a/polkadot/statement-table/src/generic.rs +++ b/polkadot/statement-table/src/generic.rs @@ -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. diff --git a/polkadot/statement-table/src/lib.rs b/polkadot/statement-table/src/lib.rs index 3b10979fcd100d00a283f4eced2427a9492de156..91173aad680e28a23790d1b8f5297c0097849ffe 100644 --- a/polkadot/statement-table/src/lib.rs +++ b/polkadot/statement-table/src/lib.rs @@ -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), + } + } +}