Unverified Commit 4b0c4968 authored by asynchronous rob's avatar asynchronous rob Committed by GitHub
Browse files

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: asynchronous rob's avatarrphmeier <rphmeier@gmail.com>
parent a35ddc58
Pipeline #26335 passed with stages
in 15 minutes and 18 seconds
......@@ -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,