From bc325eca66e96a8f907eaaa8a26a4cb7c8fda29d Mon Sep 17 00:00:00 2001 From: Marcio Diaz Date: Wed, 15 May 2019 12:22:43 +0200 Subject: [PATCH] Init store for slots-headers (#2492) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * init store for slots * fix: add check_equivocation to Aura/Babe * fix tests * fix: add pruning bound Co-Authored-By: André Silva * use saturating_sub --- substrate/core/consensus/aura/Cargo.toml | 6 +- substrate/core/consensus/aura/src/lib.rs | 106 ++++++++++-- substrate/core/consensus/babe/src/lib.rs | 111 ++++++++++++- .../core/consensus/slots/src/aux_schema.rs | 151 ++++++++++++++++++ substrate/core/consensus/slots/src/lib.rs | 2 + 5 files changed, 356 insertions(+), 20 deletions(-) create mode 100644 substrate/core/consensus/slots/src/aux_schema.rs diff --git a/substrate/core/consensus/aura/Cargo.toml b/substrate/core/consensus/aura/Cargo.toml index a0e43b8bbc..8a589aa126 100644 --- a/substrate/core/consensus/aura/Cargo.toml +++ b/substrate/core/consensus/aura/Cargo.toml @@ -18,14 +18,14 @@ srml-consensus = { path = "../../../srml/consensus" } srml-aura = { path = "../../../srml/aura" } client = { package = "substrate-client", path = "../../client" } substrate-telemetry = { path = "../../telemetry" } +consensus_common = { package = "substrate-consensus-common", path = "../common" } +authorities = { package = "substrate-consensus-authorities", path = "../authorities" } +runtime_primitives = { package = "sr-primitives", path = "../../sr-primitives" } futures = "0.1.17" tokio = "0.1.7" parking_lot = "0.7.1" error-chain = "0.12" log = "0.4" -consensus_common = { package = "substrate-consensus-common", path = "../common" } -authorities = { package = "substrate-consensus-authorities", path = "../authorities" } -runtime_primitives = { package = "sr-primitives", path = "../../sr-primitives" } [dev-dependencies] keyring = { package = "substrate-keyring", path = "../../keyring" } diff --git a/substrate/core/consensus/aura/src/lib.rs b/substrate/core/consensus/aura/src/lib.rs index b5122372ba..2c545cec33 100644 --- a/substrate/core/consensus/aura/src/lib.rs +++ b/substrate/core/consensus/aura/src/lib.rs @@ -63,7 +63,7 @@ use srml_aura::{ }; use substrate_telemetry::{telemetry, CONSENSUS_TRACE, CONSENSUS_DEBUG, CONSENSUS_WARN, CONSENSUS_INFO}; -use slots::{CheckedHeader, SlotWorker, SlotInfo, SlotCompatible, slot_now}; +use slots::{CheckedHeader, SlotWorker, SlotInfo, SlotCompatible, slot_now, check_equivocation}; pub use aura_primitives::*; pub use consensus_common::{SyncOracle, ExtraVerification}; @@ -196,7 +196,7 @@ pub fn start_aura_thread( force_authoring: bool, ) -> Result<(), consensus_common::Error> where B: Block + 'static, - C: ProvideRuntimeApi + ProvideCache + Send + Sync + 'static, + C: ProvideRuntimeApi + ProvideCache + AuxStore + Send + Sync + 'static, C::Api: AuthoritiesApi, SC: SelectChain + Clone + 'static, E: Environment + Send + Sync + 'static, @@ -247,7 +247,7 @@ pub fn start_aura( force_authoring: bool, ) -> Result, consensus_common::Error> where B: Block, - C: ProvideRuntimeApi + ProvideCache, + C: ProvideRuntimeApi + ProvideCache + AuxStore, C::Api: AuthoritiesApi, SC: SelectChain + Clone, E: Environment, @@ -292,7 +292,7 @@ struct AuraWorker { } impl SlotWorker for AuraWorker where - C: ProvideRuntimeApi + ProvideCache, + C: ProvideRuntimeApi + ProvideCache + AuxStore, C::Api: AuthoritiesApi, E: Environment, E::Proposer: Proposer, @@ -459,7 +459,8 @@ impl SlotWorker for AuraWorker( +fn check_header( + client: &Arc, slot_now: u64, mut header: B::Header, hash: B::Hash, @@ -467,8 +468,9 @@ fn check_header( allow_old_seals: bool, ) -> Result>, String> where DigestItemFor: CompatibleDigestItem

, - P::Public: AsRef, P::Signature: Decode, + C: client::backend::AuxStore, + P::Public: AsRef + Encode + Decode + PartialEq + Clone, { let digest_item = match header.digest_mut().pop() { Some(x) => x, @@ -501,7 +503,26 @@ fn check_header( let public = expected_author; if P::verify(&sig, &to_sign[..], public) { - Ok(CheckedHeader::Checked(header, digest_item)) + match check_equivocation::<_, _,

::Public>( + client, + slot_now, + slot_num, + header.clone(), + public.clone(), + ) { + Ok(Some(equivocation_proof)) => { + let log_str = format!( + "Slot author is equivocating at slot {} with headers {:?} and {:?}", + slot_num, + equivocation_proof.fst_header().hash(), + equivocation_proof.snd_header().hash(), + ); + info!("{}", log_str); + Err(log_str) + }, + Ok(None) => Ok(CheckedHeader::Checked(header, digest_item)), + Err(e) => Err(e.to_string()), + } } else { Err(format!("Bad signature on {:?}", hash)) } @@ -583,7 +604,7 @@ impl ExtraVerification for NothingExtra { #[forbid(deprecated)] impl Verifier for AuraVerifier where - C: ProvideRuntimeApi + Send + Sync, + C: ProvideRuntimeApi + Send + Sync + client::backend::AuxStore, C::Api: BlockBuilderApi, DigestItemFor: CompatibleDigestItem

+ DigestItem>, E: ExtraVerification, @@ -614,7 +635,8 @@ impl Verifier for AuraVerifier where // we add one to allow for some small drift. // FIXME #1019 in the future, alter this queue to allow deferring of headers - let checked_header = check_header::( + let checked_header = check_header::( + &self.client, slot_now + 1, header, hash, @@ -776,7 +798,7 @@ pub fn import_queue( inherent_data_providers: InherentDataProviders, ) -> Result, consensus_common::Error> where B: Block, - C: 'static + ProvideRuntimeApi + ProvideCache + Send + Sync, + C: 'static + ProvideRuntimeApi + ProvideCache + Send + Sync + AuxStore, C::Api: BlockBuilderApi + AuthoritiesApi, DigestItemFor: CompatibleDigestItem

+ DigestItem>, E: 'static + ExtraVerification, @@ -821,7 +843,7 @@ pub fn import_queue_accept_old_seals( inherent_data_providers: InherentDataProviders, ) -> Result, consensus_common::Error> where B: Block, - C: 'static + ProvideRuntimeApi + ProvideCache + Send + Sync, + C: 'static + ProvideRuntimeApi + ProvideCache + Send + Sync + AuxStore, C::Api: BlockBuilderApi + AuthoritiesApi, DigestItemFor: CompatibleDigestItem

+ DigestItem>, E: 'static + ExtraVerification, @@ -864,6 +886,9 @@ mod tests { use primitives::sr25519; use client::{LongestChain, BlockchainEvents}; use test_client; + use primitives::hash::H256; + use runtime_primitives::testing::{Header as HeaderTest, Digest as DigestTest, Block as RawBlock, ExtrinsicWrapper}; + use slots::{MAX_SLOT_CAPACITY, PRUNING_BOUND}; type Error = client::error::Error; @@ -960,6 +985,26 @@ mod tests { } } + fn create_header(slot_num: u64, number: u64, pair: &sr25519::Pair) -> (HeaderTest, H256) { + let mut header = HeaderTest { + parent_hash: Default::default(), + number, + state_root: Default::default(), + extrinsics_root: Default::default(), + digest: DigestTest { logs: vec![], }, + }; + let header_hash: H256 = header.hash(); + let to_sign = (slot_num, header_hash).encode(); + let signature = pair.sign(&to_sign[..]); + + let item = as CompatibleDigestItem>::aura_seal( + slot_num, + signature, + ); + header.digest_mut().push(item); + (header, header_hash) + } + #[test] fn authoring_blocks() { let _ = ::env_logger::try_init(); @@ -1042,4 +1087,43 @@ mod tests { Keyring::Charlie.into() ]); } + + #[test] + fn check_header_works_with_equivocation() { + let client = test_client::new(); + let pair = sr25519::Pair::generate(); + let public = pair.public(); + let authorities = vec![public.clone(), sr25519::Pair::generate().public()]; + + let (header1, header1_hash) = create_header(2, 1, &pair); + let (header2, header2_hash) = create_header(2, 2, &pair); + let (header3, header3_hash) = create_header(4, 2, &pair); + let (header4, header4_hash) = create_header(MAX_SLOT_CAPACITY + 4, 3, &pair); + let (header5, header5_hash) = create_header(MAX_SLOT_CAPACITY + 4, 4, &pair); + let (header6, header6_hash) = create_header(4, 3, &pair); + + type B = RawBlock>; + type P = sr25519::Pair; + + let c = Arc::new(client); + + // It's ok to sign same headers. + assert!(check_header::<_, B, P>(&c, 2, header1.clone(), header1_hash, &authorities, false).is_ok()); + assert!(check_header::<_, B, P>(&c, 3, header1, header1_hash, &authorities, false).is_ok()); + + // But not two different headers at the same slot. + assert!(check_header::<_, B, P>(&c, 4, header2, header2_hash, &authorities, false).is_err()); + + // Different slot is ok. + assert!(check_header::<_, B, P>(&c, 5, header3, header3_hash, &authorities, false).is_ok()); + + // Here we trigger pruning and save header 4. + assert!(check_header::<_, B, P>(&c, PRUNING_BOUND + 2, header4, header4_hash, &authorities, false).is_ok()); + + // This fails because header 5 is an equivocation of header 4. + assert!(check_header::<_, B, P>(&c, PRUNING_BOUND + 3, header5, header5_hash, &authorities, false).is_err()); + + // This is ok because we pruned the corresponding header. Shows that we are pruning. + assert!(check_header::<_, B, P>(&c, PRUNING_BOUND + 4, header6, header6_hash, &authorities, false).is_ok()); + } } diff --git a/substrate/core/consensus/babe/src/lib.rs b/substrate/core/consensus/babe/src/lib.rs index fa40fe64ba..9c6ef69777 100644 --- a/substrate/core/consensus/babe/src/lib.rs +++ b/substrate/core/consensus/babe/src/lib.rs @@ -67,7 +67,7 @@ use client::{ error::Result as CResult, backend::AuxStore, }; -use slots::CheckedHeader; +use slots::{CheckedHeader, check_equivocation}; use futures::{Future, IntoFuture, future}; use tokio::timer::Timeout; use log::{error, warn, debug, info, trace}; @@ -534,7 +534,8 @@ impl SlotWorker for BabeWorker whe // // FIXME #1018 needs misbehavior types #[forbid(warnings)] -fn check_header( +fn check_header( + client: &Arc, slot_now: u64, mut header: B::Header, hash: B::Hash, @@ -551,7 +552,7 @@ fn check_header( let BabeSeal { slot_num, - signature: LocalizedSignature {signer, signature }, + signature: LocalizedSignature { signer, signature }, proof, vrf_output, } = digest_item.as_babe_seal().ok_or_else(|| { @@ -584,8 +585,27 @@ fn check_header( format!("VRF verification failed") })? }; + if check(&inout, threshold) { - Ok(CheckedHeader::Checked(header, digest_item)) + match check_equivocation(&client, slot_now, slot_num, header.clone(), signer.clone()) { + Ok(Some(equivocation_proof)) => { + let log_str = format!( + "Slot author {:?} is equivocating at slot {} with headers {:?} and {:?}", + signer, + slot_num, + equivocation_proof.fst_header().hash(), + equivocation_proof.snd_header().hash(), + ); + info!("{}", log_str); + Err(log_str) + }, + Ok(None) => { + Ok(CheckedHeader::Checked(header, digest_item)) + }, + Err(e) => { + Err(e.to_string()) + }, + } } else { debug!(target: "babe", "VRF verification failed: threshold {} exceeded", threshold); Err(format!("Validator {:?} made seal when it wasn’t its turn", signer)) @@ -643,7 +663,7 @@ impl ExtraVerification for NothingExtra { } impl Verifier for BabeVerifier where - C: ProvideRuntimeApi + Send + Sync, + C: ProvideRuntimeApi + Send + Sync + AuxStore, C::Api: BlockBuilderApi, DigestItemFor: CompatibleDigestItem + DigestItem, E: ExtraVerification, @@ -683,7 +703,8 @@ impl Verifier for BabeVerifier where // we add one to allow for some small drift. // FIXME #1019 in the future, alter this queue to allow deferring of // headers - let checked_header = check_header::( + let checked_header = check_header::( + &self.client, slot_now + 1, header, hash, @@ -871,6 +892,10 @@ mod tests { use futures::stream::Stream; use log::debug; use std::time::Duration; + use test_client::AuthorityKeyring; + use primitives::hash::H256; + use runtime_primitives::testing::{Header as HeaderTest, Digest as DigestTest, Block as RawBlock, ExtrinsicWrapper}; + use slots::{MAX_SLOT_CAPACITY, PRUNING_BOUND}; type Error = client::error::Error; @@ -975,6 +1000,40 @@ mod tests { } } + fn create_header(slot_num: u64, number: u64, pair: &sr25519::Pair) -> (HeaderTest, H256) { + let mut header = HeaderTest { + parent_hash: Default::default(), + number, + state_root: Default::default(), + extrinsics_root: Default::default(), + digest: DigestTest { logs: vec![], }, + }; + + let transcript = make_transcript( + Default::default(), + slot_num, + Default::default(), + 0, + ); + + let (inout, proof, _batchable_proof) = get_keypair(&pair).vrf_sign_n_check(transcript, |inout| check(inout, u64::MAX)).unwrap(); + let pre_hash: H256 = header.hash(); + let to_sign = (slot_num, pre_hash, proof.to_bytes()).encode(); + let signature = pair.sign(&to_sign[..]); + let item = as CompatibleDigestItem>::babe_seal(BabeSeal { + proof, + signature: LocalizedSignature { + signature, + signer: pair.public(), + }, + slot_num, + vrf_output: inout.to_output(), + }); + + header.digest_mut().push(item); + (header, pre_hash) + } + #[test] fn can_serialize_block() { drop(env_logger::try_init()); @@ -1104,4 +1163,44 @@ mod tests { Keyring::Charlie.into() ]); } + + #[test] + fn check_header_works_with_equivocation() { + let client = test_client::new(); + let pair = sr25519::Pair::generate(); + let public = pair.public(); + let authorities = vec![public.clone(), sr25519::Pair::generate().public()]; + + let (header1, header1_hash) = create_header(2, 1, &pair); + let (header2, header2_hash) = create_header(2, 2, &pair); + let (header3, header3_hash) = create_header(4, 2, &pair); + let (header4, header4_hash) = create_header(MAX_SLOT_CAPACITY + 4, 3, &pair); + let (header5, header5_hash) = create_header(MAX_SLOT_CAPACITY + 4, 4, &pair); + let (header6, header6_hash) = create_header(4, 3, &pair); + + let c = Arc::new(client); + let max = u64::MAX; + + type B = RawBlock>; + type P = sr25519::Pair; + + // It's ok to sign same headers. + assert!(check_header::(&c, 2, header1.clone(), header1_hash, &authorities, max).is_ok()); + assert!(check_header::(&c, 3, header1, header1_hash, &authorities, max).is_ok()); + + // But not two different headers at the same slot. + assert!(check_header::(&c, 4, header2, header2_hash, &authorities, max).is_err()); + + // Different slot is ok. + assert!(check_header::(&c, 5, header3, header3_hash, &authorities, max).is_ok()); + + // Here we trigger pruning and save header 4. + assert!(check_header::(&c, PRUNING_BOUND + 2, header4, header4_hash, &authorities, max).is_ok()); + + // This fails because header 5 is an equivocation of header 4. + assert!(check_header::(&c, PRUNING_BOUND + 3, header5, header5_hash, &authorities, max).is_err()); + + // This is ok because we pruned the corresponding header. Shows that we are pruning. + assert!(check_header::(&c, PRUNING_BOUND + 4, header6, header6_hash, &authorities, max).is_ok()); + } } diff --git a/substrate/core/consensus/slots/src/aux_schema.rs b/substrate/core/consensus/slots/src/aux_schema.rs new file mode 100644 index 0000000000..44f4ca5983 --- /dev/null +++ b/substrate/core/consensus/slots/src/aux_schema.rs @@ -0,0 +1,151 @@ +// Copyright 2019 Parity Technologies (UK) Ltd. +// This file is part of Substrate. + +// Substrate 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. + +// Substrate 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 Substrate. If not, see . + +//! Schema for slots in the aux-db. + +use std::sync::Arc; +use codec::{Encode, Decode}; +use client::backend::AuxStore; +use client::error::{Result as ClientResult, Error as ClientError}; +use runtime_primitives::traits::Header; + +const SLOT_HEADER_MAP_KEY: &[u8] = b"slot_header_map"; +const SLOT_HEADER_START: &[u8] = b"slot_header_start"; + +/// We keep at least this number of slots in database. +pub const MAX_SLOT_CAPACITY: u64 = 1000; +/// We prune slots when they reach this number. +pub const PRUNING_BOUND: u64 = 2 * MAX_SLOT_CAPACITY; + +fn load_decode(backend: Arc, key: &[u8]) -> ClientResult> + where + C: AuxStore, + T: Decode, +{ + match backend.get_aux(key)? { + None => Ok(None), + Some(t) => T::decode(&mut &t[..]) + .ok_or_else( + || ClientError::Backend(format!("Slots DB is corrupted.")).into(), + ) + .map(Some) + } +} + +/// Represents an equivocation proof. +#[derive(Debug, Clone)] +pub struct EquivocationProof { + slot: u64, + fst_header: H, + snd_header: H, +} + +impl EquivocationProof { + /// Get the slot number where the equivocation happened. + pub fn slot(&self) -> u64 { + self.slot + } + + /// Get the first header involved in the equivocation. + pub fn fst_header(&self) -> &H { + &self.fst_header + } + + /// Get the second header involved in the equivocation. + pub fn snd_header(&self) -> &H { + &self.snd_header + } +} + +/// Checks if the header is an equivocation and returns the proof in that case. +/// +/// Note: it detects equivocations only when slot_now - slot <= MAX_SLOT_CAPACITY. +pub fn check_equivocation( + backend: &Arc, + slot_now: u64, + slot: u64, + header: H, + signer: P, +) -> ClientResult>> + where + H: Header, + C: AuxStore, + P: Encode + Decode + PartialEq, +{ + // We don't check equivocations for old headers out of our capacity. + if slot_now - slot > MAX_SLOT_CAPACITY { + return Ok(None) + } + + // Key for this slot. + let mut curr_slot_key = SLOT_HEADER_MAP_KEY.to_vec(); + slot.using_encoded(|s| curr_slot_key.extend(s)); + + // Get headers of this slot. + let mut headers_with_sig = load_decode::<_, Vec<(H, P)>>(backend.clone(), &curr_slot_key[..])? + .unwrap_or_else(Vec::new); + + // Get first slot saved. + let slot_header_start = SLOT_HEADER_START.to_vec(); + let first_saved_slot = load_decode::<_, u64>(backend.clone(), &slot_header_start[..])? + .unwrap_or(slot); + + for (prev_header, prev_signer) in headers_with_sig.iter() { + // A proof of equivocation consists of two headers: + // 1) signed by the same voter, + if *prev_signer == signer { + // 2) with different hash + if header.hash() != prev_header.hash() { + return Ok(Some(EquivocationProof { + slot, // 3) and mentioning the same slot. + fst_header: prev_header.clone(), + snd_header: header.clone(), + })); + } else { + // We don't need to continue in case of duplicated header, + // since it's already saved and a possible equivocation + // would have been detected before. + return Ok(None) + } + } + } + + let mut keys_to_delete = vec![]; + let mut new_first_saved_slot = first_saved_slot; + + if slot_now - first_saved_slot >= PRUNING_BOUND { + let prefix = SLOT_HEADER_MAP_KEY.to_vec(); + new_first_saved_slot = slot_now.saturating_sub(MAX_SLOT_CAPACITY); + + for s in first_saved_slot..new_first_saved_slot { + let mut p = prefix.clone(); + s.using_encoded(|s| p.extend(s)); + keys_to_delete.push(p); + } + } + + headers_with_sig.push((header, signer)); + + backend.insert_aux( + &[ + (&curr_slot_key[..], headers_with_sig.encode().as_slice()), + (&slot_header_start[..], new_first_saved_slot.encode().as_slice()), + ], + &keys_to_delete.iter().map(|k| &k[..]).collect::>()[..], + )?; + + Ok(None) +} diff --git a/substrate/core/consensus/slots/src/lib.rs b/substrate/core/consensus/slots/src/lib.rs index 2f8eedb497..4ae72fd92a 100644 --- a/substrate/core/consensus/slots/src/lib.rs +++ b/substrate/core/consensus/slots/src/lib.rs @@ -23,8 +23,10 @@ #![forbid(warnings, unsafe_code, missing_docs)] mod slots; +mod aux_schema; pub use slots::{slot_now, SlotInfo, Slots}; +pub use aux_schema::{check_equivocation, MAX_SLOT_CAPACITY, PRUNING_BOUND}; use codec::{Decode, Encode}; use consensus_common::{SyncOracle, SelectChain};