Vote out offline authorities (#524)

* notify when an authority appears to have missed their block

* Runtime API

* offline tracker

* Move to consensus

* generating reports of offline indices

* stubbed-out evaluation logic

* Slashing data pathwat

* usize -> u32

* Slash bad validators.

* update to rhododendron 0.3

* fix compilation of polkadot-consensus

* Support offline noting in checked_block

* include offline reports in block authorship voting

* do not vote validators offline after some time

* add test for offline-tracker

* fix test build

* bump spec version

* update wasm

* Only allow validators that are possible to slash

* Fix grumble

* More idiomatic

* New Wasm.

* update rhododendron

* improve logging and reduce round time exponent

* format offline validators in ss58
This commit is contained in:
Robert Habermeier
2018-08-11 11:29:30 +02:00
committed by Gav Wood
parent c2b20fe5b0
commit e8f21cf0c9
33 changed files with 500 additions and 136 deletions
+7 -7
View File
@@ -1916,7 +1916,7 @@ dependencies = [
"polkadot-runtime 0.1.0",
"polkadot-statement-table 0.1.0",
"polkadot-transaction-pool 0.1.0",
"rhododendron 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"rhododendron 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
"substrate-bft 0.1.0",
"substrate-client 0.1.0",
"substrate-codec 0.1.0",
@@ -1947,7 +1947,7 @@ dependencies = [
"polkadot-availability-store 0.1.0",
"polkadot-consensus 0.1.0",
"polkadot-primitives 0.1.0",
"rhododendron 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"rhododendron 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
"substrate-bft 0.1.0",
"substrate-codec 0.1.0",
"substrate-codec-derive 0.1.0",
@@ -2290,7 +2290,7 @@ dependencies = [
[[package]]
name = "rhododendron"
version = "0.2.0"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"error-chain 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -2643,7 +2643,7 @@ dependencies = [
"futures 0.1.21 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
"parking_lot 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
"rhododendron 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"rhododendron 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
"substrate-codec 0.1.0",
"substrate-executor 0.1.0",
"substrate-keyring 0.1.0",
@@ -2819,7 +2819,7 @@ dependencies = [
name = "substrate-misbehavior-check"
version = "0.1.0"
dependencies = [
"rhododendron 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"rhododendron 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
"substrate-bft 0.1.0",
"substrate-codec 0.1.0",
"substrate-keyring 0.1.0",
@@ -3279,7 +3279,7 @@ dependencies = [
name = "substrate-test-client"
version = "0.1.0"
dependencies = [
"rhododendron 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"rhododendron 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
"substrate-bft 0.1.0",
"substrate-client 0.1.0",
"substrate-codec 0.1.0",
@@ -4164,7 +4164,7 @@ dependencies = [
"checksum regex-syntax 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "747ba3b235651f6e2f67dfa8bcdcd073ddb7c243cb21c442fc12395dfcac212d"
"checksum relay 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "1576e382688d7e9deecea24417e350d3062d97e32e45d70b1cde65994ff1489a"
"checksum remove_dir_all 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3488ba1b9a2084d38645c4c08276a1752dcbf2c7130d74f1569681ad5d2799c5"
"checksum rhododendron 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "9e38401cc1b63e71ec9119115c7e1354fcf54c8006ad59a22409dd8bd93737b2"
"checksum rhododendron 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3d2b4c928dfb981e491432f0809e93c99857112b0a348a93eee6b13e0bf0f0f3"
"checksum ring 0.12.1 (registry+https://github.com/rust-lang/crates.io-index)" = "6f7d28b30a72c01b458428e0ae988d4149c20d902346902be881e3edc4bb325c"
"checksum rlp 0.2.1 (git+https://github.com/paritytech/parity.git)" = "<none>"
"checksum rlp 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "89db7f8dfdd5eb7ab3ac3ece7a07fd273a680b4b224cb231181280e8996f9f0b"
+1
View File
@@ -121,6 +121,7 @@ impl Convert<AccountId, SessionKey> for SessionKeyConversion {
}
impl session::Trait for Concrete {
const NOTE_OFFLINE_POSITION: u32 = 1;
type ConvertAccountIdToSessionKey = SessionKeyConversion;
type OnSessionChange = Staking;
}
+19 -8
View File
@@ -25,8 +25,11 @@ use state_machine;
use runtime::Address;
use runtime_primitives::traits::AuxLookup;
use primitives::{AccountId, Block, Header, BlockId, Hash, Index, SessionKey, Timestamp, UncheckedExtrinsic};
use primitives::parachain::{CandidateReceipt, DutyRoster, Id as ParaId};
use primitives::{
AccountId, Block, Header, BlockId, Hash, Index, InherentData,
SessionKey, Timestamp, UncheckedExtrinsic,
};
use primitives::parachain::{DutyRoster, Id as ParaId};
use {BlockBuilder, PolkadotApi, LocalPolkadotApi, ErrorKind, Error, Result};
@@ -132,20 +135,20 @@ impl<B: LocalBackend<Block>> PolkadotApi for Client<B, LocalCallExecutor<B, Nati
with_runtime!(self, at, || ::runtime::Parachains::parachain_head(parachain))
}
fn build_block(&self, at: &BlockId, timestamp: Timestamp, new_heads: Vec<CandidateReceipt>) -> Result<Self::BlockBuilder> {
fn build_block(&self, at: &BlockId, inherent_data: InherentData) -> Result<Self::BlockBuilder> {
let mut block_builder = self.new_block_at(at)?;
for inherent in self.inherent_extrinsics(at, timestamp, new_heads)? {
for inherent in self.inherent_extrinsics(at, inherent_data)? {
block_builder.push(inherent)?;
}
Ok(block_builder)
}
fn inherent_extrinsics(&self, at: &BlockId, timestamp: Timestamp, new_heads: Vec<CandidateReceipt>) -> Result<Vec<UncheckedExtrinsic>> {
fn inherent_extrinsics(&self, at: &BlockId, inherent_data: InherentData) -> Result<Vec<UncheckedExtrinsic>> {
use codec::{Encode, Decode};
with_runtime!(self, at, || {
let extrinsics = ::runtime::inherent_extrinsics(timestamp, new_heads);
let extrinsics = ::runtime::inherent_extrinsics(inherent_data);
extrinsics.into_iter()
.map(|x| x.encode()) // get encoded representation
.map(|x| Decode::decode(&mut &x[..])) // get byte-vec equivalent to extrinsic
@@ -216,7 +219,11 @@ mod tests {
let client = client();
let id = BlockId::number(0);
let block_builder = client.build_block(&id, 1_000_000, Vec::new()).unwrap();
let block_builder = client.build_block(&id, InherentData {
timestamp: 1_000_000,
parachain_heads: Vec::new(),
offline_indices: Vec::new(),
}).unwrap();
let block = block_builder.bake().unwrap();
assert_eq!(block.header.number, 1);
@@ -228,7 +235,11 @@ mod tests {
let client = client();
let id = BlockId::number(0);
let inherent = client.inherent_extrinsics(&id, 1_000_000, Vec::new()).unwrap();
let inherent = client.inherent_extrinsics(&id, InherentData {
timestamp: 1_000_000,
parachain_heads: Vec::new(),
offline_indices: Vec::new(),
}).unwrap();
let mut block_builder = client.new_block_at(&id).unwrap();
for extrinsic in inherent {
+7 -5
View File
@@ -38,10 +38,12 @@ extern crate substrate_keyring as keyring;
pub mod full;
pub mod light;
use primitives::{AccountId, Block, BlockId, Hash, Index, SessionKey, Timestamp,
UncheckedExtrinsic};
use primitives::{
AccountId, Block, BlockId, Hash, Index, SessionKey, Timestamp,
UncheckedExtrinsic, InherentData,
};
use runtime::Address;
use primitives::parachain::{CandidateReceipt, DutyRoster, Id as ParaId};
use primitives::parachain::{DutyRoster, Id as ParaId};
error_chain! {
errors {
@@ -128,11 +130,11 @@ pub trait PolkadotApi {
fn evaluate_block(&self, at: &BlockId, block: Block) -> Result<bool>;
/// Build a block on top of the given, with inherent extrinsics pre-pushed.
fn build_block(&self, at: &BlockId, timestamp: Timestamp, new_heads: Vec<CandidateReceipt>) -> Result<Self::BlockBuilder>;
fn build_block(&self, at: &BlockId, inherent_data: InherentData) -> Result<Self::BlockBuilder>;
/// Attempt to produce the (encoded) inherent extrinsics for a block being built upon the given.
/// This may vary by runtime and will fail if a runtime doesn't follow the same API.
fn inherent_extrinsics(&self, at: &BlockId, timestamp: Timestamp, new_heads: Vec<CandidateReceipt>) -> Result<Vec<UncheckedExtrinsic>>;
fn inherent_extrinsics(&self, at: &BlockId, inherent_data: InherentData) -> Result<Vec<UncheckedExtrinsic>>;
}
/// Mark for all Polkadot API implementations, that are making use of state data, stored locally.
+7 -4
View File
@@ -20,9 +20,12 @@ use std::sync::Arc;
use client::backend::{Backend, RemoteBackend};
use client::{Client, CallExecutor};
use codec::Decode;
use primitives::{AccountId, Block, BlockId, Hash, Index, SessionKey, Timestamp, UncheckedExtrinsic};
use primitives::{
AccountId, Block, BlockId, Hash, Index, InherentData,
SessionKey, Timestamp, UncheckedExtrinsic,
};
use runtime::Address;
use primitives::parachain::{CandidateReceipt, DutyRoster, Id as ParaId};
use primitives::parachain::{DutyRoster, Id as ParaId};
use {PolkadotApi, BlockBuilder, RemotePolkadotApi, Result, ErrorKind};
/// Light block builder. TODO: make this work (efficiently)
@@ -92,11 +95,11 @@ impl<B: Backend<Block>, E: CallExecutor<Block>> PolkadotApi for RemotePolkadotAp
Err(ErrorKind::UnknownRuntime.into())
}
fn build_block(&self, _at: &BlockId, _timestamp: Timestamp, _new_heads: Vec<CandidateReceipt>) -> Result<Self::BlockBuilder> {
fn build_block(&self, _at: &BlockId, _inherent: InherentData) -> Result<Self::BlockBuilder> {
Err(ErrorKind::UnknownRuntime.into())
}
fn inherent_extrinsics(&self, _at: &BlockId, _timestamp: Timestamp, _new_heads: Vec<CandidateReceipt>) -> Result<Vec<Vec<u8>>> {
fn inherent_extrinsics(&self, _at: &BlockId, _inherent: InherentData) -> Result<Vec<Vec<u8>>> {
Err(ErrorKind::UnknownRuntime.into())
}
}
+1 -1
View File
@@ -11,7 +11,7 @@ ed25519 = { path = "../../substrate/ed25519" }
error-chain = "0.12"
log = "0.3"
exit-future = "0.1"
rhododendron = "0.2"
rhododendron = "0.3"
polkadot-api = { path = "../api" }
polkadot-availability-store = { path = "../availability-store" }
polkadot-parachain = { path = "../parachain" }
@@ -74,6 +74,9 @@ impl DynamicInclusion {
Some(now + until)
}
}
/// Get the start instant.
pub fn started_at(&self) -> Instant { self.start }
}
#[cfg(test)]
+98 -11
View File
@@ -69,7 +69,7 @@ use std::time::{Duration, Instant};
use codec::{Decode, Encode};
use extrinsic_store::Store as ExtrinsicStore;
use polkadot_api::PolkadotApi;
use polkadot_primitives::{Hash, Block, BlockId, BlockNumber, Header, Timestamp, SessionKey};
use polkadot_primitives::{AccountId, Hash, Block, BlockId, BlockNumber, Header, Timestamp, SessionKey};
use polkadot_primitives::parachain::{Id as ParaId, Chain, DutyRoster, BlockData, Extrinsic as ParachainExtrinsic, CandidateReceipt, CandidateSignature};
use primitives::AuthorityId;
use transaction_pool::TransactionPool;
@@ -80,20 +80,26 @@ use futures::prelude::*;
use futures::future;
use collation::CollationFetch;
use dynamic_inclusion::DynamicInclusion;
use parking_lot::RwLock;
pub use self::collation::{validate_collation, Collators};
pub use self::error::{ErrorKind, Error};
pub use self::offline_tracker::OfflineTracker;
pub use self::shared_table::{SharedTable, StatementProducer, ProducedStatements, Statement, SignedStatement, GenericStatement};
pub use service::Service;
mod dynamic_inclusion;
mod evaluation;
mod error;
mod offline_tracker;
mod service;
mod shared_table;
pub mod collation;
/// Shared offline validator tracker.
pub type SharedOfflineTracker = Arc<RwLock<OfflineTracker>>;
// block size limit.
const MAX_TRANSACTIONS_SIZE: usize = 4 * 1024 * 1024;
@@ -240,6 +246,8 @@ pub struct ProposerFactory<C, N, P> {
pub parachain_empty_duration: Duration,
/// Store for extrinsic data.
pub extrinsic_store: ExtrinsicStore,
/// Offline-tracker.
pub offline: SharedOfflineTracker,
}
impl<C, N, P> bft::Environment<Block> for ProposerFactory<C, N, P>
@@ -255,10 +263,11 @@ impl<C, N, P> bft::Environment<Block> for ProposerFactory<C, N, P>
type Output = N::Output;
type Error = Error;
fn init(&self,
fn init(
&self,
parent_header: &Header,
authorities: &[AuthorityId],
sign_with: Arc<ed25519::Pair>
sign_with: Arc<ed25519::Pair>,
) -> Result<(Self::Proposer, Self::Input, Self::Output), Error> {
use runtime_primitives::traits::{Hash as HashT, BlakeTwo256};
@@ -269,6 +278,9 @@ impl<C, N, P> bft::Environment<Block> for ProposerFactory<C, N, P>
let random_seed = self.client.random_seed(&id)?;
let random_seed = BlakeTwo256::hash(&*random_seed);
let validators = self.client.validators(&id)?;
self.offline.write().note_new_block(&validators[..]);
let (group_info, local_duty) = make_group_info(
duty_roster,
authorities,
@@ -326,6 +338,8 @@ impl<C, N, P> bft::Environment<Block> for ProposerFactory<C, N, P>
random_seed,
table,
transaction_pool: self.transaction_pool.clone(),
offline: self.offline.clone(),
validators,
_drop_signal: drop_signal,
};
@@ -403,9 +417,22 @@ pub struct Proposer<C: PolkadotApi> {
random_seed: Hash,
table: Arc<SharedTable>,
transaction_pool: Arc<TransactionPool<C>>,
offline: SharedOfflineTracker,
validators: Vec<AccountId>,
_drop_signal: exit_future::Signal,
}
impl<C: PolkadotApi + Send + Sync> Proposer<C> {
fn primary_index(&self, round_number: usize, len: usize) -> usize {
use primitives::uint::U256;
let big_len = U256::from(len);
let offset = U256::from_big_endian(&self.random_seed.0) % big_len;
let offset = offset.low_u64() as usize + round_number;
offset % len
}
}
impl<C> bft::Proposer<Block> for Proposer<C>
where
C: PolkadotApi + Send + Sync,
@@ -441,6 +468,8 @@ impl<C> bft::Proposer<Block> for Proposer<C>
client: self.client.clone(),
transaction_pool: self.transaction_pool.clone(),
table: self.table.clone(),
offline: self.offline.clone(),
validators: self.validators.clone(),
timing,
})
}
@@ -515,6 +544,13 @@ impl<C> bft::Proposer<Block> for Proposer<C>
includability_tracker.join(temporary_delay)
};
// refuse to vote if this block says a validator is offline that we
// think isn't.
let offline = proposal.noted_offline();
if !self.offline.read().check_consistency(&self.validators[..], offline) {
return Box::new(futures::empty());
}
// evaluate whether the block is actually valid.
// TODO: is it better to delay this until the delays are finished?
let evaluated = self.client
@@ -536,13 +572,8 @@ impl<C> bft::Proposer<Block> for Proposer<C>
}
fn round_proposer(&self, round_number: usize, authorities: &[AuthorityId]) -> AuthorityId {
use primitives::uint::U256;
let len: U256 = authorities.len().into();
let offset = U256::from_big_endian(&self.random_seed.0) % len;
let offset = offset.low_u64() as usize + round_number;
let proposer = authorities[offset % authorities.len()].clone();
let offset = self.primary_index(round_number, authorities.len());
let proposer = authorities[offset].clone();
trace!(target: "bft", "proposer for round {} is {}", round_number, proposer);
proposer
@@ -611,6 +642,36 @@ impl<C> bft::Proposer<Block> for Proposer<C>
.expect("locally signed extrinsic is valid; qed");
}
}
fn on_round_end(&self, round_number: usize, was_proposed: bool) {
let primary_validator = self.validators[
self.primary_index(round_number, self.validators.len())
];
// alter the message based on whether we think the empty proposer was forced to skip the round.
// this is determined by checking if our local validator would have been forced to skip the round.
let consider_online = was_proposed || {
let forced_delay = self.dynamic_inclusion.acceptable_in(Instant::now(), self.table.includable_count());
let public = ::ed25519::Public::from_raw(primary_validator.0);
match forced_delay {
None => info!(
"Potential Offline Validator: {} failed to propose during assigned slot: {}",
public,
round_number,
),
Some(_) => info!(
"Potential Offline Validator {} potentially forced to skip assigned slot: {}",
public,
round_number,
),
}
forced_delay.is_some()
};
self.offline.write().note_round_end(primary_validator, consider_online);
}
}
fn current_timestamp() -> Timestamp {
@@ -667,16 +728,42 @@ pub struct CreateProposal<C: PolkadotApi> {
transaction_pool: Arc<TransactionPool<C>>,
table: Arc<SharedTable>,
timing: ProposalTiming,
validators: Vec<AccountId>,
offline: SharedOfflineTracker,
}
impl<C> CreateProposal<C> where C: PolkadotApi {
fn propose_with(&self, candidates: Vec<CandidateReceipt>) -> Result<Block, Error> {
use polkadot_api::BlockBuilder;
use runtime_primitives::traits::{Hash as HashT, BlakeTwo256};
use polkadot_primitives::InherentData;
const MAX_VOTE_OFFLINE_SECONDS: Duration = Duration::from_secs(60);
// TODO: handle case when current timestamp behind that in state.
let timestamp = current_timestamp();
let mut block_builder = self.client.build_block(&self.parent_id, timestamp, candidates)?;
let elapsed_since_start = self.timing.dynamic_inclusion.started_at().elapsed();
let offline_indices = if elapsed_since_start > MAX_VOTE_OFFLINE_SECONDS {
Vec::new()
} else {
self.offline.read().reports(&self.validators[..])
};
if !offline_indices.is_empty() {
info!(
"Submitting offline validators {:?} for slash-vote",
offline_indices.iter().map(|&i| self.validators[i as usize]).collect::<Vec<_>>(),
)
}
let inherent_data = InherentData {
timestamp,
parachain_heads: candidates,
offline_indices,
};
let mut block_builder = self.client.build_block(&self.parent_id, inherent_data)?;
{
let mut unqueue_invalid = Vec::new();
@@ -0,0 +1,137 @@
// Copyright 2018 Parity Technologies (UK) Ltd.
// This file is part of Polkadot.
// Polkadot is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Polkadot is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Polkadot. If not, see <http://www.gnu.org/licenses/>.
//! Tracks offline validators.
use polkadot_primitives::AccountId;
use std::collections::HashMap;
use std::time::{Instant, Duration};
// time before we report a validator.
const REPORT_TIME: Duration = Duration::from_secs(60 * 5);
struct Observed {
last_round_end: Instant,
offline_since: Instant,
}
impl Observed {
fn new() -> Observed {
let now = Instant::now();
Observed {
last_round_end: now,
offline_since: now,
}
}
fn note_round_end(&mut self, was_online: bool) {
let now = Instant::now();
self.last_round_end = now;
if was_online {
self.offline_since = now;
}
}
fn is_active(&self) -> bool {
// can happen if clocks are not monotonic
if self.offline_since > self.last_round_end { return true }
self.last_round_end.duration_since(self.offline_since) < REPORT_TIME
}
}
/// Tracks offline validators and can issue a report for those offline.
pub struct OfflineTracker {
observed: HashMap<AccountId, Observed>,
}
impl OfflineTracker {
/// Create a new tracker.
pub fn new() -> Self {
OfflineTracker { observed: HashMap::new() }
}
/// Note new consensus is starting with the given set of validators.
pub fn note_new_block(&mut self, validators: &[AccountId]) {
use std::collections::HashSet;
let set: HashSet<_> = validators.iter().cloned().collect();
self.observed.retain(|k, _| set.contains(k));
}
/// Note that a round has ended.
pub fn note_round_end(&mut self, validator: AccountId, was_online: bool) {
self.observed.entry(validator)
.or_insert_with(Observed::new)
.note_round_end(was_online);
}
/// Generate a vector of indices for offline account IDs.
pub fn reports(&self, validators: &[AccountId]) -> Vec<u32> {
validators.iter()
.enumerate()
.filter_map(|(i, v)| if self.is_online(v) {
None
} else {
Some(i as u32)
})
.collect()
}
/// Whether reports on a validator set are consistent with our view of things.
pub fn check_consistency(&self, validators: &[AccountId], reports: &[u32]) -> bool {
reports.iter().cloned().all(|r| {
let v = match validators.get(r as usize) {
Some(v) => v,
None => return false,
};
// we must think all validators reported externally are offline.
let thinks_online = self.is_online(v);
!thinks_online
})
}
fn is_online(&self, v: &AccountId) -> bool {
self.observed.get(v).map(Observed::is_active).unwrap_or(true)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn validator_offline() {
let mut tracker = OfflineTracker::new();
let v = [0; 32].into();
let v2 = [1; 32].into();
let v3 = [2; 32].into();
tracker.note_round_end(v, true);
tracker.note_round_end(v2, true);
tracker.note_round_end(v3, true);
let slash_time = REPORT_TIME + Duration::from_secs(5);
tracker.observed.get_mut(&v).unwrap().offline_since -= slash_time;
tracker.observed.get_mut(&v2).unwrap().offline_since -= slash_time;
assert_eq!(tracker.reports(&[v, v2, v3]), vec![0, 1]);
tracker.note_new_block(&[v, v3]);
assert_eq!(tracker.reports(&[v, v2, v3]), vec![0]);
}
}
@@ -172,6 +172,9 @@ impl Service {
N::TableRouter: Send + 'static,
<N::Collation as IntoFuture>::Future: Send + 'static,
{
use parking_lot::RwLock;
use super::OfflineTracker;
let (signal, exit) = ::exit_future::signal();
let thread = thread::spawn(move || {
let mut runtime = LocalRuntime::new().expect("Could not create local runtime");
@@ -185,6 +188,7 @@ impl Service {
parachain_empty_duration,
handle: thread_pool.clone(),
extrinsic_store: extrinsic_store.clone(),
offline: Arc::new(RwLock::new(OfflineTracker::new())),
};
let bft_service = Arc::new(BftService::new(client.clone(), key, factory));
+1 -1
View File
@@ -19,4 +19,4 @@ ed25519 = { path = "../../substrate/ed25519" }
futures = "0.1"
tokio = "0.1.7"
log = "0.4"
rhododendron = "0.2"
rhododendron = "0.3"
+11
View File
@@ -110,3 +110,14 @@ pub type BlockId = generic::BlockId<Block>;
#[derive(PartialEq, Eq, Clone, Default, Encode, Decode)]
#[cfg_attr(feature = "std", derive(Serialize, Deserialize, Debug))]
pub struct Log(#[cfg_attr(feature = "std", serde(with="bytes"))] pub Vec<u8>);
/// Inherent data to include in a block.
#[derive(Encode, Decode)]
pub struct InherentData {
/// Current timestamp.
pub timestamp: Timestamp,
/// Parachain heads update.
pub parachain_heads: Vec<::parachain::CandidateReceipt>,
/// Indices of offline validators.
pub offline_indices: Vec<u32>,
}
@@ -16,9 +16,10 @@
//! Typesafe block interaction.
use super::{Call, Block, TIMESTAMP_SET_POSITION, PARACHAINS_SET_POSITION};
use super::{Call, Block, TIMESTAMP_SET_POSITION, PARACHAINS_SET_POSITION, NOTE_OFFLINE_POSITION};
use timestamp::Call as TimestampCall;
use parachains::Call as ParachainsCall;
use session::Call as SessionCall;
use primitives::parachain::CandidateReceipt;
/// Provides a type-safe wrapper around a structurally valid block.
@@ -47,6 +48,7 @@ impl CheckedBlock {
});
if !has_heads { return Err(block) }
Ok(CheckedBlock {
inner: block,
file_line: None,
@@ -88,6 +90,14 @@ impl CheckedBlock {
}
}
/// Extract the noted offline validator indices (if any) from the block.
pub fn noted_offline(&self) -> &[u32] {
self.inner.extrinsics.get(NOTE_OFFLINE_POSITION as usize).and_then(|xt| match xt.extrinsic.function {
Call::Session(SessionCall::note_offline(ref x)) => Some(&x[..]),
_ => None,
}).unwrap_or(&[])
}
/// Convert into inner block.
pub fn into_inner(self) -> Block { self.inner }
}
+5 -2
View File
@@ -85,6 +85,8 @@ pub use primitives::Header;
pub const TIMESTAMP_SET_POSITION: u32 = 0;
/// The position of the parachains set extrinsic.
pub const PARACHAINS_SET_POSITION: u32 = 1;
/// The position of the offline nodes noting extrinsic.
pub const NOTE_OFFLINE_POSITION: u32 = 2;
/// The address format for describing accounts.
pub type Address = staking::Address<Concrete>;
@@ -110,7 +112,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion {
spec_name: ver_str!("polkadot"),
impl_name: ver_str!("parity-polkadot"),
authoring_version: 1,
spec_version: 100,
spec_version: 101,
impl_version: 0,
};
@@ -160,6 +162,7 @@ impl Convert<AccountId, SessionKey> for SessionKeyConversion {
}
impl session::Trait for Concrete {
const NOTE_OFFLINE_POSITION: u32 = NOTE_OFFLINE_POSITION;
type ConvertAccountIdToSessionKey = SessionKeyConversion;
type OnSessionChange = Staking;
}
@@ -247,7 +250,7 @@ pub mod api {
apply_extrinsic => |extrinsic| super::Executive::apply_extrinsic(extrinsic),
execute_block => |block| super::Executive::execute_block(block),
finalise_block => |()| super::Executive::finalise_block(),
inherent_extrinsics => |(timestamp, heads)| super::inherent_extrinsics(timestamp, heads),
inherent_extrinsics => |inherent| super::inherent_extrinsics(inherent),
validator_count => |()| super::Session::validator_count(),
validators => |()| super::Session::validators()
);
@@ -268,6 +268,7 @@ mod tests {
type Header = Header;
}
impl session::Trait for Test {
const NOTE_OFFLINE_POSITION: u32 = 1;
type ConvertAccountIdToSessionKey = Identity;
type OnSessionChange = ();
}
+23 -20
View File
@@ -19,30 +19,33 @@
use rstd::prelude::*;
use super::{Call, UncheckedExtrinsic, Extrinsic, Staking};
use runtime_primitives::traits::{Checkable, AuxLookup};
use primitives::parachain::CandidateReceipt;
use timestamp::Call as TimestampCall;
use parachains::Call as ParachainsCall;
use session::Call as SessionCall;
/// Produces the list of inherent extrinsics.
pub fn inherent_extrinsics(timestamp: ::primitives::Timestamp, parachain_heads: Vec<CandidateReceipt>) -> Vec<UncheckedExtrinsic> {
vec![
UncheckedExtrinsic::new(
Extrinsic {
signed: Default::default(),
function: Call::Timestamp(TimestampCall::set(timestamp)),
index: 0,
},
Default::default()
),
UncheckedExtrinsic::new(
Extrinsic {
signed: Default::default(),
function: Call::Parachains(ParachainsCall::set_heads(parachain_heads)),
index: 0,
},
Default::default()
)
]
pub fn inherent_extrinsics(data: ::primitives::InherentData) -> Vec<UncheckedExtrinsic> {
let make_inherent = |function| UncheckedExtrinsic::new(
Extrinsic {
signed: Default::default(),
function,
index: 0,
},
Default::default(),
);
let mut inherent = vec![
make_inherent(Call::Timestamp(TimestampCall::set(data.timestamp))),
make_inherent(Call::Parachains(ParachainsCall::set_heads(data.parachain_heads))),
];
if !data.offline_indices.is_empty() {
inherent.push(make_inherent(
Call::Session(SessionCall::note_offline(data.offline_indices))
));
}
inherent
}
/// Checks an unchecked extrinsic for validity.
+15 -10
View File
@@ -416,7 +416,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"libc 0.2.41 (registry+https://github.com/rust-lang/crates.io-index)",
"rand 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
"smallvec 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)",
"smallvec 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
]
@@ -466,7 +466,7 @@ version = "0.1.0"
dependencies = [
"integer-sqrt 0.1.0 (git+https://github.com/paritytech/integer-sqrt-rs.git)",
"polkadot-primitives 0.1.0",
"safe-mix 0.1.0",
"safe-mix 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
"substrate-codec 0.1.0",
"substrate-primitives 0.1.0",
"substrate-runtime-consensus 0.1.0",
@@ -652,7 +652,8 @@ dependencies = [
[[package]]
name = "safe-mix"
version = "0.1.0"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"rustc_version 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
]
@@ -692,8 +693,11 @@ dependencies = [
[[package]]
name = "smallvec"
version = "0.6.1"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"unreachable 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "stable_deref_trait"
@@ -766,7 +770,7 @@ version = "0.1.0"
dependencies = [
"hex-literal 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
"integer-sqrt 0.1.0 (git+https://github.com/paritytech/integer-sqrt-rs.git)",
"safe-mix 0.1.0",
"safe-mix 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.64 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_derive 1.0.64 (registry+https://github.com/rust-lang/crates.io-index)",
"substrate-codec 0.1.0",
@@ -788,7 +792,7 @@ name = "substrate-runtime-democracy"
version = "0.1.0"
dependencies = [
"hex-literal 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
"safe-mix 0.1.0",
"safe-mix 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.64 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_derive 1.0.64 (registry+https://github.com/rust-lang/crates.io-index)",
"substrate-codec 0.1.0",
@@ -867,7 +871,7 @@ name = "substrate-runtime-session"
version = "0.1.0"
dependencies = [
"hex-literal 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
"safe-mix 0.1.0",
"safe-mix 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.64 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_derive 1.0.64 (registry+https://github.com/rust-lang/crates.io-index)",
"substrate-codec 0.1.0",
@@ -887,7 +891,7 @@ name = "substrate-runtime-staking"
version = "0.1.0"
dependencies = [
"hex-literal 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
"safe-mix 0.1.0",
"safe-mix 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.64 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_derive 1.0.64 (registry+https://github.com/rust-lang/crates.io-index)",
"substrate-codec 0.1.0",
@@ -932,7 +936,7 @@ name = "substrate-runtime-system"
version = "0.1.0"
dependencies = [
"hex-literal 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
"safe-mix 0.1.0",
"safe-mix 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.64 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_derive 1.0.64 (registry+https://github.com/rust-lang/crates.io-index)",
"substrate-codec 0.1.0",
@@ -1222,12 +1226,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
"checksum rustc-hex 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "0ceb8ce7a5e520de349e1fa172baeba4a9e8d5ef06c47471863530bc4972ee1e"
"checksum rustc-hex 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d2b03280c2813907a030785570c577fb27d3deec8da4c18566751ade94de0ace"
"checksum rustc_version 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a54aa04a10c68c1c4eacb4337fd883b435997ede17a9385784b990777686b09a"
"checksum safe-mix 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7f7bf422d23a88c16d5090d455f182bc99c60af4df6a345c63428acf5129e347"
"checksum scopeguard 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "94258f53601af11e6a49f722422f6e3425c52b06245a5cf9bc09908b174f5e27"
"checksum semver 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403"
"checksum semver-parser 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
"checksum serde 1.0.64 (registry+https://github.com/rust-lang/crates.io-index)" = "fba5be06346c5200249c8c8ca4ccba4a09e8747c71c16e420bd359a0db4d8f91"
"checksum serde_derive 1.0.64 (registry+https://github.com/rust-lang/crates.io-index)" = "79e4620ba6fbe051fc7506fab6f84205823564d55da18d55b695160fb3479cd8"
"checksum smallvec 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)" = "03dab98ab5ded3a8b43b2c80751194608d0b2aa0f1d46cf95d1c35e192844aa7"
"checksum smallvec 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)" = "211a489e65e94b103926d2054ae515a1cdb5d515ea0ef414fee23b7e043ce748"
"checksum stable_deref_trait 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "15132e0e364248108c5e2c02e3ab539be8d6f5d52a01ca9bbf27ed657316f02b"
"checksum syn 0.14.1 (registry+https://github.com/rust-lang/crates.io-index)" = "6dfd71b2be5a58ee30a6f8ea355ba8290d397131c00dfa55c3d34e6e13db5101"
"checksum termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "689a3bdfaab439fd92bc87df5c4c78417d3cbe537487274e9b0b2dce76e92096"
@@ -286,7 +286,7 @@ impl<'a, A> txpool::Verifier<UncheckedExtrinsic> for Verifier<'a, A> where
let encoded = uxt.encode();
let (encoded_size, hash) = (encoded.len(), BlakeTwo256::hash(&encoded));
debug!(target: "transaction-pool", "Transaction submitted: {}", ::substrate_primitives::hexdisplay::HexDisplay::from(&encoded));
let inner = match uxt.clone().check_with(|a| self.lookup(a)) {
@@ -446,10 +446,10 @@ mod tests {
use substrate_keyring::Keyring::{self, *};
use codec::{Decode, Encode};
use polkadot_api::{PolkadotApi, BlockBuilder, Result};
use primitives::{AccountId, AccountIndex, Block, BlockId, Hash, Index, SessionKey, Timestamp,
use primitives::{AccountId, AccountIndex, Block, BlockId, Hash, Index, SessionKey,
UncheckedExtrinsic as FutureProofUncheckedExtrinsic};
use runtime::{RawAddress, Call, TimestampCall, BareExtrinsic, Extrinsic, UncheckedExtrinsic};
use primitives::parachain::{CandidateReceipt, DutyRoster, Id as ParaId};
use primitives::parachain::{DutyRoster, Id as ParaId};
use substrate_runtime_primitives::{MaybeUnsigned, generic};
struct TestBlockBuilder;
@@ -494,8 +494,8 @@ mod tests {
fn active_parachains(&self, _at: &BlockId) -> Result<Vec<ParaId>> { unimplemented!() }
fn parachain_code(&self, _at: &BlockId, _parachain: ParaId) -> Result<Option<Vec<u8>>> { unimplemented!() }
fn parachain_head(&self, _at: &BlockId, _parachain: ParaId) -> Result<Option<Vec<u8>>> { unimplemented!() }
fn build_block(&self, _at: &BlockId, _timestamp: Timestamp, _new_heads: Vec<CandidateReceipt>) -> Result<Self::BlockBuilder> { unimplemented!() }
fn inherent_extrinsics(&self, _at: &BlockId, _timestamp: Timestamp, _new_heads: Vec<CandidateReceipt>) -> Result<Vec<Vec<u8>>> { unimplemented!() }
fn build_block(&self, _at: &BlockId, _inherent: ::primitives::InherentData) -> Result<Self::BlockBuilder> { unimplemented!() }
fn inherent_extrinsics(&self, _at: &BlockId, _inherent: ::primitives::InherentData) -> Result<Vec<Vec<u8>>> { unimplemented!() }
fn index(&self, _at: &BlockId, _account: AccountId) -> Result<Index> {
Ok((_account[0] as u32) + number_of(_at))
+1 -1
View File
@@ -15,7 +15,7 @@ tokio = "0.1.7"
parking_lot = "0.4"
error-chain = "0.12"
log = "0.3"
rhododendron = "0.2"
rhododendron = "0.3"
[dev-dependencies]
substrate-keyring = { path = "../keyring" }
+56 -10
View File
@@ -56,6 +56,7 @@ extern crate error_chain;
use std::mem;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::{Instant, Duration};
use codec::Encode;
use ed25519::LocalizedSignature;
@@ -69,7 +70,7 @@ use futures::sync::oneshot;
use tokio::timer::Delay;
use parking_lot::Mutex;
pub use rhododendron::InputStreamConcluded;
pub use rhododendron::{InputStreamConcluded, AdvanceRoundReason};
pub use error::{Error, ErrorKind};
/// Messages over the proposal.
@@ -184,6 +185,9 @@ pub trait Proposer<B: Block> {
/// Determine the proposer for a given round. This should be a deterministic function
/// with consistent results across all authorities.
fn round_proposer(&self, round_number: usize, authorities: &[AuthorityId]) -> AuthorityId;
/// Hook called when a BFT round advances without a proposal.
fn on_round_end(&self, _round_number: usize, _proposed: bool) { }
}
/// Block import trait.
@@ -207,6 +211,26 @@ struct BftInstance<B: Block, P> {
proposer: P,
}
impl<B: Block, P: Proposer<B>> BftInstance<B, P>
where
B: Clone + Eq,
B::Hash: ::std::hash::Hash
{
fn round_timeout_duration(&self, round: usize) -> Duration {
const ROUND_INCREMENT_STEP: usize = 10000;
let round = round / ROUND_INCREMENT_STEP;
let round = ::std::cmp::min(63, round) as u32;
let timeout = 1u64.checked_shl(round)
.unwrap_or_else(u64::max_value)
.saturating_mul(self.round_timeout_multiplier);
Duration::from_secs(timeout)
}
}
impl<B: Block, P: Proposer<B>> rhododendron::Context for BftInstance<B, P>
where
B: Clone + Eq,
@@ -246,20 +270,39 @@ impl<B: Block, P: Proposer<B>> rhododendron::Context for BftInstance<B, P>
}
fn begin_round_timeout(&self, round: usize) -> Self::RoundTimeout {
use std::time::{Instant, Duration};
let round = round / 3;
let round = ::std::cmp::min(63, round) as u32;
let timeout = 1u64.checked_shl(round)
.unwrap_or_else(u64::max_value)
.saturating_mul(self.round_timeout_multiplier);
let fut = Delay::new(Instant::now() + Duration::from_secs(timeout))
let timeout = self.round_timeout_duration(round);
let fut = Delay::new(Instant::now() + timeout)
.map_err(|e| Error::from(ErrorKind::FaultyTimer(e)))
.map_err(Into::into);
Box::new(fut)
}
fn on_advance_round(
&self,
accumulator: &::rhododendron::Accumulator<B, B::Hash, Self::AuthorityId, Self::Signature>,
round: usize,
next_round: usize,
reason: AdvanceRoundReason,
) {
use std::collections::HashSet;
let collect_pubkeys = |participants: HashSet<&Self::AuthorityId>| participants.into_iter()
.map(|p| ::ed25519::Public::from_raw(p.0))
.collect::<Vec<_>>();
let round_timeout = self.round_timeout_duration(next_round);
debug!(target: "bft", "Advancing to round {} from {}", next_round, round);
debug!(target: "bft", "Participating authorities: {:?}",
collect_pubkeys(accumulator.participants()));
debug!(target: "bft", "Voting authorities: {:?}",
collect_pubkeys(accumulator.voters()));
debug!(target: "bft", "Round {} should end in at most {} seconds from now", next_round, round_timeout.as_secs());
if let AdvanceRoundReason::Timeout = reason {
self.proposer.on_round_end(round, accumulator.proposal().is_some());
}
}
}
/// A future that resolves either when canceled (witnessing a block from the network at same height)
@@ -303,6 +346,9 @@ impl<B, P, I, InStream, OutSink> Future for BftFuture<B, P, I, InStream, OutSink
// TODO: handle and log this error in a way which isn't noisy on exit.
let committed = try_ready!(self.inner.poll().map_err(|_| ()));
// if something was committed, the round leader must have proposed.
self.inner.context().proposer.on_round_end(committed.round_number, true);
// If we didn't see the proposal (very unlikely),
// we will get the block from the network later.
if let Some(justified_block) = committed.candidate {
@@ -11,7 +11,7 @@ substrate-runtime-io = { path = "../runtime-io", default-features = false }
[dev-dependencies]
substrate-bft = { path = "../bft" }
rhododendron = "0.2"
rhododendron = "0.3"
substrate-keyring = { path = "../keyring" }
[features]
@@ -54,6 +54,7 @@ impl staking::Trait for Test {
type OnAccountKill = Contract;
}
impl session::Trait for Test {
const NOTE_OFFLINE_POSITION: u32 = 1;
type ConvertAccountIdToSessionKey = Identity;
type OnSessionChange = Staking;
}
@@ -649,6 +649,7 @@ mod tests {
type Header = Header;
}
impl session::Trait for Test {
const NOTE_OFFLINE_POSITION: u32 = 1;
type ConvertAccountIdToSessionKey = Identity;
type OnSessionChange = staking::Module<Test>;
}
@@ -392,6 +392,7 @@ mod tests {
type Header = Header;
}
impl session::Trait for Test {
const NOTE_OFFLINE_POSITION: u32 = 1;
type ConvertAccountIdToSessionKey = Identity;
type OnSessionChange = staking::Module<Test>;
}
@@ -495,7 +496,7 @@ mod tests {
assert_eq!(Democracy::tally(r), (10, 0));
assert_eq!(Democracy::end_block(System::block_number()), Ok(()));
Staking::on_session_change(true, 0);
Staking::on_session_change(0, Vec::new());
assert_eq!(Staking::era_length(), 2);
});
@@ -573,19 +574,19 @@ mod tests {
System::set_block_number(1);
assert_ok!(Democracy::vote(&1, 0, true));
assert_eq!(Democracy::end_block(System::block_number()), Ok(()));
Staking::on_session_change(true, 0);
Staking::on_session_change(0, Vec::new());
assert_eq!(Staking::bonding_duration(), 4);
System::set_block_number(2);
assert_ok!(Democracy::vote(&1, 1, true));
assert_eq!(Democracy::end_block(System::block_number()), Ok(()));
Staking::on_session_change(true, 0);
Staking::on_session_change(0, Vec::new());
assert_eq!(Staking::bonding_duration(), 3);
System::set_block_number(3);
assert_ok!(Democracy::vote(&1, 2, true));
assert_eq!(Democracy::end_block(System::block_number()), Ok(()));
Staking::on_session_change(true, 0);
Staking::on_session_change(0, Vec::new());
assert_eq!(Staking::bonding_duration(), 2);
});
}
@@ -606,7 +607,7 @@ mod tests {
assert_eq!(Democracy::tally(r), (10, 0));
assert_eq!(Democracy::end_block(System::block_number()), Ok(()));
Staking::on_session_change(true, 0);
Staking::on_session_change(0, Vec::new());
assert_eq!(Staking::era_length(), 2);
});
@@ -621,7 +622,7 @@ mod tests {
assert_ok!(Democracy::cancel_referendum(r));
assert_eq!(Democracy::end_block(System::block_number()), Ok(()));
Staking::on_session_change(true, 0);
Staking::on_session_change(0, Vec::new());
assert_eq!(Staking::era_length(), 1);
});
@@ -639,7 +640,7 @@ mod tests {
assert_eq!(Democracy::tally(r), (0, 10));
assert_eq!(Democracy::end_block(System::block_number()), Ok(()));
Staking::on_session_change(true, 0);
Staking::on_session_change(0, Vec::new());
assert_eq!(Staking::era_length(), 1);
});
@@ -660,7 +661,7 @@ mod tests {
assert_eq!(Democracy::tally(r), (110, 100));
assert_eq!(Democracy::end_block(System::block_number()), Ok(()));
Staking::on_session_change(true, 0);
Staking::on_session_change(0, Vec::new());
assert_eq!(Staking::era_length(), 2);
});
@@ -677,7 +678,7 @@ mod tests {
assert_eq!(Democracy::tally(r), (60, 50));
assert_eq!(Democracy::end_block(System::block_number()), Ok(()));
Staking::on_session_change(true, 0);
Staking::on_session_change(0, Vec::new());
assert_eq!(Staking::era_length(), 1);
});
@@ -698,7 +699,7 @@ mod tests {
assert_eq!(Democracy::tally(r), (100, 50));
assert_eq!(Democracy::end_block(System::block_number()), Ok(()));
Staking::on_session_change(true, 0);
Staking::on_session_change(0, Vec::new());
assert_eq!(Staking::era_length(), 2);
});
@@ -252,6 +252,7 @@ mod tests {
type Header = Header;
}
impl session::Trait for Test {
const NOTE_OFFLINE_POSITION: u32 = 1;
type ConvertAccountIdToSessionKey = Identity;
type OnSessionChange = staking::Module<Test>;
}
+37 -16
View File
@@ -46,23 +46,26 @@ extern crate substrate_runtime_system as system;
extern crate substrate_runtime_timestamp as timestamp;
use rstd::prelude::*;
use primitives::traits::{Zero, One, RefInto, Executable, Convert, As};
use primitives::traits::{Zero, One, RefInto, MaybeEmpty, Executable, Convert, As};
use runtime_support::{StorageValue, StorageMap};
use runtime_support::dispatch::Result;
/// A session has changed.
pub trait OnSessionChange<T> {
pub trait OnSessionChange<T, A> {
/// Session has changed.
fn on_session_change(normal_rotation: bool, time_elapsed: T);
fn on_session_change(time_elapsed: T, bad_validators: Vec<A>);
}
impl<T> OnSessionChange<T> for () {
fn on_session_change(_: bool, _: T) {}
impl<T, A> OnSessionChange<T, A> for () {
fn on_session_change(_: T, _: Vec<A>) {}
}
pub trait Trait: timestamp::Trait {
// the position of the required timestamp-set extrinsic.
const NOTE_OFFLINE_POSITION: u32;
type ConvertAccountIdToSessionKey: Convert<Self::AccountId, Self::SessionKey>;
type OnSessionChange: OnSessionChange<Self::Moment>;
type OnSessionChange: OnSessionChange<Self::Moment, Self::AccountId>;
}
decl_module! {
@@ -71,6 +74,7 @@ decl_module! {
#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
pub enum Call where aux: T::PublicAux {
fn set_key(aux, key: T::SessionKey) -> Result = 0;
fn note_offline(aux, offline_val_indices: Vec<u32>) -> Result = 1;
}
#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
@@ -94,6 +98,10 @@ decl_storage! {
// Percent by which the session must necessarily finish late before we early-exit the session.
pub BrokenPercentLate get(broken_percent_late): b"ses:broken_percent_late" => required T::Moment;
// Opinions of the current validator set about the activeness of their peers.
// Gets cleared when the validator set changes.
pub BadValidators get(bad_validators): b"ses:bad_validators" => Vec<T::AccountId>;
// New session is being forced is this entry exists; in which case, the boolean value is whether
// the new session should be considered a normal rotation (rewardable) or exceptional (slashable).
pub ForcingNewSession get(forcing_new_session): b"ses:forcing_new_session" => bool;
@@ -136,6 +144,20 @@ impl<T: Trait> Module<T> {
Ok(())
}
/// Notes which of the validators appear to be online from the point of the view of the block author.
pub fn note_offline(aux: &T::PublicAux, offline_val_indices: Vec<u32>) -> Result {
assert!(aux.is_empty());
assert!(
<system::Module<T>>::extrinsic_index() == T::NOTE_OFFLINE_POSITION,
"note_offline extrinsic must be at position {} in the block",
T::NOTE_OFFLINE_POSITION
);
let vs = Self::validators();
<BadValidators<T>>::put(offline_val_indices.into_iter().map(|i| vs[i as usize].clone()).collect::<Vec<T::AccountId>>());
Ok(())
}
// INTERNAL API (available to other runtime modules)
/// Set the current set of validators.
@@ -156,17 +178,15 @@ impl<T: Trait> Module<T> {
// check block number and call next_session if necessary.
let block_number = <system::Module<T>>::block_number();
let is_final_block = ((block_number - Self::last_length_change()) % Self::length()).is_zero();
let broken_validation = Self::broken_validation();
if let Some(normal_rotation) = Self::forcing_new_session() {
Self::rotate_session(normal_rotation, is_final_block);
<ForcingNewSession<T>>::kill();
} else if is_final_block || broken_validation {
Self::rotate_session(!broken_validation, is_final_block);
let bad_validators = <BadValidators<T>>::take().unwrap_or_default();
let should_end_session = <ForcingNewSession<T>>::take().is_some() || !bad_validators.is_empty() || is_final_block;
if should_end_session {
Self::rotate_session(is_final_block, bad_validators);
}
}
/// Move onto next session: register the new authority set.
pub fn rotate_session(normal_rotation: bool, is_final_block: bool) {
pub fn rotate_session(is_final_block: bool, bad_validators: Vec<T::AccountId>) {
let now = <timestamp::Module<T>>::get();
let time_elapsed = now.clone() - Self::current_start();
@@ -186,7 +206,7 @@ impl<T: Trait> Module<T> {
<LastLengthChange<T>>::put(block_number);
}
T::OnSessionChange::on_session_change(normal_rotation, time_elapsed);
T::OnSessionChange::on_session_change(time_elapsed, bad_validators);
// Update any changes in session keys.
Self::validators().iter().enumerate().for_each(|(i, v)| {
@@ -301,6 +321,7 @@ mod tests {
type Moment = u64;
}
impl Trait for Test {
const NOTE_OFFLINE_POSITION: u32 = 1;
type ConvertAccountIdToSessionKey = Identity;
type OnSessionChange = ();
}
@@ -350,7 +371,7 @@ mod tests {
assert_eq!(Session::ideal_session_duration(), 15);
// ideal end = 0 + 15 * 3 = 15
// broken_limit = 15 * 130 / 100 = 19
System::set_block_number(3);
assert_eq!(Session::blocks_remaining(), 2);
Timestamp::set_timestamp(9); // earliest end = 9 + 2 * 5 = 19; OK.
@@ -378,7 +399,7 @@ mod tests {
assert_eq!(Session::blocks_remaining(), 0);
Session::check_rotate_session();
assert_eq!(Session::length(), 10);
System::set_block_number(7);
assert_eq!(Session::current_index(), 1);
assert_eq!(Session::blocks_remaining(), 5);
+33 -22
View File
@@ -177,6 +177,9 @@ decl_storage! {
// The current era stake threshold
pub StakeThreshold get(stake_threshold): b"sta:stake_threshold" => required T::Balance;
// The current bad validator slash.
pub CurrentSlash get(current_slash): b"sta:current_slash" => default T::Balance;
// The next free enumeration set.
pub NextEnumSet get(next_enum_set): b"sta:next_enum" => required T::AccountIndex;
// The enumeration sets.
@@ -589,10 +592,30 @@ impl<T: Trait> Module<T> {
/// Session has just changed. We need to determine whether we pay a reward, slash and/or
/// move to a new era.
fn new_session(normal_rotation: bool, actual_elapsed: T::Moment) {
fn new_session(actual_elapsed: T::Moment, bad_validators: Vec<T::AccountId>) {
let session_index = <session::Module<T>>::current_index();
let early_exit_era = !bad_validators.is_empty();
if early_exit_era {
// slash
let slash = Self::current_slash() + Self::early_era_slash();
<CurrentSlash<T>>::put(&slash);
for v in bad_validators.into_iter() {
if let Some(rem) = Self::slash(&v, slash) {
let noms = Self::current_nominators_for(&v);
let total = noms.iter().map(Self::voting_balance).fold(T::Balance::zero(), |acc, x| acc + x);
if !total.is_zero() {
let safe_mul_rational = |b| b * rem / total;// TODO: avoid overflow
for n in noms.iter() {
let _ = Self::slash(n, safe_mul_rational(Self::voting_balance(n))); // best effort - not much that can be done on fail.
}
}
}
}
} else {
// Zero any cumulative slash since we're healthy now.
<CurrentSlash<T>>::kill();
if normal_rotation {
// reward
let ideal_elapsed = <session::Module<T>>::ideal_session_duration();
let per65536: u64 = (T::Moment::sa(65536u64) * ideal_elapsed.clone() / actual_elapsed.max(ideal_elapsed)).as_();
@@ -609,25 +632,10 @@ impl<T: Trait> Module<T> {
let _ = Self::reward(v, safe_mul_rational(Self::voting_balance(v)));
}
}
} else {
// slash
let early_era_slash = Self::early_era_slash();
for v in <session::Module<T>>::validators().iter() {
if let Some(rem) = Self::slash(v, early_era_slash) {
let noms = Self::current_nominators_for(v);
let total = noms.iter().map(Self::voting_balance).fold(T::Balance::zero(), |acc, x| acc + x);
if !total.is_zero() {
let safe_mul_rational = |b| b * rem / total;// TODO: avoid overflow
for n in noms.iter() {
let _ = Self::slash(n, safe_mul_rational(Self::voting_balance(n))); // best effort - not much that can be done on fail.
}
}
}
}
}
if <ForcingNewEra<T>>::take().is_some()
|| ((session_index - Self::last_era_length_change()) % Self::sessions_per_era()).is_zero()
|| !normal_rotation
|| early_exit_era
{
Self::new_era();
}
@@ -654,6 +662,8 @@ impl<T: Trait> Module<T> {
}
}
let minimum_allowed = Self::early_era_slash();
// evaluate desired staking amounts and nominations and optimise to find the best
// combination of validators, then use session::internal::set_validators().
// for now, this just orders would-be stakers by their balances and chooses the top-most
@@ -662,11 +672,12 @@ impl<T: Trait> Module<T> {
let mut intentions = <Intentions<T>>::get()
.into_iter()
.map(|v| (Self::voting_balance(&v) + Self::nomination_balance(&v), v))
.filter(|&(b, _)| b >= minimum_allowed)
.collect::<Vec<_>>();
intentions.sort_unstable_by(|&(ref b1, _), &(ref b2, _)| b2.cmp(&b1));
<StakeThreshold<T>>::put(
if intentions.len() > 0 {
if !intentions.is_empty() {
let i = (<ValidatorCount<T>>::get() as usize).min(intentions.len() - 1);
intentions[i].0.clone()
} else { Zero::zero() }
@@ -797,9 +808,9 @@ impl<T: Trait> Executable for Module<T> {
}
}
impl<T: Trait> OnSessionChange<T::Moment> for Module<T> {
fn on_session_change(normal_rotation: bool, elapsed: T::Moment) {
Self::new_session(normal_rotation, elapsed);
impl<T: Trait> OnSessionChange<T::Moment, T::AccountId> for Module<T> {
fn on_session_change(elapsed: T::Moment, bad_validators: Vec<T::AccountId>) {
Self::new_session(elapsed, bad_validators);
}
}
@@ -45,6 +45,7 @@ impl system::Trait for Test {
type Header = Header;
}
impl session::Trait for Test {
const NOTE_OFFLINE_POSITION: u32 = 1;
type ConvertAccountIdToSessionKey = Identity;
type OnSessionChange = Staking;
}
+1 -1
View File
@@ -4,7 +4,7 @@ version = "0.1.0"
authors = ["Parity Technologies <admin@parity.io>"]
[dependencies]
rhododendron = "0.2"
rhododendron = "0.3"
substrate-bft = { path = "../bft" }
substrate-client = { path = "../client" }
substrate-codec = { path = "../codec" }