diff --git a/Cargo.lock b/Cargo.lock index 56fa41cb08eae..3d5d7ef092d4e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1352,9 +1352,9 @@ dependencies = [ [[package]] name = "finality-grandpa" -version = "0.12.1" +version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ab32971efbe776e46bfbc34d5b662d8e1de51fd14e26a2eba522c0f3470fc0f" +checksum = "1f4682570188cd105606e621b9992e580f717c15f8cd1b7d106b59f1c6e54680" dependencies = [ "either", "futures 0.3.4", @@ -3440,6 +3440,7 @@ dependencies = [ "pallet-authority-discovery", "pallet-balances", "pallet-contracts", + "pallet-grandpa", "pallet-im-online", "pallet-indices", "pallet-staking", @@ -3546,7 +3547,10 @@ dependencies = [ name = "node-primitives" version = "2.0.0-dev" dependencies = [ + "frame-system", + "parity-scale-codec", "pretty_assertions", + "sp-application-crypto", "sp-core", "sp-runtime", "sp-serializer", @@ -4275,16 +4279,25 @@ dependencies = [ name = "pallet-grandpa" version = "2.0.0-dev" dependencies = [ + "finality-grandpa", "frame-support", "frame-system", + "pallet-balances", "pallet-finality-tracker", + "pallet-offences", "pallet-session", + "pallet-staking", + "pallet-staking-reward-curve", + "pallet-timestamp", "parity-scale-codec", "serde", + "sp-application-crypto", "sp-core", "sp-finality-grandpa", "sp-io", + "sp-keyring", "sp-runtime", + "sp-session", "sp-staking", "sp-std", ] @@ -4486,6 +4499,7 @@ dependencies = [ "sp-core", "sp-io", "sp-runtime", + "sp-session", "sp-staking", "sp-std", "sp-trie", @@ -7524,10 +7538,13 @@ dependencies = [ name = "sp-finality-grandpa" version = "2.0.0-dev" dependencies = [ + "finality-grandpa", + "log", "parity-scale-codec", "serde", "sp-api", "sp-application-crypto", + "sp-core", "sp-runtime", "sp-std", ] @@ -7760,9 +7777,11 @@ dependencies = [ name = "sp-session" version = "2.0.0-dev" dependencies = [ + "parity-scale-codec", "sp-api", "sp-core", "sp-runtime", + "sp-staking", "sp-std", ] @@ -8027,6 +8046,7 @@ dependencies = [ "node-primitives", "node-runtime", "pallet-balances", + "pallet-grandpa", "pallet-transaction-payment", "parity-scale-codec", "rand 0.7.3", @@ -8181,6 +8201,7 @@ dependencies = [ "sp-consensus-aura", "sp-consensus-babe", "sp-core", + "sp-finality-grandpa", "sp-inherents", "sp-io", "sp-keyring", diff --git a/bin/node-template/runtime/src/lib.rs b/bin/node-template/runtime/src/lib.rs index 44332f61a1583..8261577586e47 100644 --- a/bin/node-template/runtime/src/lib.rs +++ b/bin/node-template/runtime/src/lib.rs @@ -9,17 +9,17 @@ include!(concat!(env!("OUT_DIR"), "/wasm_binary.rs")); use sp_std::prelude::*; -use sp_core::OpaqueMetadata; +use sp_core::{crypto::KeyTypeId, OpaqueMetadata}; use sp_runtime::{ ApplyExtrinsicResult, generic, create_runtime_str, impl_opaque_keys, MultiSignature, transaction_validity::{TransactionValidity, TransactionSource}, }; use sp_runtime::traits::{ - BlakeTwo256, Block as BlockT, IdentityLookup, Verify, ConvertInto, IdentifyAccount + BlakeTwo256, Block as BlockT, IdentityLookup, Verify, ConvertInto, IdentifyAccount, NumberFor, }; use sp_api::impl_runtime_apis; use sp_consensus_aura::sr25519::AuthorityId as AuraId; -use grandpa::AuthorityList as GrandpaAuthorityList; +use grandpa::{AuthorityId as GrandpaId, AuthorityList as GrandpaAuthorityList}; use grandpa::fg_primitives; use sp_version::RuntimeVersion; #[cfg(feature = "std")] @@ -32,8 +32,8 @@ pub use timestamp::Call as TimestampCall; pub use balances::Call as BalancesCall; pub use sp_runtime::{Permill, Perbill}; pub use frame_support::{ - StorageValue, construct_runtime, parameter_types, - traits::Randomness, + construct_runtime, parameter_types, StorageValue, + traits::{KeyOwnerProofSystem, Randomness}, weights::{ Weight, constants::{BlockExecutionWeight, ExtrinsicBaseWeight, RocksDbWeight, WEIGHT_PER_SECOND}, @@ -188,6 +188,19 @@ impl aura::Trait for Runtime { impl grandpa::Trait for Runtime { type Event = Event; + type Call = Call; + + type KeyOwnerProofSystem = (); + + type KeyOwnerProof = + >::Proof; + + type KeyOwnerIdentification = >::IdentificationTuple; + + type HandleEquivocation = (); } parameter_types! { @@ -360,7 +373,7 @@ impl_runtime_apis! { fn decode_session_keys( encoded: Vec, - ) -> Option, sp_core::crypto::KeyTypeId)>> { + ) -> Option, KeyTypeId)>> { opaque::SessionKeys::decode_into_raw_public_keys(&encoded) } } @@ -369,5 +382,25 @@ impl_runtime_apis! { fn grandpa_authorities() -> GrandpaAuthorityList { Grandpa::grandpa_authorities() } + + fn submit_report_equivocation_extrinsic( + _equivocation_proof: fg_primitives::EquivocationProof< + ::Hash, + NumberFor, + >, + _key_owner_proof: fg_primitives::OpaqueKeyOwnershipProof, + ) -> Option<()> { + None + } + + fn generate_key_ownership_proof( + _set_id: fg_primitives::SetId, + _authority_id: GrandpaId, + ) -> Option { + // NOTE: this is the only implementation possible since we've + // defined our key owner proof type as a bottom type (i.e. a type + // with no values). + None + } } } diff --git a/bin/node/cli/Cargo.toml b/bin/node/cli/Cargo.toml index 5b23a989cd0bf..de38ac66e5489 100644 --- a/bin/node/cli/Cargo.toml +++ b/bin/node/cli/Cargo.toml @@ -85,8 +85,9 @@ pallet-balances = { version = "2.0.0-dev", path = "../../../frame/balances" } pallet-transaction-payment = { version = "2.0.0-dev", path = "../../../frame/transaction-payment" } frame-support = { version = "2.0.0-dev", default-features = false, path = "../../../frame/support" } pallet-im-online = { version = "2.0.0-dev", default-features = false, path = "../../../frame/im-online" } -pallet-authority-discovery = { version = "2.0.0-dev", path = "../../../frame/authority-discovery" } -pallet-staking = { version = "2.0.0-dev", path = "../../../frame/staking" } +pallet-authority-discovery = { version = "2.0.0-dev", path = "../../../frame/authority-discovery" } +pallet-staking = { version = "2.0.0-dev", path = "../../../frame/staking" } +pallet-grandpa = { version = "2.0.0-dev", path = "../../../frame/grandpa" } # node-specific dependencies node-runtime = { version = "2.0.0-dev", path = "../runtime" } diff --git a/bin/node/cli/src/service.rs b/bin/node/cli/src/service.rs index e7c9a240ae61a..7e27d57063ecf 100644 --- a/bin/node/cli/src/service.rs +++ b/bin/node/cli/src/service.rs @@ -613,6 +613,7 @@ mod tests { let check_nonce = frame_system::CheckNonce::from(index); let check_weight = frame_system::CheckWeight::new(); let payment = pallet_transaction_payment::ChargeTransactionPayment::from(0); + let validate_grandpa_equivocation = pallet_grandpa::ValidateEquivocationReport::new(); let extra = ( check_version, check_genesis, @@ -620,11 +621,12 @@ mod tests { check_nonce, check_weight, payment, + validate_grandpa_equivocation, ); let raw_payload = SignedPayload::from_raw( function, extra, - (version, genesis_hash, genesis_hash, (), (), ()) + (version, genesis_hash, genesis_hash, (), (), (), ()) ); let signature = raw_payload.using_encoded(|payload| { signer.sign(payload) diff --git a/bin/node/primitives/Cargo.toml b/bin/node/primitives/Cargo.toml index 11959bde75c64..e69b626b5420f 100644 --- a/bin/node/primitives/Cargo.toml +++ b/bin/node/primitives/Cargo.toml @@ -11,6 +11,9 @@ repository = "https://github.com/paritytech/substrate/" targets = ["x86_64-unknown-linux-gnu"] [dependencies] +codec = { package = "parity-scale-codec", version = "1.3.0", default-features = false, features = ["derive"] } +frame-system = { version = "2.0.0-dev", default-features = false, path = "../../../frame/system" } +sp-application-crypto = { version = "2.0.0-dev", default-features = false, path = "../../../primitives/application-crypto" } sp-core = { version = "2.0.0-dev", default-features = false, path = "../../../primitives/core" } sp-runtime = { version = "2.0.0-dev", default-features = false, path = "../../../primitives/runtime" } @@ -21,6 +24,9 @@ pretty_assertions = "0.6.1" [features] default = ["std"] std = [ + "codec/std", + "frame-system/std", + "sp-application-crypto/std", "sp-core/std", "sp-runtime/std", ] diff --git a/bin/node/primitives/src/lib.rs b/bin/node/primitives/src/lib.rs index 97e8f50c27145..c6180b31f7e05 100644 --- a/bin/node/primitives/src/lib.rs +++ b/bin/node/primitives/src/lib.rs @@ -62,3 +62,34 @@ pub type Header = generic::Header; pub type Block = generic::Block; /// Block ID. pub type BlockId = generic::BlockId; + +/// App-specific crypto used for reporting equivocation/misbehavior in BABE and +/// GRANDPA. Any rewards for misbehavior reporting will be paid out to this +/// account. +pub mod report { + use super::{Signature, Verify}; + use frame_system::offchain::AppCrypto; + use sp_core::crypto::KeyTypeId; + + /// Key type for the reporting module. Used for reporting BABE and GRANDPA + /// equivocations. + pub const KEY_TYPE: KeyTypeId = KeyTypeId(*b"fish"); + + mod app { + use sp_application_crypto::{app_crypto, sr25519}; + app_crypto!(sr25519, super::KEY_TYPE); + } + + /// Identity of the equivocation/misbehavior reporter. + pub type ReporterId = app::Public; + + /// An `AppCrypto` type to allow submitting signed transactions using the reporting + /// application key as signer. + pub struct ReporterAppCrypto; + + impl AppCrypto<::Signer, Signature> for ReporterAppCrypto { + type RuntimeAppPublic = ReporterId; + type GenericSignature = sp_core::sr25519::Signature; + type GenericPublic = sp_core::sr25519::Public; + } +} diff --git a/bin/node/runtime/src/lib.rs b/bin/node/runtime/src/lib.rs index e3613ea75abfc..294aac8ae3957 100644 --- a/bin/node/runtime/src/lib.rs +++ b/bin/node/runtime/src/lib.rs @@ -27,9 +27,13 @@ use frame_support::{ Weight, constants::{BlockExecutionWeight, ExtrinsicBaseWeight, RocksDbWeight, WEIGHT_PER_SECOND}, }, - traits::{Currency, Randomness, OnUnbalanced, Imbalance, LockIdentifier}, + traits::{Currency, Imbalance, KeyOwnerProofSystem, OnUnbalanced, Randomness, LockIdentifier}, +}; +use sp_core::{ + crypto::KeyTypeId, + u32_trait::{_1, _2, _3, _4}, + OpaqueMetadata, }; -use sp_core::u32_trait::{_1, _2, _3, _4}; pub use node_primitives::{AccountId, Signature}; use node_primitives::{AccountIndex, Balance, BlockNumber, Hash, Index, Moment}; use sp_api::impl_runtime_apis; @@ -41,18 +45,18 @@ use sp_runtime::curve::PiecewiseLinear; use sp_runtime::transaction_validity::{TransactionValidity, TransactionSource, TransactionPriority}; use sp_runtime::traits::{ self, BlakeTwo256, Block as BlockT, StaticLookup, SaturatedConversion, - ConvertInto, OpaqueKeys, + ConvertInto, OpaqueKeys, NumberFor, }; use sp_version::RuntimeVersion; #[cfg(any(feature = "std", test))] use sp_version::NativeVersion; -use sp_core::OpaqueMetadata; -use pallet_grandpa::AuthorityList as GrandpaAuthorityList; +use pallet_grandpa::{AuthorityId as GrandpaId, AuthorityList as GrandpaAuthorityList}; use pallet_grandpa::fg_primitives; use pallet_im_online::sr25519::AuthorityId as ImOnlineId; use sp_authority_discovery::AuthorityId as AuthorityDiscoveryId; use pallet_transaction_payment_rpc_runtime_api::RuntimeDispatchInfo; use pallet_contracts_rpc_runtime_api::ContractExecResult; +use pallet_session::{historical as pallet_session_historical}; use sp_inherents::{InherentData, CheckInherentsResult}; #[cfg(any(feature = "std", test))] @@ -86,7 +90,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { // and set impl_version to 0. If only runtime // implementation changes and behavior does not, then leave spec_version as // is and increment impl_version. - spec_version: 246, + spec_version: 247, impl_version: 0, apis: RUNTIME_API_VERSIONS, transaction_version: 1, @@ -271,7 +275,7 @@ impl pallet_session::Trait for Runtime { type ValidatorId = ::AccountId; type ValidatorIdOf = pallet_staking::StashOf; type ShouldEndSession = Babe; - type SessionManager = Staking; + type SessionManager = pallet_session::historical::NoteHistoricalRoot; type SessionHandler = ::KeyTypeIdProviders; type Keys = SessionKeys; type DisabledValidatorsThreshold = DisabledValidatorsThreshold; @@ -529,6 +533,7 @@ impl frame_system::offchain::CreateSignedTransaction for R frame_system::CheckNonce::::from(nonce), frame_system::CheckWeight::::new(), pallet_transaction_payment::ChargeTransactionPayment::::from(tip), + pallet_grandpa::ValidateEquivocationReport::::new(), ); let raw_payload = SignedPayload::new(call, extra).map_err(|e| { debug::warn!("Unable to create signed payload: {:?}", e); @@ -572,6 +577,24 @@ impl pallet_authority_discovery::Trait for Runtime {} impl pallet_grandpa::Trait for Runtime { type Event = Event; + type Call = Call; + + type KeyOwnerProofSystem = Historical; + + type KeyOwnerProof = + >::Proof; + + type KeyOwnerIdentification = >::IdentificationTuple; + + type HandleEquivocation = pallet_grandpa::EquivocationHandler< + Self::KeyOwnerIdentification, + node_primitives::report::ReporterAppCrypto, + Runtime, + Offences, + >; } parameter_types! { @@ -693,6 +716,7 @@ construct_runtime!( ImOnline: pallet_im_online::{Module, Call, Storage, Event, ValidateUnsigned, Config}, AuthorityDiscovery: pallet_authority_discovery::{Module, Call, Config}, Offences: pallet_offences::{Module, Call, Storage, Event}, + Historical: pallet_session_historical::{Module}, RandomnessCollectiveFlip: pallet_randomness_collective_flip::{Module, Call, Storage}, Identity: pallet_identity::{Module, Call, Storage, Event}, Society: pallet_society::{Module, Call, Storage, Event, Config}, @@ -720,6 +744,7 @@ pub type SignedExtra = ( frame_system::CheckNonce, frame_system::CheckWeight, pallet_transaction_payment::ChargeTransactionPayment, + pallet_grandpa::ValidateEquivocationReport, ); /// Unchecked extrinsic type as expected by this runtime. pub type UncheckedExtrinsic = generic::UncheckedExtrinsic; @@ -792,6 +817,32 @@ impl_runtime_apis! { fn grandpa_authorities() -> GrandpaAuthorityList { Grandpa::grandpa_authorities() } + + fn submit_report_equivocation_extrinsic( + equivocation_proof: fg_primitives::EquivocationProof< + ::Hash, + NumberFor, + >, + key_owner_proof: fg_primitives::OpaqueKeyOwnershipProof, + ) -> Option<()> { + let key_owner_proof = key_owner_proof.decode()?; + + Grandpa::submit_report_equivocation_extrinsic( + equivocation_proof, + key_owner_proof, + ) + } + + fn generate_key_ownership_proof( + _set_id: fg_primitives::SetId, + authority_id: GrandpaId, + ) -> Option { + use codec::Encode; + + Historical::prove((fg_primitives::KEY_TYPE, authority_id)) + .map(|p| p.encode()) + .map(fg_primitives::OpaqueKeyOwnershipProof::new) + } } impl sp_consensus_babe::BabeApi for Runtime { @@ -880,7 +931,7 @@ impl_runtime_apis! { fn decode_session_keys( encoded: Vec, - ) -> Option, sp_core::crypto::KeyTypeId)>> { + ) -> Option, KeyTypeId)>> { SessionKeys::decode_into_raw_public_keys(&encoded) } } diff --git a/bin/node/testing/src/keyring.rs b/bin/node/testing/src/keyring.rs index 5eebc09f4b7af..7ed2b36502e85 100644 --- a/bin/node/testing/src/keyring.rs +++ b/bin/node/testing/src/keyring.rs @@ -74,6 +74,7 @@ pub fn signed_extra(nonce: Index, extra_fee: Balance) -> SignedExtra { frame_system::CheckNonce::from(nonce), frame_system::CheckWeight::new(), pallet_transaction_payment::ChargeTransactionPayment::from(extra_fee), + pallet_grandpa::ValidateEquivocationReport::new(), ) } diff --git a/bin/utils/subkey/Cargo.toml b/bin/utils/subkey/Cargo.toml index b0c642ec43e7c..7f50f4546ce5a 100644 --- a/bin/utils/subkey/Cargo.toml +++ b/bin/utils/subkey/Cargo.toml @@ -26,6 +26,7 @@ codec = { package = "parity-scale-codec", version = "1.3.0" } frame-system = { version = "2.0.0-dev", path = "../../../frame/system" } pallet-balances = { version = "2.0.0-dev", path = "../../../frame/balances" } pallet-transaction-payment = { version = "2.0.0-dev", path = "../../../frame/transaction-payment" } +pallet-grandpa = { version = "2.0.0-dev", path = "../../../frame/grandpa" } rpassword = "4.0.1" itertools = "0.8.2" derive_more = { version = "0.99.2" } diff --git a/bin/utils/subkey/src/main.rs b/bin/utils/subkey/src/main.rs index 22706ebd822da..754a2611bcd0f 100644 --- a/bin/utils/subkey/src/main.rs +++ b/bin/utils/subkey/src/main.rs @@ -708,6 +708,7 @@ fn create_extrinsic( frame_system::CheckNonce::::from(i), frame_system::CheckWeight::::new(), pallet_transaction_payment::ChargeTransactionPayment::::from(f), + pallet_grandpa::ValidateEquivocationReport::::new(), ) }; let raw_payload = SignedPayload::from_raw( @@ -720,6 +721,7 @@ fn create_extrinsic( (), (), (), + (), ), ); let signature = raw_payload.using_encoded(|payload| signer.sign(payload)).into_runtime(); diff --git a/client/finality-grandpa/Cargo.toml b/client/finality-grandpa/Cargo.toml index 97dafc3d46b8b..9b5d787f889a8 100644 --- a/client/finality-grandpa/Cargo.toml +++ b/client/finality-grandpa/Cargo.toml @@ -42,11 +42,11 @@ sp-finality-tracker = { version = "2.0.0-dev", path = "../../primitives/finality sp-finality-grandpa = { version = "2.0.0-dev", path = "../../primitives/finality-grandpa" } prometheus-endpoint = { package = "substrate-prometheus-endpoint", path = "../../utils/prometheus", version = "0.8.0-dev"} sc-block-builder = { version = "0.8.0-dev", path = "../block-builder" } -finality-grandpa = { version = "0.12.1", features = ["derive-codec"] } +finality-grandpa = { version = "0.12.2", features = ["derive-codec"] } pin-project = "0.4.6" [dev-dependencies] -finality-grandpa = { version = "0.12.1", features = ["derive-codec", "test-helpers"] } +finality-grandpa = { version = "0.12.2", features = ["derive-codec", "test-helpers"] } sc-network = { version = "0.8.0-dev", path = "../network" } sc-network-test = { version = "0.8.0-dev", path = "../network/test" } sp-keyring = { version = "2.0.0-dev", path = "../../primitives/keyring" } diff --git a/client/finality-grandpa/rpc/Cargo.toml b/client/finality-grandpa/rpc/Cargo.toml index 5f7ccc9218edf..175a4ccfe8edf 100644 --- a/client/finality-grandpa/rpc/Cargo.toml +++ b/client/finality-grandpa/rpc/Cargo.toml @@ -9,7 +9,7 @@ license = "GPL-3.0" [dependencies] sc-finality-grandpa = { version = "0.8.0-dev", path = "../" } -finality-grandpa = { version = "0.12.1", features = ["derive-codec"] } +finality-grandpa = { version = "0.12.2", features = ["derive-codec"] } jsonrpc-core = "14.0.3" jsonrpc-core-client = "14.0.3" jsonrpc-derive = "14.0.3" diff --git a/client/finality-grandpa/src/authorities.rs b/client/finality-grandpa/src/authorities.rs index cb7675743017e..80c1f4ad3fe38 100644 --- a/client/finality-grandpa/src/authorities.rs +++ b/client/finality-grandpa/src/authorities.rs @@ -100,15 +100,17 @@ pub(crate) struct Status { /// A set of authorities. #[derive(Debug, Clone, Encode, Decode, PartialEq)] pub(crate) struct AuthoritySet { + /// The current active authorities. pub(crate) current_authorities: AuthorityList, - set_id: u64, - // Tree of pending standard changes across forks. Standard changes are - // enacted on finality and must be enacted (i.e. finalized) in-order across - // a given branch + /// The current set id. + pub(crate) set_id: u64, + /// Tree of pending standard changes across forks. Standard changes are + /// enacted on finality and must be enacted (i.e. finalized) in-order across + /// a given branch pub(crate) pending_standard_changes: ForkTree>, - // Pending forced changes across different forks (at most one per fork). - // Forced changes are enacted on block depth (not finality), for this reason - // only one forced change should exist per fork. + /// Pending forced changes across different forks (at most one per fork). + /// Forced changes are enacted on block depth (not finality), for this reason + /// only one forced change should exist per fork. pending_forced_changes: Vec>, } @@ -162,9 +164,55 @@ where H: PartialEq, impl AuthoritySet where - N: Add + Ord + Clone + Debug, - H: Clone + Debug + N: Add + Ord + Clone + Debug, + H: Clone + Debug, { + /// Returns the block hash and height at which the next pending change in + /// the given chain (i.e. it includes `best_hash`) was signalled, `None` if + /// there are no pending changes for the given chain. + /// + /// This is useful since we know that when a change is signalled the + /// underlying runtime authority set management module (e.g. session module) + /// has updated its internal state (e.g. a new session started). + pub(crate) fn next_change( + &self, + best_hash: &H, + is_descendent_of: &F, + ) -> Result, fork_tree::Error> + where + F: Fn(&H, &H) -> Result, + E: std::error::Error, + { + let mut forced = None; + for change in &self.pending_forced_changes { + if is_descendent_of(&change.canon_hash, best_hash)? { + forced = Some((change.canon_hash.clone(), change.canon_height.clone())); + break; + } + } + + let mut standard = None; + for (_, _, change) in self.pending_standard_changes.roots() { + if is_descendent_of(&change.canon_hash, best_hash)? { + standard = Some((change.canon_hash.clone(), change.canon_height.clone())); + break; + } + } + + let earliest = match (forced, standard) { + (Some(forced), Some(standard)) => Some(if forced.1 < standard.1 { + forced + } else { + standard + }), + (Some(forced), None) => Some(forced), + (None, Some(standard)) => Some(standard), + (None, None) => None, + }; + + Ok(earliest) + } + fn add_standard_change( &mut self, pending: PendingChange, @@ -922,6 +970,128 @@ mod tests { ); } + #[test] + fn next_change_works() { + let current_authorities = vec![(AuthorityId::from_slice(&[1; 32]), 1)]; + + let mut authorities = AuthoritySet { + current_authorities: current_authorities.clone(), + set_id: 0, + pending_standard_changes: ForkTree::new(), + pending_forced_changes: Vec::new(), + }; + + let new_set = current_authorities.clone(); + + // We have three pending changes with 2 possible roots that are enacted + // immediately on finality (i.e. standard changes). + let change_a0 = PendingChange { + next_authorities: new_set.clone(), + delay: 0, + canon_height: 5, + canon_hash: "hash_a0", + delay_kind: DelayKind::Finalized, + }; + + let change_a1 = PendingChange { + next_authorities: new_set.clone(), + delay: 0, + canon_height: 10, + canon_hash: "hash_a1", + delay_kind: DelayKind::Finalized, + }; + + let change_b = PendingChange { + next_authorities: new_set.clone(), + delay: 0, + canon_height: 4, + canon_hash: "hash_b", + delay_kind: DelayKind::Finalized, + }; + + // A0 (#5) <- A10 (#8) <- A1 (#10) <- best_a + // B (#4) <- best_b + let is_descendent_of = is_descendent_of(|base, hash| match (*base, *hash) { + ("hash_a0", "hash_a1") => true, + ("hash_a0", "best_a") => true, + ("hash_a1", "best_a") => true, + ("hash_a10", "best_a") => true, + ("hash_b", "best_b") => true, + _ => false, + }); + + // add the three pending changes + authorities + .add_pending_change(change_b, &is_descendent_of) + .unwrap(); + authorities + .add_pending_change(change_a0, &is_descendent_of) + .unwrap(); + authorities + .add_pending_change(change_a1, &is_descendent_of) + .unwrap(); + + // the earliest change at block `best_a` should be the change at A0 (#5) + assert_eq!( + authorities + .next_change(&"best_a", &is_descendent_of) + .unwrap(), + Some(("hash_a0", 5)), + ); + + // the earliest change at block `best_b` should be the change at B (#4) + assert_eq!( + authorities + .next_change(&"best_b", &is_descendent_of) + .unwrap(), + Some(("hash_b", 4)), + ); + + // we apply the change at A0 which should prune it and the fork at B + authorities + .apply_standard_changes("hash_a0", 5, &is_descendent_of, false) + .unwrap(); + + // the next change is now at A1 (#10) + assert_eq!( + authorities + .next_change(&"best_a", &is_descendent_of) + .unwrap(), + Some(("hash_a1", 10)), + ); + + // there's no longer any pending change at `best_b` fork + assert_eq!( + authorities + .next_change(&"best_b", &is_descendent_of) + .unwrap(), + None, + ); + + // we a forced change at A10 (#8) + let change_a10 = PendingChange { + next_authorities: new_set.clone(), + delay: 0, + canon_height: 8, + canon_hash: "hash_a10", + delay_kind: DelayKind::Best { + median_last_finalized: 0, + }, + }; + + authorities + .add_pending_change(change_a10, &static_is_descendent_of(false)) + .unwrap(); + + // it should take precedence over the change at A1 (#10) + assert_eq!( + authorities + .next_change(&"best_a", &is_descendent_of) + .unwrap(), + Some(("hash_a10", 8)), + ); + } + #[test] fn maintains_authority_list_invariants() { // empty authority lists are invalid diff --git a/client/finality-grandpa/src/aux_schema.rs b/client/finality-grandpa/src/aux_schema.rs index c217bc328a9a1..e4e8a980420d8 100644 --- a/client/finality-grandpa/src/aux_schema.rs +++ b/client/finality-grandpa/src/aux_schema.rs @@ -154,7 +154,7 @@ fn migrate_from_version0( None => (0, genesis_round()), }; - let set_id = new_set.current().0; + let set_id = new_set.set_id; let base = last_round_state.prevote_ghost .expect("state is for completed round; completed rounds must have a prevote ghost; qed."); @@ -201,7 +201,7 @@ fn migrate_from_version1( backend, AUTHORITY_SET_KEY, )? { - let set_id = set.current().0; + let set_id = set.set_id; let completed_rounds = |number, state, base| CompletedRounds::new( CompletedRound { @@ -312,7 +312,7 @@ pub(crate) fn load_persistent( .expect("state is for completed round; completed rounds must have a prevote ghost; qed."); VoterSetState::live( - set.current().0, + set.set_id, &set, base, ) diff --git a/client/finality-grandpa/src/communication/gossip.rs b/client/finality-grandpa/src/communication/gossip.rs index afa6581702514..183ffc65e8378 100644 --- a/client/finality-grandpa/src/communication/gossip.rs +++ b/client/finality-grandpa/src/communication/gossip.rs @@ -814,7 +814,7 @@ impl Inner { return Action::Discard(cost::UNKNOWN_VOTER); } - if let Err(()) = super::check_message_sig::( + if let Err(()) = sp_finality_grandpa::check_message_signature( &full.message.message, &full.message.id, &full.message.signature, diff --git a/client/finality-grandpa/src/communication/mod.rs b/client/finality-grandpa/src/communication/mod.rs index c75fe251e78ce..16af54986a02a 100644 --- a/client/finality-grandpa/src/communication/mod.rs +++ b/client/finality-grandpa/src/communication/mod.rs @@ -610,30 +610,6 @@ impl> Clone for NetworkBridge { } } -/// Encode round message localized to a given round and set id. -pub(crate) fn localized_payload( - round: RoundNumber, - set_id: SetIdNumber, - message: &E, -) -> Vec { - let mut buf = Vec::new(); - localized_payload_with_buffer(round, set_id, message, &mut buf); - buf -} - -/// Encode round message localized to a given round and set id using the given -/// buffer. The given buffer will be cleared and the resulting encoded payload -/// will always be written to the start of the buffer. -pub(crate) fn localized_payload_with_buffer( - round: RoundNumber, - set_id: SetIdNumber, - message: &E, - buf: &mut Vec, -) { - buf.clear(); - (message, round, set_id).encode_to(buf) -} - /// Type-safe wrapper around a round number. #[derive(Debug, Clone, Copy, Eq, PartialEq, PartialOrd, Ord, Encode, Decode)] pub struct Round(pub RoundNumber); @@ -642,48 +618,6 @@ pub struct Round(pub RoundNumber); #[derive(Debug, Clone, Copy, Eq, PartialEq, PartialOrd, Ord, Encode, Decode)] pub struct SetId(pub SetIdNumber); -/// Check a message signature by encoding the message as a localized payload and -/// verifying the provided signature using the expected authority id. -pub(crate) fn check_message_sig( - message: &Message, - id: &AuthorityId, - signature: &AuthoritySignature, - round: RoundNumber, - set_id: SetIdNumber, -) -> Result<(), ()> { - check_message_sig_with_buffer::( - message, - id, - signature, - round, - set_id, - &mut Vec::new(), - ) -} - -/// Check a message signature by encoding the message as a localized payload and -/// verifying the provided signature using the expected authority id. -/// The encoding necessary to verify the signature will be done using the given -/// buffer, the original content of the buffer will be cleared. -pub(crate) fn check_message_sig_with_buffer( - message: &Message, - id: &AuthorityId, - signature: &AuthoritySignature, - round: RoundNumber, - set_id: SetIdNumber, - buf: &mut Vec, -) -> Result<(), ()> { - let as_public = id.clone(); - localized_payload_with_buffer(round, set_id, message, buf); - - if AuthorityPair::verify(signature, buf, &as_public) { - Ok(()) - } else { - debug!(target: "afg", "Bad signature on message from {:?}", id); - Err(()) - } -} - /// A sink for outgoing messages to the network. Any messages that are sent will /// be replaced, as appropriate, according to the given `HasVoted`. /// NOTE: The votes are stored unsigned, which means that the signatures need to @@ -731,16 +665,14 @@ impl Sink> for OutgoingMessages } // when locals exist, sign messages on import - if let Some((ref pair, ref local_id)) = self.locals { - let encoded = localized_payload(self.round, self.set_id, &msg); - let signature = pair.sign(&encoded[..]); - + if let Some((ref pair, _)) = self.locals { let target_hash = msg.target().0.clone(); - let signed = SignedMessage:: { - message: msg, - signature, - id: local_id.clone(), - }; + let signed = sp_finality_grandpa::sign_message( + msg, + pair, + self.round, + self.set_id, + ); let message = GossipMessage::Vote(VoteMessage:: { message: signed.clone(), @@ -828,7 +760,7 @@ fn check_compact_commit( use crate::communication::gossip::Misbehavior; use finality_grandpa::Message as GrandpaMessage; - if let Err(()) = check_message_sig_with_buffer::( + if let Err(()) = sp_finality_grandpa::check_message_signature_with_buffer( &GrandpaMessage::Precommit(precommit.clone()), id, sig, @@ -916,7 +848,7 @@ fn check_catch_up( for (msg, id, sig) in messages { signatures_checked += 1; - if let Err(()) = check_message_sig_with_buffer::( + if let Err(()) = sp_finality_grandpa::check_message_signature_with_buffer( &msg, id, sig, diff --git a/client/finality-grandpa/src/communication/tests.rs b/client/finality-grandpa/src/communication/tests.rs index 883fdb4f26fbd..273804f7a4508 100644 --- a/client/finality-grandpa/src/communication/tests.rs +++ b/client/finality-grandpa/src/communication/tests.rs @@ -226,7 +226,7 @@ fn good_commit_leads_to_relay() { let target_number = 500; let precommit = finality_grandpa::Precommit { target_hash: target_hash.clone(), target_number }; - let payload = super::localized_payload( + let payload = sp_finality_grandpa::localized_payload( round, set_id, &finality_grandpa::Message::Precommit(precommit.clone()) ); @@ -374,7 +374,7 @@ fn bad_commit_leads_to_report() { let target_number = 500; let precommit = finality_grandpa::Precommit { target_hash: target_hash.clone(), target_number }; - let payload = super::localized_payload( + let payload = sp_finality_grandpa::localized_payload( round, set_id, &finality_grandpa::Message::Precommit(precommit.clone()) ); diff --git a/client/finality-grandpa/src/environment.rs b/client/finality-grandpa/src/environment.rs index 1c11c52468d49..1db1bcbb8d4a5 100644 --- a/client/finality-grandpa/src/environment.rs +++ b/client/finality-grandpa/src/environment.rs @@ -25,14 +25,14 @@ use parity_scale_codec::{Decode, Encode}; use futures::prelude::*; use futures_timer::Delay; use parking_lot::RwLock; -use sp_blockchain::{HeaderBackend, Error as ClientError, HeaderMetadata}; use std::marker::PhantomData; use sc_client_api::{backend::{Backend, apply_aux}, utils::is_descendent_of}; use finality_grandpa::{ - BlockNumberOps, Equivocation, Error as GrandpaError, round::State as RoundState, + BlockNumberOps, Error as GrandpaError, round::State as RoundState, voter, voter_set::VoterSet, }; +use sp_blockchain::{HeaderBackend, HeaderMetadata, Error as ClientError}; use sp_core::Pair; use sp_runtime::generic::BlockId; use sp_runtime::traits::{ @@ -53,7 +53,10 @@ use crate::consensus_changes::SharedConsensusChanges; use crate::justification::GrandpaJustification; use crate::until_imported::UntilVoteTargetImported; use crate::voting_rule::VotingRule; -use sp_finality_grandpa::{AuthorityId, AuthoritySignature, SetId, RoundNumber}; +use sp_finality_grandpa::{ + AuthorityId, AuthoritySignature, Equivocation, EquivocationProof, + GrandpaApi, RoundNumber, SetId, +}; use prometheus_endpoint::{Gauge, U64, register, PrometheusError}; type HistoricalVotes = finality_grandpa::HistoricalVotes< @@ -124,7 +127,7 @@ impl CompletedRounds { let mut rounds = Vec::with_capacity(NUM_LAST_COMPLETED_ROUNDS); rounds.push(genesis); - let voters = voters.current().1.iter().map(|(a, _)| a.clone()).collect(); + let voters = voters.current_authorities.iter().map(|(a, _)| a.clone()).collect(); CompletedRounds { rounds, set_id, voters } } @@ -403,7 +406,7 @@ pub(crate) struct Environment, SC, pub(crate) _phantom: PhantomData, } -impl, SC, VR> Environment { +impl, SC, VR> Environment { /// Updates the voter set state using the given closure. The write lock is /// held during evaluation of the closure and the environment's voter set /// state is set to its result if successful. @@ -430,6 +433,98 @@ impl, SC, VR> Environment Environment +where + Block: BlockT, + BE: Backend, + C: crate::ClientForGrandpa, + C::Api: GrandpaApi, + N: NetworkT, + SC: SelectChain + 'static, +{ + /// Report the given equivocation to the GRANDPA runtime module. This method + /// generates a session membership proof of the offender and then submits an + /// extrinsic to report the equivocation. In particular, the session membership + /// proof must be generated at the block at which the given set was active which + /// isn't necessarily the best block if there are pending authority set changes. + fn report_equivocation( + &self, + equivocation: Equivocation>, + ) -> Result<(), Error> { + let is_descendent_of = is_descendent_of(&*self.client, None); + + let best_header = self.select_chain + .best_chain() + .map_err(|e| Error::Blockchain(e.to_string()))?; + + let authority_set = self.authority_set.inner().read(); + + // block hash and number of the next pending authority set change in the + // given best chain. + let next_change = authority_set + .next_change(&best_header.hash(), &is_descendent_of) + .map_err(|e| Error::Safety(e.to_string()))?; + + // find the hash of the latest block in the current set + let current_set_latest_hash = match next_change { + Some((_, n)) if n.is_zero() => { + return Err(Error::Safety( + "Authority set change signalled at genesis.".to_string(), + )) + } + // the next set starts at `n` so the current one lasts until `n - 1`. if + // `n` is later than the best block, then the current set is still live + // at best block. + Some((_, n)) if n > *best_header.number() => best_header.hash(), + Some((h, _)) => { + // this is the header at which the new set will start + let header = self.client.header(BlockId::Hash(h))?.expect( + "got block hash from registered pending change; \ + pending changes are only registered on block import; qed.", + ); + + // its parent block is the last block in the current set + *header.parent_hash() + } + // there is no pending change, the latest block for the current set is + // the best block. + None => best_header.hash(), + }; + + // generate key ownership proof at that block + let key_owner_proof = match self.client + .runtime_api() + .generate_key_ownership_proof( + &BlockId::Hash(current_set_latest_hash), + authority_set.set_id, + equivocation.offender().clone(), + ) + .map_err(Error::Client)? + { + Some(proof) => proof, + None => { + debug!(target: "afg", "Equivocation offender is not part of the authority set."); + return Ok(()); + } + }; + + // submit equivocation report at **best** block + let equivocation_proof = EquivocationProof::new( + authority_set.set_id, + equivocation, + ); + + self.client.runtime_api() + .submit_report_equivocation_extrinsic( + &BlockId::Hash(best_header.hash()), + equivocation_proof, + key_owner_proof, + ).map_err(Error::Client)?; + + Ok(()) + } +} + impl finality_grandpa::Chain> for Environment @@ -437,7 +532,7 @@ where Block: 'static, BE: Backend, C: crate::ClientForGrandpa, - N: NetworkT + 'static + Send, + N: NetworkT + 'static + Send, SC: SelectChain + 'static, VR: VotingRule, NumberFor: BlockNumberOps, @@ -451,7 +546,7 @@ where // signaled asynchronously. therefore the voter could still vote in the next round // before activating the new set. the `authority_set` is updated immediately thus we // restrict the voter based on that. - if self.set_id != self.authority_set.inner().read().current().0 { + if self.set_id != self.authority_set.set_id() { return None; } @@ -579,7 +674,8 @@ where Block: 'static, B: Backend, C: crate::ClientForGrandpa + 'static, - N: NetworkT + 'static + Send + Sync, + C::Api: GrandpaApi, + N: NetworkT + 'static + Send + Sync, SC: SelectChain + 'static, VR: VotingRule, NumberFor: BlockNumberOps, @@ -925,19 +1021,23 @@ where fn prevote_equivocation( &self, _round: RoundNumber, - equivocation: ::finality_grandpa::Equivocation, Self::Signature> + equivocation: finality_grandpa::Equivocation, Self::Signature>, ) { warn!(target: "afg", "Detected prevote equivocation in the finality worker: {:?}", equivocation); - // nothing yet; this could craft misbehavior reports of some kind. + if let Err(err) = self.report_equivocation(equivocation.into()) { + warn!(target: "afg", "Error reporting prevote equivocation: {:?}", err); + } } fn precommit_equivocation( &self, _round: RoundNumber, - equivocation: Equivocation, Self::Signature> + equivocation: finality_grandpa::Equivocation, Self::Signature>, ) { warn!(target: "afg", "Detected precommit equivocation in the finality worker: {:?}", equivocation); - // nothing yet + if let Err(err) = self.report_equivocation(equivocation.into()) { + warn!(target: "afg", "Error reporting precommit equivocation: {:?}", err); + } } } diff --git a/client/finality-grandpa/src/justification.rs b/client/finality-grandpa/src/justification.rs index ebce90f2c18a8..cbaa2cb4415b8 100644 --- a/client/finality-grandpa/src/justification.rs +++ b/client/finality-grandpa/src/justification.rs @@ -26,7 +26,6 @@ use sp_runtime::traits::{NumberFor, Block as BlockT, Header as HeaderT}; use sp_finality_grandpa::AuthorityId; use crate::{Commit, Error}; -use crate::communication; /// A GRANDPA justification for block finality, it includes a commit message and /// an ancestry proof including all headers routing all precommit target blocks @@ -132,7 +131,7 @@ impl GrandpaJustification { let mut buf = Vec::new(); let mut visited_hashes = HashSet::new(); for signed in self.commit.precommits.iter() { - if let Err(_) = communication::check_message_sig_with_buffer::( + if let Err(_) = sp_finality_grandpa::check_message_signature_with_buffer( &finality_grandpa::Message::Precommit(signed.precommit.clone()), &signed.id, &signed.signature, diff --git a/client/finality-grandpa/src/lib.rs b/client/finality-grandpa/src/lib.rs index 9b64be895d802..ac677bf3f32d2 100644 --- a/client/finality-grandpa/src/lib.rs +++ b/client/finality-grandpa/src/lib.rs @@ -62,9 +62,10 @@ use sc_client_api::{ LockImportRun, BlockchainEvents, CallExecutor, ExecutionStrategy, Finalizer, TransactionFor, ExecutorProvider, }; -use sp_blockchain::{HeaderBackend, Error as ClientError, HeaderMetadata}; use parity_scale_codec::{Decode, Encode}; use prometheus_endpoint::{PrometheusError, Registry}; +use sp_api::ProvideRuntimeApi; +use sp_blockchain::{HeaderBackend, Error as ClientError, HeaderMetadata}; use sp_runtime::generic::BlockId; use sp_runtime::traits::{NumberFor, Block as BlockT, DigestFor, Zero}; use sc_keystore::KeyStorePtr; @@ -134,8 +135,7 @@ use communication::{NetworkBridge, Network as NetworkT}; use sp_finality_grandpa::{AuthorityList, AuthorityPair, AuthoritySignature, SetId}; // Re-export these two because it's just so damn convenient. -pub use sp_finality_grandpa::{AuthorityId, ScheduledChange}; -use sp_api::ProvideRuntimeApi; +pub use sp_finality_grandpa::{AuthorityId, GrandpaApi, ScheduledChange}; use std::marker::PhantomData; #[cfg(test)] @@ -143,6 +143,7 @@ mod tests; /// A GRANDPA message for a substrate chain. pub type Message = finality_grandpa::Message<::Hash, NumberFor>; + /// A signed message. pub type SignedMessage = finality_grandpa::SignedMessage< ::Hash, @@ -687,6 +688,7 @@ pub fn run_grandpa_voter( NumberFor: BlockNumberOps, DigestFor: Encode, C: ClientForGrandpa + 'static, + C::Api: GrandpaApi, { let GrandpaParams { mut config, @@ -802,6 +804,7 @@ where Block: BlockT, B: Backend + 'static, C: ClientForGrandpa + 'static, + C::Api: GrandpaApi, N: NetworkT + Sync, NumberFor: BlockNumberOps, SC: SelectChain + 'static, @@ -971,7 +974,6 @@ where voters, set_id: new.set_id, voter_set_state: self.env.voter_set_state.clone(), - // Fields below are simply transferred and not updated. client: self.env.client.clone(), select_chain: self.env.select_chain.clone(), config: self.env.config.clone(), @@ -1013,6 +1015,7 @@ where NumberFor: BlockNumberOps, SC: SelectChain + 'static, C: ClientForGrandpa + 'static, + C::Api: GrandpaApi, VR: VotingRule + Clone + 'static, { type Output = Result<(), Error>; diff --git a/client/finality-grandpa/src/observer.rs b/client/finality-grandpa/src/observer.rs index ac67675fef841..ab06f06280c83 100644 --- a/client/finality-grandpa/src/observer.rs +++ b/client/finality-grandpa/src/observer.rs @@ -175,6 +175,7 @@ where select_chain: _, persistent_data, voter_commands_rx, + .. } = link; let network = NetworkBridge::new( diff --git a/client/finality-grandpa/src/tests.rs b/client/finality-grandpa/src/tests.rs index 5eafb3c15dee9..64e5b0545b546 100644 --- a/client/finality-grandpa/src/tests.rs +++ b/client/finality-grandpa/src/tests.rs @@ -40,7 +40,7 @@ use parity_scale_codec::Decode; use sp_runtime::traits::{Block as BlockT, Header as HeaderT, HashFor}; use sp_runtime::generic::{BlockId, DigestItem}; use sp_core::{H256, crypto::Public}; -use sp_finality_grandpa::{GRANDPA_ENGINE_ID, AuthorityList, GrandpaApi}; +use sp_finality_grandpa::{GRANDPA_ENGINE_ID, AuthorityList, EquivocationProof, GrandpaApi, OpaqueKeyOwnershipProof}; use sp_state_machine::{InMemoryBackend, prove_read, read_proof_check}; use authorities::AuthoritySet; @@ -214,6 +214,20 @@ sp_api::mock_impl_runtime_apis! { fn grandpa_authorities(&self) -> AuthorityList { self.inner.genesis_authorities.clone() } + + fn submit_report_equivocation_extrinsic( + _equivocation_proof: EquivocationProof, + _key_owner_proof: OpaqueKeyOwnershipProof, + ) -> Option<()> { + None + } + + fn generate_key_ownership_proof( + _set_id: SetId, + _authority_id: AuthorityId, + ) -> Option { + None + } } } @@ -1664,7 +1678,7 @@ fn imports_justification_for_regular_blocks_on_import() { }; let msg = finality_grandpa::Message::Precommit(precommit.clone()); - let encoded = communication::localized_payload(round, set_id, &msg); + let encoded = sp_finality_grandpa::localized_payload(round, set_id, &msg); let signature = peers[0].sign(&encoded[..]).into(); let precommit = finality_grandpa::SignedPrecommit { diff --git a/frame/grandpa/Cargo.toml b/frame/grandpa/Cargo.toml index 2e1dbc4deba4c..649b4053d0536 100644 --- a/frame/grandpa/Cargo.toml +++ b/frame/grandpa/Cargo.toml @@ -14,8 +14,10 @@ targets = ["x86_64-unknown-linux-gnu"] [dependencies] serde = { version = "1.0.101", optional = true, features = ["derive"] } codec = { package = "parity-scale-codec", version = "1.3.0", default-features = false, features = ["derive"] } +sp-application-crypto = { version = "2.0.0-dev", default-features = false, path = "../../primitives/application-crypto" } sp-core = { version = "2.0.0-dev", default-features = false, path = "../../primitives/core" } sp-finality-grandpa = { version = "2.0.0-dev", default-features = false, path = "../../primitives/finality-grandpa" } +sp-session = { version = "2.0.0-dev", default-features = false, path = "../../primitives/session" } sp-std = { version = "2.0.0-dev", default-features = false, path = "../../primitives/std" } sp-runtime = { version = "2.0.0-dev", default-features = false, path = "../../primitives/runtime" } sp-staking = { version = "2.0.0-dev", default-features = false, path = "../../primitives/staking" } @@ -25,15 +27,24 @@ pallet-session = { version = "2.0.0-dev", default-features = false, path = "../s pallet-finality-tracker = { version = "2.0.0-dev", default-features = false, path = "../finality-tracker" } [dev-dependencies] -sp-io ={ version = "2.0.0-dev", path = "../../primitives/io" } +grandpa = { package = "finality-grandpa", version = "0.12.2", features = ["derive-codec"] } +sp-io = { version = "2.0.0-dev", path = "../../primitives/io" } +sp-keyring = { version = "2.0.0-dev", path = "../../primitives/keyring" } +pallet-balances = { version = "2.0.0-dev", path = "../balances" } +pallet-offences = { version = "2.0.0-dev", path = "../offences" } +pallet-staking = { version = "2.0.0-dev", path = "../staking" } +pallet-staking-reward-curve = { version = "2.0.0-dev", path = "../staking/reward-curve" } +pallet-timestamp = { version = "2.0.0-dev", path = "../timestamp" } [features] default = ["std"] std = [ "serde", "codec/std", + "sp-application-crypto/std", "sp-core/std", "sp-finality-grandpa/std", + "sp-session/std", "sp-std/std", "frame-support/std", "sp-runtime/std", diff --git a/frame/grandpa/src/equivocation.rs b/frame/grandpa/src/equivocation.rs new file mode 100644 index 0000000000000..f9ae43021673b --- /dev/null +++ b/frame/grandpa/src/equivocation.rs @@ -0,0 +1,406 @@ +// Copyright 2017-2020 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 . + +//! +//! An opt-in utility module for reporting equivocations. +//! +//! This module defines an offence type for GRANDPA equivocations +//! and some utility traits to wire together: +//! - a key ownership proof system (e.g. to prove that a given authority was +//! part of a session); +//! - a system for reporting offences; +//! - a system for signing and submitting transactions; +//! +//! These can be used in an offchain context in order to submit equivocation +//! reporting extrinsics (from the client that's running the GRANDPA protocol). +//! And in a runtime context, so that the GRANDPA module can validate the +//! equivocation proofs in the extrinsic and report the offences. +//! + +use sp_std::prelude::*; + +use codec::{self as codec, Decode, Encode}; +use frame_support::{debug, dispatch::IsSubType, traits::KeyOwnerProofSystem}; +use frame_system::offchain::{AppCrypto, CreateSignedTransaction, Signer}; +use sp_finality_grandpa::{EquivocationProof, RoundNumber, SetId}; +use sp_runtime::{ + traits::{DispatchInfoOf, SignedExtension}, + transaction_validity::{ + InvalidTransaction, TransactionValidity, TransactionValidityError, ValidTransaction, + }, + DispatchResult, Perbill, +}; +use sp_staking::{ + offence::{Kind, Offence, OffenceError, ReportOffence}, + SessionIndex, +}; + +/// Ensure that equivocation reports are only processed if valid. +#[derive(Encode, Decode, Clone, Eq, PartialEq)] +pub struct ValidateEquivocationReport(sp_std::marker::PhantomData); + +impl Default for ValidateEquivocationReport { + fn default() -> ValidateEquivocationReport { + ValidateEquivocationReport::new() + } +} + +impl ValidateEquivocationReport { + pub fn new() -> ValidateEquivocationReport { + ValidateEquivocationReport(Default::default()) + } +} + +impl sp_std::fmt::Debug for ValidateEquivocationReport { + fn fmt(&self, f: &mut sp_std::fmt::Formatter) -> sp_std::fmt::Result { + write!(f, "ValidateEquivocationReport") + } +} + +/// Custom validity error used when validating equivocation reports. +#[derive(Debug)] +#[repr(u8)] +pub enum ReportEquivocationValidityError { + /// The proof provided in the report is not valid. + InvalidEquivocationProof = 1, + /// The proof provided in the report is not valid. + InvalidKeyOwnershipProof = 2, + /// The set id provided in the report is not valid. + InvalidSetId = 3, + /// The session index provided in the report is not valid. + InvalidSession = 4, +} + +impl From for TransactionValidityError { + fn from(e: ReportEquivocationValidityError) -> TransactionValidityError { + TransactionValidityError::from(InvalidTransaction::Custom(e as u8)) + } +} + +impl SignedExtension for ValidateEquivocationReport +where + ::Call: IsSubType, T>, +{ + const IDENTIFIER: &'static str = "ValidateEquivocationReport"; + type AccountId = T::AccountId; + type Call = ::Call; + type AdditionalSigned = (); + type Pre = (); + + fn additional_signed( + &self, + ) -> sp_std::result::Result { + Ok(()) + } + + fn validate( + &self, + _who: &Self::AccountId, + call: &Self::Call, + _info: &DispatchInfoOf, + _len: usize, + ) -> TransactionValidity { + let (equivocation_proof, key_owner_proof) = match call.is_sub_type() { + Some(super::Call::report_equivocation(equivocation_proof, key_owner_proof)) => { + (equivocation_proof, key_owner_proof) + } + _ => return Ok(ValidTransaction::default()), + }; + + // validate the key ownership proof extracting the id of the offender. + if let None = T::KeyOwnerProofSystem::check_proof( + ( + sp_finality_grandpa::KEY_TYPE, + equivocation_proof.offender().clone(), + ), + key_owner_proof.clone(), + ) { + return Err(ReportEquivocationValidityError::InvalidKeyOwnershipProof.into()); + } + + // we check the equivocation within the context of its set id (and + // associated session). + let set_id = equivocation_proof.set_id(); + let session_index = key_owner_proof.session(); + + // validate equivocation proof (check votes are different and + // signatures are valid). + if let Err(_) = sp_finality_grandpa::check_equivocation_proof(equivocation_proof.clone()) { + return Err(ReportEquivocationValidityError::InvalidEquivocationProof.into()); + } + + // fetch the current and previous sets last session index. on the + // genesis set there's no previous set. + let previous_set_id_session_index = if set_id == 0 { + None + } else { + let session_index = + if let Some(session_id) = >::session_for_set(set_id - 1) { + session_id + } else { + return Err(ReportEquivocationValidityError::InvalidSetId.into()); + }; + + Some(session_index) + }; + + let set_id_session_index = + if let Some(session_id) = >::session_for_set(set_id) { + session_id + } else { + return Err(ReportEquivocationValidityError::InvalidSetId.into()); + }; + + // check that the session id for the membership proof is within the + // bounds of the set id reported in the equivocation. + if session_index > set_id_session_index || + previous_set_id_session_index + .map(|previous_index| session_index <= previous_index) + .unwrap_or(false) + { + return Err(ReportEquivocationValidityError::InvalidSession.into()); + } + + Ok(ValidTransaction::default()) + } +} + +/// A trait with utility methods for handling equivocation reports in GRANDPA. +/// The offence type is generic, and the trait provides , reporting an offence +/// triggered by a valid equivocation report, and also for creating and +/// submitting equivocation report extrinsics (useful only in offchain context). +pub trait HandleEquivocation { + /// The offence type used for reporting offences on valid equivocation reports. + type Offence: GrandpaOffence; + + /// Report an offence proved by the given reporters. + fn report_offence( + reporters: Vec, + offence: Self::Offence, + ) -> Result<(), OffenceError>; + + /// Create and dispatch an equivocation report extrinsic. + fn submit_equivocation_report( + equivocation_proof: EquivocationProof, + key_owner_proof: T::KeyOwnerProof, + ) -> DispatchResult; +} + +impl HandleEquivocation for () { + type Offence = GrandpaEquivocationOffence; + + fn report_offence( + _reporters: Vec, + _offence: GrandpaEquivocationOffence, + ) -> Result<(), OffenceError> { + Ok(()) + } + + fn submit_equivocation_report( + _equivocation_proof: EquivocationProof, + _key_owner_proof: T::KeyOwnerProof, + ) -> DispatchResult { + Ok(()) + } +} + +/// Generic equivocation handler. This type implements `HandleEquivocation` +/// using existing subsystems that are part of frame (type bounds described +/// below) and will dispatch to them directly, it's only purpose is to wire all +/// subsystems together. +pub struct EquivocationHandler> { + _phantom: sp_std::marker::PhantomData<(I, C, S, R, O)>, +} + +impl Default for EquivocationHandler { + fn default() -> Self { + Self { + _phantom: Default::default(), + } + } +} + +impl HandleEquivocation + for EquivocationHandler +where + // A signed transaction creator. Used for signing and submitting equivocation reports. + T: super::Trait + CreateSignedTransaction>, + // Application-specific crypto bindings. + C: AppCrypto, + // The offence type that should be used when reporting. + O: GrandpaOffence, + // A system for reporting offences after valid equivocation reports are + // processed. + R: ReportOffence, +{ + type Offence = O; + + fn report_offence(reporters: Vec, offence: O) -> Result<(), OffenceError> { + R::report_offence(reporters, offence) + } + + fn submit_equivocation_report( + equivocation_proof: EquivocationProof, + key_owner_proof: T::KeyOwnerProof, + ) -> DispatchResult { + use frame_system::offchain::SendSignedTransaction; + + let signer = Signer::::all_accounts(); + if !signer.can_sign() { + return Err( + "No local accounts available. Consider adding one via `author_insertKey` RPC.", + )?; + } + + let results = signer.send_signed_transaction(|_account| { + super::Call::report_equivocation(equivocation_proof.clone(), key_owner_proof.clone()) + }); + + for (acc, res) in &results { + match res { + Ok(()) => debug::info!("[{:?}] Submitted GRANDPA equivocation report.", acc.id), + Err(e) => debug::error!( + "[{:?}] Error submitting equivocation report: {:?}", + acc.id, + e + ), + } + } + + Ok(()) + } +} + +/// A round number and set id which point on the time of an offence. +#[derive(Copy, Clone, PartialOrd, Ord, Eq, PartialEq, Encode, Decode)] +pub struct GrandpaTimeSlot { + // The order of these matters for `derive(Ord)`. + /// Grandpa Set ID. + pub set_id: SetId, + /// Round number. + pub round: RoundNumber, +} + +/// A grandpa equivocation offence report. +#[allow(dead_code)] +pub struct GrandpaEquivocationOffence { + /// Time slot at which this incident happened. + pub time_slot: GrandpaTimeSlot, + /// The session index in which the incident happened. + pub session_index: SessionIndex, + /// The size of the validator set at the time of the offence. + pub validator_set_count: u32, + /// The authority which produced this equivocation. + pub offender: FullIdentification, +} + +/// An interface for types that will be used as GRANDPA offences and must also +/// implement the `Offence` trait. This trait provides a constructor that is +/// provided all available data during processing of GRANDPA equivocations. +pub trait GrandpaOffence: Offence { + /// Create a new GRANDPA offence using the given equivocation details. + fn new( + session_index: SessionIndex, + validator_set_count: u32, + offender: FullIdentification, + set_id: SetId, + round: RoundNumber, + ) -> Self; +} + +impl GrandpaOffence + for GrandpaEquivocationOffence +{ + fn new( + session_index: SessionIndex, + validator_set_count: u32, + offender: FullIdentification, + set_id: SetId, + round: RoundNumber, + ) -> Self { + GrandpaEquivocationOffence { + session_index, + validator_set_count, + offender, + time_slot: GrandpaTimeSlot { set_id, round }, + } + } +} + +impl Offence + for GrandpaEquivocationOffence +{ + const ID: Kind = *b"grandpa:equivoca"; + type TimeSlot = GrandpaTimeSlot; + + fn offenders(&self) -> Vec { + vec![self.offender.clone()] + } + + fn session_index(&self) -> SessionIndex { + self.session_index + } + + fn validator_set_count(&self) -> u32 { + self.validator_set_count + } + + fn time_slot(&self) -> Self::TimeSlot { + self.time_slot + } + + fn slash_fraction(offenders_count: u32, validator_set_count: u32) -> Perbill { + // the formula is min((3k / n)^2, 1) + let x = Perbill::from_rational_approximation(3 * offenders_count, validator_set_count); + // _ ^ 2 + x.square() + } +} + +/// A trait to get a session number the `MembershipProof` belongs to. +pub trait GetSessionNumber { + fn session(&self) -> SessionIndex; +} + +/// A trait to get the validator count at the session the `MembershipProof` +/// belongs to. +pub trait GetValidatorCount { + fn validator_count(&self) -> sp_session::ValidatorCount; +} + +impl GetSessionNumber for frame_support::Void { + fn session(&self) -> SessionIndex { + Default::default() + } +} + +impl GetValidatorCount for frame_support::Void { + fn validator_count(&self) -> sp_session::ValidatorCount { + Default::default() + } +} + +impl GetSessionNumber for sp_session::MembershipProof { + fn session(&self) -> SessionIndex { + self.session() + } +} + +impl GetValidatorCount for sp_session::MembershipProof { + fn validator_count(&self) -> sp_session::ValidatorCount { + self.validator_count() + } +} diff --git a/frame/grandpa/src/lib.rs b/frame/grandpa/src/lib.rs index 5827d2f195b3c..16aebe335f9ae 100644 --- a/frame/grandpa/src/lib.rs +++ b/frame/grandpa/src/lib.rs @@ -31,27 +31,65 @@ pub use sp_finality_grandpa as fg_primitives; use sp_std::prelude::*; -use codec::{self as codec, Encode, Decode}; -use frame_support::{decl_event, decl_storage, decl_module, decl_error, storage}; -use sp_runtime::{ - DispatchResult, generic::{DigestItem, OpaqueDigestItemId}, traits::Zero, Perbill, -}; -use sp_staking::{ - SessionIndex, - offence::{Offence, Kind}, -}; + +use codec::{self as codec, Decode, Encode}; +pub use fg_primitives::{AuthorityId, AuthorityList, AuthorityWeight, VersionedAuthorityList}; use fg_primitives::{ - GRANDPA_AUTHORITIES_KEY, GRANDPA_ENGINE_ID, ScheduledChange, ConsensusLog, SetId, RoundNumber, + ConsensusLog, EquivocationProof, ScheduledChange, SetId, GRANDPA_AUTHORITIES_KEY, + GRANDPA_ENGINE_ID, +}; +use frame_support::{ + decl_error, decl_event, decl_module, decl_storage, storage, traits::KeyOwnerProofSystem, + Parameter, }; -pub use fg_primitives::{AuthorityId, AuthorityList, AuthorityWeight, VersionedAuthorityList}; use frame_system::{self as system, ensure_signed, DigestOf}; +use sp_runtime::{ + generic::{DigestItem, OpaqueDigestItemId}, + traits::Zero, + DispatchResult, KeyTypeId, +}; +use sp_staking::SessionIndex; +mod equivocation; mod mock; mod tests; +pub use equivocation::{ + EquivocationHandler, GetSessionNumber, GetValidatorCount, GrandpaEquivocationOffence, + GrandpaOffence, GrandpaTimeSlot, HandleEquivocation, ValidateEquivocationReport, +}; + pub trait Trait: frame_system::Trait { /// The event type of this module. type Event: From + Into<::Event>; + + /// The function call. + type Call: From>; + + /// The proof of key ownership, used for validating equivocation reports. + /// The proof must include the session index and validator count of the + /// session at which the equivocation occurred. + type KeyOwnerProof: Parameter + GetSessionNumber + GetValidatorCount; + + /// The identification of a key owner, used when reporting equivocations. + type KeyOwnerIdentification: Parameter; + + /// A system for proving ownership of keys, i.e. that a given key was part + /// of a validator set, needed for validating equivocation reports. + type KeyOwnerProofSystem: KeyOwnerProofSystem< + (KeyTypeId, AuthorityId), + Proof = Self::KeyOwnerProof, + IdentificationTuple = Self::KeyOwnerIdentification, + >; + + /// The equivocation handling subsystem, defines methods to report an + /// offence (after the equivocation has been validated) and for submitting a + /// transaction to report an equivocation (from an offchain context). + /// NOTE: when enabling equivocation handling (i.e. this type isn't set to + /// `()`) you must add the `equivocation::ValidateEquivocationReport` signed + /// extension to the runtime's `SignedExtra` definition, otherwise + /// equivocation reports won't be properly validated. + type HandleEquivocation: HandleEquivocation; } /// A stored pending change, old format. @@ -173,7 +211,9 @@ decl_storage! { } add_extra_genesis { config(authorities): AuthorityList; - build(|config| Module::::initialize_authorities(&config.authorities)) + build(|config| { + Module::::initialize(&config.authorities) + }) } } @@ -183,11 +223,49 @@ decl_module! { fn deposit_event() = default; - /// Report some misbehavior. + /// Report voter equivocation/misbehavior. This method will verify the + /// equivocation proof and validate the given key ownership proof + /// against the extracted offender. If both are valid, the offence + /// will be reported. + /// + /// Since the weight is 0 in order to avoid DoS pre-validation is implemented in a + /// `SignedExtension`. #[weight = 0] - fn report_misbehavior(origin, _report: Vec) { - ensure_signed(origin)?; - // FIXME: https://github.com/paritytech/substrate/issues/1112 + fn report_equivocation( + origin, + equivocation_proof: EquivocationProof, + key_owner_proof: T::KeyOwnerProof, + ) { + let reporter_id = ensure_signed(origin)?; + + let (session_index, validator_set_count) = ( + key_owner_proof.session(), + key_owner_proof.validator_count(), + ); + + // we have already checked this proof in `SignedExtension`, we to + // check it again to get the full identification of the offender. + let offender = + T::KeyOwnerProofSystem::check_proof( + (fg_primitives::KEY_TYPE, equivocation_proof.offender().clone()), + key_owner_proof, + ).ok_or("Invalid key ownership proof.")?; + + // the set id and round when the offence happened + let set_id = equivocation_proof.set_id(); + let round = equivocation_proof.round(); + + // report to the offences module rewarding the sender. + T::HandleEquivocation::report_offence( + vec![reporter_id], + >::Offence::new( + session_index, + validator_set_count, + offender, + set_id, + round, + ), + ).map_err(|_| "Duplicate offence report.")?; } fn on_finalize(block_number: T::BlockNumber) { @@ -351,7 +429,9 @@ impl Module { >::deposit_log(log.into()); } - fn initialize_authorities(authorities: &AuthorityList) { + // Perform module initialization, abstracted so that it can be called either through genesis + // config builder or through `on_genesis_session`. + fn initialize(authorities: &AuthorityList) { if !authorities.is_empty() { assert!( Self::grandpa_authorities().is_empty(), @@ -359,6 +439,25 @@ impl Module { ); Self::set_grandpa_authorities(authorities); } + + // NOTE: initialize first session of first set. this is necessary + // because we only update this `on_new_session` which isn't called + // for the genesis session. + SetIdSession::insert(0, 0); + } + + /// Submits an extrinsic to report an equivocation. This method will sign an + /// extrinsic with a call to `report_equivocation` with any reporting keys + /// available in the keystore and will push the transaction to the pool. + /// Only useful in an offchain context. + pub fn submit_report_equivocation_extrinsic( + equivocation_proof: EquivocationProof, + key_owner_proof: T::KeyOwnerProof, + ) -> Option<()> { + T::HandleEquivocation::submit_equivocation_report(equivocation_proof, key_owner_proof) + .ok()?; + + Some(()) } } @@ -411,7 +510,7 @@ impl pallet_session::OneSessionHandler for Module where I: Iterator { let authorities = validators.map(|(_, k)| (k, 1)).collect::>(); - Self::initialize_authorities(&authorities); + Self::initialize(&authorities); } fn on_new_session<'a, I: 'a>(changed: bool, validators: I, _queued_validators: I) @@ -453,56 +552,3 @@ impl pallet_finality_tracker::OnFinalizationStalled fo >::put((further_wait, median)); } } - -/// A round number and set id which point on the time of an offence. -#[derive(Copy, Clone, PartialOrd, Ord, Eq, PartialEq, Encode, Decode)] -pub struct GrandpaTimeSlot { - // The order of these matters for `derive(Ord)`. - /// Grandpa Set ID. - pub set_id: SetId, - /// Round number. - pub round: RoundNumber, -} - -/// A grandpa equivocation offence report. -pub struct GrandpaEquivocationOffence { - /// Time slot at which this incident happened. - pub time_slot: GrandpaTimeSlot, - /// The session index in which the incident happened. - pub session_index: SessionIndex, - /// The size of the validator set at the time of the offence. - pub validator_set_count: u32, - /// The authority which produced this equivocation. - pub offender: FullIdentification, -} - -impl Offence for GrandpaEquivocationOffence { - const ID: Kind = *b"grandpa:equivoca"; - type TimeSlot = GrandpaTimeSlot; - - fn offenders(&self) -> Vec { - vec![self.offender.clone()] - } - - fn session_index(&self) -> SessionIndex { - self.session_index - } - - fn validator_set_count(&self) -> u32 { - self.validator_set_count - } - - fn time_slot(&self) -> Self::TimeSlot { - self.time_slot - } - - fn slash_fraction( - offenders_count: u32, - validator_set_count: u32, - ) -> Perbill { - // the formula is min((3k / n)^2, 1) - let x = Perbill::from_rational_approximation(3 * offenders_count, validator_set_count); - // _ ^ 2 - x.square() - } -} diff --git a/frame/grandpa/src/mock.rs b/frame/grandpa/src/mock.rs index 3ef78e7571184..f307f17fd4d53 100644 --- a/frame/grandpa/src/mock.rs +++ b/frame/grandpa/src/mock.rs @@ -18,41 +18,85 @@ #![cfg(test)] -use sp_runtime::{Perbill, DigestItem, traits::IdentityLookup, testing::{Header, UintAuthorityId}}; +use crate::{ + equivocation::ValidateEquivocationReport, AuthorityId, AuthorityList, Call as GrandpaCall, + ConsensusLog, Module, Trait, +}; +use ::grandpa as finality_grandpa; +use codec::Encode; +use frame_support::{ + impl_outer_dispatch, impl_outer_event, impl_outer_origin, parameter_types, + traits::{KeyOwnerProofSystem, OnFinalize, OnInitialize}, + weights::{DispatchInfo, Weight}, +}; +use pallet_staking::EraIndex; +use sp_core::{crypto::KeyTypeId, H256}; +use sp_finality_grandpa::{RoundNumber, SetId, GRANDPA_ENGINE_ID}; use sp_io; -use frame_support::{impl_outer_origin, impl_outer_event, parameter_types, weights::Weight}; -use sp_core::H256; -use codec::{Encode, Decode}; -use crate::{AuthorityId, AuthorityList, GenesisConfig, Trait, Module, ConsensusLog}; -use sp_finality_grandpa::GRANDPA_ENGINE_ID; +use sp_keyring::Ed25519Keyring; +use sp_runtime::{ + curve::PiecewiseLinear, + impl_opaque_keys, + testing::{Header, TestXt, UintAuthorityId}, + traits::{ + Convert, Extrinsic as ExtrinsicT, Header as _, IdentityLookup, OpaqueKeys, + SaturatedConversion, SignedExtension, + }, + transaction_validity::TransactionValidityError, + DigestItem, Perbill, +}; +use sp_staking::SessionIndex; use frame_system as system; -impl_outer_origin!{ - pub enum Origin for Test where system = frame_system {} +use pallet_balances as balances; +use pallet_offences as offences; +use pallet_session as session; +use pallet_staking as staking; +use pallet_timestamp as timestamp; + +impl_outer_origin! { + pub enum Origin for Test {} } -pub fn grandpa_log(log: ConsensusLog) -> DigestItem { - DigestItem::Consensus(GRANDPA_ENGINE_ID, log.encode()) +impl_outer_dispatch! { + pub enum Call for Test where origin: Origin { + grandpa::Grandpa, + staking::Staking, + } } -// Workaround for https://github.com/rust-lang/rust/issues/26925 . Remove when sorted. -#[derive(Clone, PartialEq, Eq, Debug, Decode, Encode)] -pub struct Test; +impl_opaque_keys! { + pub struct TestSessionKeys { + pub grandpa_authority: super::Module, + } +} -impl Trait for Test { - type Event = TestEvent; +impl_outer_event! { + pub enum TestEvent for Test { + system, + balances, + grandpa, + offences, + session, + staking, + } } + +#[derive(Clone, Eq, PartialEq)] +pub struct Test; + parameter_types! { pub const BlockHashCount: u64 = 250; pub const MaximumBlockWeight: Weight = 1024; pub const MaximumBlockLength: u32 = 2 * 1024; pub const AvailableBlockRatio: Perbill = Perbill::one(); } + impl frame_system::Trait for Test { type Origin = Origin; type Index = u64; type BlockNumber = u64; - type Call = (); + type Call = Call; type Hash = H256; type Hashing = sp_runtime::traits::BlakeTwo256; type AccountId = u64; @@ -68,20 +112,220 @@ impl frame_system::Trait for Test { type AvailableBlockRatio = AvailableBlockRatio; type Version = (); type ModuleToIndex = (); - type AccountData = (); + type AccountData = balances::AccountData; type OnNewAccount = (); type OnKilledAccount = (); } +impl system::offchain::SendTransactionTypes for Test +where + Call: From, +{ + type OverarchingCall = Call; + type Extrinsic = TestXt; +} + +parameter_types! { + pub const Period: u64 = 1; + pub const Offset: u64 = 0; + pub const DisabledValidatorsThreshold: Perbill = Perbill::from_percent(17); +} + +/// Custom `SessionHandler` since we use `TestSessionKeys` as `Keys`. +impl session::Trait for Test { + type Event = TestEvent; + type ValidatorId = u64; + type ValidatorIdOf = staking::StashOf; + type ShouldEndSession = session::PeriodicSessions; + type NextSessionRotation = session::PeriodicSessions; + type SessionManager = session::historical::NoteHistoricalRoot; + type SessionHandler = ::KeyTypeIdProviders; + type Keys = TestSessionKeys; + type DisabledValidatorsThreshold = DisabledValidatorsThreshold; +} + +impl session::historical::Trait for Test { + type FullIdentification = staking::Exposure; + type FullIdentificationOf = staking::ExposureOf; +} + +parameter_types! { + pub const ExistentialDeposit: u128 = 1; +} + +impl balances::Trait for Test { + type Balance = u128; + type DustRemoval = (); + type Event = TestEvent; + type ExistentialDeposit = ExistentialDeposit; + type AccountStore = System; +} + +parameter_types! { + pub const MinimumPeriod: u64 = 3; +} + +impl timestamp::Trait for Test { + type Moment = u64; + type OnTimestampSet = (); + type MinimumPeriod = MinimumPeriod; +} + +pallet_staking_reward_curve::build! { + const REWARD_CURVE: PiecewiseLinear<'static> = curve!( + min_inflation: 0_025_000u64, + max_inflation: 0_100_000, + ideal_stake: 0_500_000, + falloff: 0_050_000, + max_piece_count: 40, + test_precision: 0_005_000, + ); +} + +parameter_types! { + pub const SessionsPerEra: SessionIndex = 3; + pub const BondingDuration: EraIndex = 3; + pub const SlashDeferDuration: EraIndex = 0; + pub const AttestationPeriod: u64 = 100; + pub const RewardCurve: &'static PiecewiseLinear<'static> = &REWARD_CURVE; + pub const MaxNominatorRewardedPerValidator: u32 = 64; + pub const ElectionLookahead: u64 = 0; + pub const StakingUnsignedPriority: u64 = u64::max_value() / 2; +} + +pub struct CurrencyToVoteHandler; + +impl Convert for CurrencyToVoteHandler { + fn convert(x: u128) -> u128 { + x + } +} + +impl Convert for CurrencyToVoteHandler { + fn convert(x: u128) -> u64 { + x.saturated_into() + } +} + +impl staking::Trait for Test { + type RewardRemainder = (); + type CurrencyToVote = CurrencyToVoteHandler; + type Event = TestEvent; + type Currency = Balances; + type Slash = (); + type Reward = (); + type SessionsPerEra = SessionsPerEra; + type BondingDuration = BondingDuration; + type SlashDeferDuration = SlashDeferDuration; + type SlashCancelOrigin = system::EnsureRoot; + type SessionInterface = Self; + type UnixTime = timestamp::Module; + type RewardCurve = RewardCurve; + type MaxNominatorRewardedPerValidator = MaxNominatorRewardedPerValidator; + type NextNewSession = Session; + type ElectionLookahead = ElectionLookahead; + type Call = Call; + type UnsignedPriority = StakingUnsignedPriority; + type MaxIterations = (); +} + +impl offences::Trait for Test { + type Event = TestEvent; + type IdentificationTuple = session::historical::IdentificationTuple; + type OnOffenceHandler = Staking; +} + +impl Trait for Test { + type Event = TestEvent; + type Call = Call; + + type KeyOwnerProofSystem = Historical; + + type KeyOwnerProof = + >::Proof; + + type KeyOwnerIdentification = >::IdentificationTuple; + + type HandleEquivocation = super::EquivocationHandler< + Self::KeyOwnerIdentification, + reporting_keys::ReporterAppCrypto, + Test, + Offences, + >; +} + +pub mod reporting_keys { + use sp_core::crypto::KeyTypeId; + + pub const KEY_TYPE: KeyTypeId = KeyTypeId(*b"test"); + + mod app { + use sp_application_crypto::{app_crypto, ed25519}; + app_crypto!(ed25519, super::KEY_TYPE); + + impl sp_runtime::traits::IdentifyAccount for Public { + type AccountId = u64; + fn into_account(self) -> Self::AccountId { + super::super::Grandpa::grandpa_authorities() + .iter() + .map(|(k, _)| k) + .position(|b| *b == self.0.clone().into()) + .unwrap() as u64 + } + } + } + + pub type ReporterId = app::Public; + + pub struct ReporterAppCrypto; + impl frame_system::offchain::AppCrypto + for ReporterAppCrypto + { + type RuntimeAppPublic = ReporterId; + type GenericSignature = sp_core::ed25519::Signature; + type GenericPublic = sp_core::ed25519::Public; + } +} + +type Extrinsic = TestXt; + +impl system::offchain::CreateSignedTransaction for Test +where + Call: From, +{ + fn create_transaction>( + call: Call, + _public: reporting_keys::ReporterId, + _account: ::AccountId, + nonce: ::Index, + ) -> Option<(Call, ::SignaturePayload)> { + Some((call, (nonce, ()))) + } +} + +impl frame_system::offchain::SigningTypes for Test { + type Public = reporting_keys::ReporterId; + type Signature = sp_core::ed25519::Signature; +} + mod grandpa { pub use crate::Event; } -impl_outer_event!{ - pub enum TestEvent for Test { - system, - grandpa, - } +pub type Balances = pallet_balances::Module; +pub type Historical = pallet_session::historical::Module; +pub type Offences = pallet_offences::Module; +pub type Session = pallet_session::Module; +pub type Staking = pallet_staking::Module; +pub type System = frame_system::Module; +pub type Timestamp = pallet_timestamp::Module; +pub type Grandpa = Module; + +pub fn grandpa_log(log: ConsensusLog) -> DigestItem { + DigestItem::Consensus(GRANDPA_ENGINE_ID, log.encode()) } pub fn to_authorities(vec: Vec<(u64, u64)>) -> AuthorityList { @@ -90,13 +334,164 @@ pub fn to_authorities(vec: Vec<(u64, u64)>) -> AuthorityList { .collect() } -pub fn new_test_ext(authorities: Vec<(u64, u64)>) -> sp_io::TestExternalities { - let mut t = frame_system::GenesisConfig::default().build_storage::().unwrap(); - GenesisConfig { - authorities: to_authorities(authorities), - }.assimilate_storage::(&mut t).unwrap(); +pub fn extract_keyring(id: &AuthorityId) -> Ed25519Keyring { + let mut raw_public = [0; 32]; + raw_public.copy_from_slice(id.as_ref()); + Ed25519Keyring::from_raw_public(raw_public).unwrap() +} + +pub fn new_test_ext(vec: Vec<(u64, u64)>) -> sp_io::TestExternalities { + new_test_ext_raw_authorities(to_authorities(vec)) +} + +pub fn new_test_ext_raw_authorities(authorities: AuthorityList) -> sp_io::TestExternalities { + let mut t = frame_system::GenesisConfig::default() + .build_storage::() + .unwrap(); + + // stashes are the index. + let session_keys: Vec<_> = authorities + .iter() + .enumerate() + .map(|(i, (k, _))| { + ( + i as u64, + i as u64, + TestSessionKeys { + grandpa_authority: AuthorityId::from(k.clone()), + }, + ) + }) + .collect(); + + // controllers are the index + 1000 + let stakers: Vec<_> = (0..authorities.len()) + .map(|i| { + ( + i as u64, + i as u64 + 1000, + 10_000, + staking::StakerStatus::::Validator, + ) + }) + .collect(); + + let balances: Vec<_> = (0..authorities.len()) + .map(|i| (i as u64, 10_000_000)) + .collect(); + + // NOTE: this will initialize the grandpa authorities + // through OneSessionHandler::on_genesis_session + session::GenesisConfig:: { keys: session_keys } + .assimilate_storage(&mut t) + .unwrap(); + + balances::GenesisConfig:: { balances } + .assimilate_storage(&mut t) + .unwrap(); + + let staking_config = staking::GenesisConfig:: { + stakers, + validator_count: 8, + force_era: staking::Forcing::ForceNew, + minimum_validator_count: 0, + invulnerables: vec![], + ..Default::default() + }; + + staking_config.assimilate_storage(&mut t).unwrap(); + t.into() } -pub type System = frame_system::Module; -pub type Grandpa = Module; +pub fn start_session(session_index: SessionIndex) { + let mut parent_hash = System::parent_hash(); + + for i in Session::current_index()..session_index { + Staking::on_finalize(System::block_number()); + System::set_block_number((i + 1).into()); + Timestamp::set_timestamp(System::block_number() * 6000); + + // In order to be able to use `System::parent_hash()` in the tests + // we need to first get it via `System::finalize` and then set it + // the `System::initialize`. However, it is needed to be taken into + // consideration that finalizing will prune some data in `System` + // storage including old values `BlockHash` if that reaches above + // `BlockHashCount` capacity. + if System::block_number() > 1 { + let hdr = System::finalize(); + parent_hash = hdr.hash(); + } + + System::initialize( + &(i as u64 + 1), + &parent_hash, + &Default::default(), + &Default::default(), + Default::default(), + ); + + Session::on_initialize(System::block_number()); + System::on_initialize(System::block_number()); + } + + assert_eq!(Session::current_index(), session_index); +} + +pub fn start_era(era_index: EraIndex) { + start_session((era_index * 3).into()); + assert_eq!(Staking::current_era(), Some(era_index)); +} + +pub fn initialize_block(number: u64, parent_hash: H256) { + System::initialize( + &number, + &parent_hash, + &Default::default(), + &Default::default(), + Default::default(), + ); +} + +pub fn report_equivocation( + equivocation_proof: sp_finality_grandpa::EquivocationProof, + key_owner_proof: sp_session::MembershipProof, +) -> Result, TransactionValidityError> { + let inner = GrandpaCall::report_equivocation(equivocation_proof, key_owner_proof); + let call = Call::Grandpa(inner.clone()); + + ValidateEquivocationReport::::new().validate(&0, &call, &DispatchInfo::default(), 0)?; + + Ok(inner) +} + +pub fn generate_equivocation_proof( + set_id: SetId, + vote1: (RoundNumber, H256, u64, &Ed25519Keyring), + vote2: (RoundNumber, H256, u64, &Ed25519Keyring), +) -> sp_finality_grandpa::EquivocationProof { + let signed_prevote = |round, hash, number, keyring: &Ed25519Keyring| { + let prevote = finality_grandpa::Prevote { + target_hash: hash, + target_number: number, + }; + + let prevote_msg = finality_grandpa::Message::Prevote(prevote.clone()); + let payload = sp_finality_grandpa::localized_payload(round, set_id, &prevote_msg); + let signed = keyring.sign(&payload).into(); + (prevote, signed) + }; + + let (prevote1, signed1) = signed_prevote(vote1.0, vote1.1, vote1.2, vote1.3); + let (prevote2, signed2) = signed_prevote(vote2.0, vote2.1, vote2.2, vote2.3); + + sp_finality_grandpa::EquivocationProof::new( + set_id, + sp_finality_grandpa::Equivocation::Prevote(finality_grandpa::Equivocation { + round_number: vote1.0, + identity: vote1.3.public().into(), + first: (prevote1, signed1), + second: (prevote2, signed2), + }), + ) +} diff --git a/frame/grandpa/src/tests.rs b/frame/grandpa/src/tests.rs index b583c31968d89..898c67583565f 100644 --- a/frame/grandpa/src/tests.rs +++ b/frame/grandpa/src/tests.rs @@ -18,23 +18,18 @@ #![cfg(test)] -use sp_runtime::{testing::{H256, Digest}, traits::Header}; -use frame_support::traits::OnFinalize; +use super::*; use crate::mock::*; -use frame_system::{EventRecord, Phase}; use codec::{Decode, Encode}; use fg_primitives::ScheduledChange; -use super::*; - -fn initialize_block(number: u64, parent_hash: H256) { - System::initialize( - &number, - &parent_hash, - &Default::default(), - &Default::default(), - Default::default(), - ); -} +use frame_support::{ + assert_err, assert_ok, + traits::{Currency, OnFinalize}, +}; +use frame_system::{EventRecord, Phase}; +use sp_core::H256; +use sp_keyring::Ed25519Keyring; +use sp_runtime::{testing::Digest, traits::Header}; #[test] fn authorities_change_logged() { @@ -319,3 +314,348 @@ fn time_slot_have_sane_ord() { ]; assert!(FIXTURE.windows(2).all(|f| f[0] < f[1])); } + +fn test_authorities() -> AuthorityList { + let authorities = vec![ + Ed25519Keyring::Alice, + Ed25519Keyring::Bob, + Ed25519Keyring::Charlie, + ]; + + authorities + .into_iter() + .map(|id| (id.public().into(), 1u64)) + .collect() +} + +#[test] +fn report_equivocation_current_set_works() { + let authorities = test_authorities(); + + new_test_ext_raw_authorities(authorities).execute_with(|| { + assert_eq!(Staking::current_era(), Some(0)); + assert_eq!(Session::current_index(), 0); + + start_era(1); + + let authorities = Grandpa::grandpa_authorities(); + + // make sure that all authorities have the same balance + for i in 0..authorities.len() { + assert_eq!(Balances::total_balance(&(i as u64)), 10_000_000); + assert_eq!(Staking::slashable_balance_of(&(i as u64)), 10_000); + + assert_eq!( + Staking::eras_stakers(1, i as u64), + pallet_staking::Exposure { + total: 10_000, + own: 10_000, + others: vec![], + }, + ); + } + + let equivocation_authority_index = 0; + let equivocation_key = &authorities[equivocation_authority_index].0; + let equivocation_keyring = extract_keyring(equivocation_key); + + let set_id = Grandpa::current_set_id(); + + // generate an equivocation proof, with two votes in the same round for + // different block hashes signed by the same key + let equivocation_proof = generate_equivocation_proof( + set_id, + (1, H256::random(), 10, &equivocation_keyring), + (1, H256::random(), 10, &equivocation_keyring), + ); + + // create the key ownership proof + let key_owner_proof = + Historical::prove((sp_finality_grandpa::KEY_TYPE, &equivocation_key)).unwrap(); + + // report the equivocation and the tx should be dispatched successfully + let inner = report_equivocation(equivocation_proof, key_owner_proof).unwrap(); + assert_ok!(Grandpa::dispatch(inner, Origin::signed(1))); + + start_era(2); + + // check that the balance of 0-th validator is slashed 100%. + assert_eq!(Balances::total_balance(&0), 10_000_000 - 10_000); + assert_eq!(Staking::slashable_balance_of(&0), 0); + + assert_eq!( + Staking::eras_stakers(2, 0), + pallet_staking::Exposure { + total: 0, + own: 0, + others: vec![], + }, + ); + + // check that the balances of all other validators are left intact. + for i in 1..authorities.len() { + assert_eq!(Balances::total_balance(&(i as u64)), 10_000_000); + assert_eq!(Staking::slashable_balance_of(&(i as u64)), 10_000); + + assert_eq!( + Staking::eras_stakers(2, i as u64), + pallet_staking::Exposure { + total: 10_000, + own: 10_000, + others: vec![], + }, + ); + } + }); +} + +#[test] +fn report_equivocation_old_set_works() { + let authorities = test_authorities(); + + new_test_ext_raw_authorities(authorities).execute_with(|| { + start_era(1); + + let authorities = Grandpa::grandpa_authorities(); + + let equivocation_authority_index = 0; + let equivocation_key = &authorities[equivocation_authority_index].0; + + // create the key ownership proof in the "old" set + let key_owner_proof = + Historical::prove((sp_finality_grandpa::KEY_TYPE, &equivocation_key)).unwrap(); + + start_era(2); + + // make sure that all authorities have the same balance + for i in 0..authorities.len() { + assert_eq!(Balances::total_balance(&(i as u64)), 10_000_000); + assert_eq!(Staking::slashable_balance_of(&(i as u64)), 10_000); + + assert_eq!( + Staking::eras_stakers(2, i as u64), + pallet_staking::Exposure { + total: 10_000, + own: 10_000, + others: vec![], + }, + ); + } + + let equivocation_keyring = extract_keyring(equivocation_key); + + let set_id = Grandpa::current_set_id(); + + // generate an equivocation proof for the old set, + let equivocation_proof = generate_equivocation_proof( + set_id - 1, + (1, H256::random(), 10, &equivocation_keyring), + (1, H256::random(), 10, &equivocation_keyring), + ); + + // report the equivocation using the key ownership proof generated on + // the old set, the tx should be dispatched successfully + let inner = report_equivocation(equivocation_proof, key_owner_proof).unwrap(); + assert_ok!(Grandpa::dispatch(inner, Origin::signed(1))); + + start_era(3); + + // check that the balance of 0-th validator is slashed 100%. + assert_eq!(Balances::total_balance(&0), 10_000_000 - 10_000); + assert_eq!(Staking::slashable_balance_of(&0), 0); + + assert_eq!( + Staking::eras_stakers(3, 0), + pallet_staking::Exposure { + total: 0, + own: 0, + others: vec![], + }, + ); + + // check that the balances of all other validators are left intact. + for i in 1..authorities.len() { + assert_eq!(Balances::total_balance(&(i as u64)), 10_000_000); + assert_eq!(Staking::slashable_balance_of(&(i as u64)), 10_000); + + assert_eq!( + Staking::eras_stakers(3, i as u64), + pallet_staking::Exposure { + total: 10_000, + own: 10_000, + others: vec![], + }, + ); + } + }); +} + +#[test] +fn report_equivocation_invalid_set_id() { + let authorities = test_authorities(); + + new_test_ext_raw_authorities(authorities).execute_with(|| { + start_era(1); + + let authorities = Grandpa::grandpa_authorities(); + + let equivocation_authority_index = 0; + let equivocation_key = &authorities[equivocation_authority_index].0; + let equivocation_keyring = extract_keyring(equivocation_key); + + let key_owner_proof = + Historical::prove((sp_finality_grandpa::KEY_TYPE, &equivocation_key)).unwrap(); + + let set_id = Grandpa::current_set_id(); + + // generate an equivocation for a future set + let equivocation_proof = generate_equivocation_proof( + set_id + 1, + (1, H256::random(), 10, &equivocation_keyring), + (1, H256::random(), 10, &equivocation_keyring), + ); + + // it should be filtered by the signed extension validation + assert_err!( + report_equivocation(equivocation_proof, key_owner_proof), + equivocation::ReportEquivocationValidityError::InvalidSetId, + ); + }); +} + +#[test] +fn report_equivocation_invalid_session() { + let authorities = test_authorities(); + + new_test_ext_raw_authorities(authorities).execute_with(|| { + start_era(1); + + let authorities = Grandpa::grandpa_authorities(); + + let equivocation_authority_index = 0; + let equivocation_key = &authorities[equivocation_authority_index].0; + let equivocation_keyring = extract_keyring(equivocation_key); + + // generate a key ownership proof at set id = 1 + let key_owner_proof = + Historical::prove((sp_finality_grandpa::KEY_TYPE, &equivocation_key)).unwrap(); + + start_era(2); + + let set_id = Grandpa::current_set_id(); + + // generate an equivocation proof at set id = 2 + let equivocation_proof = generate_equivocation_proof( + set_id, + (1, H256::random(), 10, &equivocation_keyring), + (1, H256::random(), 10, &equivocation_keyring), + ); + + // report an equivocation for the current set using an key ownership + // proof from the previous set, the session should be invalid. + assert_err!( + report_equivocation(equivocation_proof, key_owner_proof), + equivocation::ReportEquivocationValidityError::InvalidSession, + ); + }); +} + +#[test] +fn report_equivocation_invalid_key_owner_proof() { + let authorities = test_authorities(); + + new_test_ext_raw_authorities(authorities).execute_with(|| { + start_era(1); + let authorities = Grandpa::grandpa_authorities(); + + let invalid_owner_authority_index = 1; + let invalid_owner_key = &authorities[invalid_owner_authority_index].0; + + // generate a key ownership proof for the authority at index 1 + let invalid_key_owner_proof = + Historical::prove((sp_finality_grandpa::KEY_TYPE, &invalid_owner_key)).unwrap(); + + let equivocation_authority_index = 0; + let equivocation_key = &authorities[equivocation_authority_index].0; + let equivocation_keyring = extract_keyring(equivocation_key); + + let set_id = Grandpa::current_set_id(); + + // generate an equivocation proof for the authority at index 0 + let equivocation_proof = generate_equivocation_proof( + set_id, + (1, H256::random(), 10, &equivocation_keyring), + (1, H256::random(), 10, &equivocation_keyring), + ); + + // we need to start a new era otherwise the key ownership proof won't be + // checked since the authorities are part of the current session + start_era(2); + + // report an equivocation for the current set using a key ownership + // proof for a different key than the one in the equivocation proof. + assert_err!( + report_equivocation(equivocation_proof, invalid_key_owner_proof), + equivocation::ReportEquivocationValidityError::InvalidKeyOwnershipProof, + ); + }); +} + +#[test] +fn report_equivocation_invalid_equivocation_proof() { + let authorities = test_authorities(); + + new_test_ext_raw_authorities(authorities).execute_with(|| { + start_era(1); + + let authorities = Grandpa::grandpa_authorities(); + + let equivocation_authority_index = 0; + let equivocation_key = &authorities[equivocation_authority_index].0; + let equivocation_keyring = extract_keyring(equivocation_key); + + // generate a key ownership proof at set id = 1 + let key_owner_proof = + Historical::prove((sp_finality_grandpa::KEY_TYPE, &equivocation_key)).unwrap(); + + let set_id = Grandpa::current_set_id(); + + let assert_invalid_equivocation_proof = |equivocation_proof| { + assert_err!( + report_equivocation(equivocation_proof, key_owner_proof.clone()), + equivocation::ReportEquivocationValidityError::InvalidEquivocationProof, + ); + }; + + start_era(2); + + // both votes target the same block number and hash, + // there is no equivocation. + assert_invalid_equivocation_proof(generate_equivocation_proof( + set_id, + (1, H256::zero(), 10, &equivocation_keyring), + (1, H256::zero(), 10, &equivocation_keyring), + )); + + // votes targetting different rounds, there is no equivocation. + assert_invalid_equivocation_proof(generate_equivocation_proof( + set_id, + (1, H256::random(), 10, &equivocation_keyring), + (2, H256::random(), 10, &equivocation_keyring), + )); + + // votes signed with different authority keys + assert_invalid_equivocation_proof(generate_equivocation_proof( + set_id, + (1, H256::random(), 10, &equivocation_keyring), + (1, H256::random(), 10, &Ed25519Keyring::Charlie), + )); + + // votes signed with a key that isn't part of the authority set + assert_invalid_equivocation_proof(generate_equivocation_proof( + set_id, + (1, H256::random(), 10, &equivocation_keyring), + (1, H256::random(), 10, &Ed25519Keyring::Dave), + )); + }); +} diff --git a/frame/session/Cargo.toml b/frame/session/Cargo.toml index b3ca1ad596cca..9237f0a16f64a 100644 --- a/frame/session/Cargo.toml +++ b/frame/session/Cargo.toml @@ -16,6 +16,7 @@ serde = { version = "1.0.101", optional = true } codec = { package = "parity-scale-codec", version = "1.3.0", default-features = false, features = ["derive"] } sp-std = { version = "2.0.0-dev", default-features = false, path = "../../primitives/std" } sp-runtime = { version = "2.0.0-dev", default-features = false, path = "../../primitives/runtime" } +sp-session = { version = "2.0.0-dev", default-features = false, path = "../../primitives/session" } sp-staking = { version = "2.0.0-dev", default-features = false, path = "../../primitives/staking" } frame-support = { version = "2.0.0-dev", default-features = false, path = "../support" } frame-system = { version = "2.0.0-dev", default-features = false, path = "../system" } @@ -38,6 +39,7 @@ std = [ "sp-std/std", "frame-support/std", "sp-runtime/std", + "sp-session/std", "sp-staking/std", "pallet-timestamp/std", "sp-trie/std", diff --git a/frame/session/src/historical.rs b/frame/session/src/historical.rs index f9990dd1e8a7a..d9711ecf9d3bd 100644 --- a/frame/session/src/historical.rs +++ b/frame/session/src/historical.rs @@ -27,16 +27,15 @@ use sp_std::prelude::*; use codec::{Encode, Decode}; -use sp_runtime::{KeyTypeId, RuntimeDebug}; +use sp_runtime::KeyTypeId; use sp_runtime::traits::{Convert, OpaqueKeys}; +use sp_session::{MembershipProof, ValidatorCount}; use frame_support::{decl_module, decl_storage}; use frame_support::{Parameter, print}; use sp_trie::{MemoryDB, Trie, TrieMut, Recorder, EMPTY_PREFIX}; use sp_trie::trie_types::{TrieDBMut, TrieDB}; use super::{SessionIndex, Module as SessionModule}; -type ValidatorCount = u32; - /// Trait necessary for the historical module. pub trait Trait: super::Trait { /// Full identification of the validator. @@ -126,7 +125,7 @@ impl crate::SessionManager for NoteHistoricalRoot::generate_for(new_validators) { Ok(trie) => >::insert(new_index, &(trie.root, count)), Err(reason) => { @@ -253,54 +252,58 @@ impl ProvingTrie { } -/// Proof of ownership of a specific key. -#[derive(Encode, Decode, Clone, Eq, PartialEq, RuntimeDebug)] -pub struct Proof { - session: SessionIndex, - trie_nodes: Vec>, -} - -impl Proof { - /// Returns a session this proof was generated for. - pub fn session(&self) -> SessionIndex { - self.session - } -} - impl> frame_support::traits::KeyOwnerProofSystem<(KeyTypeId, D)> for Module { - type Proof = Proof; + type Proof = MembershipProof; type IdentificationTuple = IdentificationTuple; fn prove(key: (KeyTypeId, D)) -> Option { let session = >::current_index(); - let validators = >::validators().into_iter() + let validators = >::validators() + .into_iter() .filter_map(|validator| { T::FullIdentificationOf::convert(validator.clone()) .map(|full_id| (validator, full_id)) - }); + }) + .collect::>(); + + let count = validators.len() as ValidatorCount; + let trie = ProvingTrie::::generate_for(validators).ok()?; let (id, data) = key; - - trie.prove(id, data.as_ref()).map(|trie_nodes| Proof { - session, - trie_nodes, - }) + trie.prove(id, data.as_ref()) + .map(|trie_nodes| MembershipProof { + session, + trie_nodes, + validator_count: count, + }) } - fn check_proof(key: (KeyTypeId, D), proof: Proof) -> Option> { + fn check_proof(key: (KeyTypeId, D), proof: Self::Proof) -> Option> { let (id, data) = key; if proof.session == >::current_index() { - >::key_owner(id, data.as_ref()).and_then(|owner| - T::FullIdentificationOf::convert(owner.clone()).map(move |id| (owner, id)) - ) + >::key_owner(id, data.as_ref()).and_then(|owner| { + T::FullIdentificationOf::convert(owner.clone()).and_then(move |id| { + let count = >::validators().len() as ValidatorCount; + + if count != proof.validator_count { + return None; + } + + Some((owner, id)) + }) + }) } else { - let (root, _) = >::get(&proof.session)?; - let trie = ProvingTrie::::from_nodes(root, &proof.trie_nodes); + let (root, count) = >::get(&proof.session)?; + + if count != proof.validator_count { + return None; + } + let trie = ProvingTrie::::from_nodes(root, &proof.trie_nodes); trie.query(id, data.as_ref()) } } diff --git a/frame/support/src/lib.rs b/frame/support/src/lib.rs index 5222e506d5800..6bad5985abcb6 100644 --- a/frame/support/src/lib.rs +++ b/frame/support/src/lib.rs @@ -33,6 +33,7 @@ pub use serde; pub use sp_std; #[doc(hidden)] pub use codec; +use codec::{Decode, Encode}; #[cfg(feature = "std")] #[doc(hidden)] pub use once_cell; @@ -237,7 +238,7 @@ macro_rules! assert_ok { /// The void type - it cannot exist. // Oh rust, you crack me up... -#[derive(Clone, Eq, PartialEq, RuntimeDebug)] +#[derive(Clone, Decode, Encode, Eq, PartialEq, RuntimeDebug)] pub enum Void {} #[cfg(feature = "std")] diff --git a/frame/support/src/traits.rs b/frame/support/src/traits.rs index 3d5a3403a9724..eaf94f44f86f5 100644 --- a/frame/support/src/traits.rs +++ b/frame/support/src/traits.rs @@ -288,6 +288,21 @@ pub trait KeyOwnerProofSystem { fn check_proof(key: Key, proof: Self::Proof) -> Option; } +impl KeyOwnerProofSystem for () { + // The proof and identification tuples is any bottom type to guarantee that the methods of this + // implementation can never be called or return anything other than `None`. + type Proof = crate::Void; + type IdentificationTuple = crate::Void; + + fn prove(_key: Key) -> Option { + None + } + + fn check_proof(_key: Key, _proof: Self::Proof) -> Option { + None + } +} + /// Handler for when some currency "account" decreased in balance for /// some reason. /// diff --git a/primitives/core/src/lib.rs b/primitives/core/src/lib.rs index 8d5ad7daaec83..9da56018e95d7 100644 --- a/primitives/core/src/lib.rs +++ b/primitives/core/src/lib.rs @@ -110,8 +110,11 @@ impl ExecutionContext { match self { Importing | Syncing | BlockConstruction => offchain::Capabilities::none(), - // Enable keystore by default for offchain calls. CC @bkchr - OffchainCall(None) => [offchain::Capability::Keystore][..].into(), + // Enable keystore and transaction pool by default for offchain calls. + OffchainCall(None) => [ + offchain::Capability::Keystore, + offchain::Capability::TransactionPool, + ][..].into(), OffchainCall(Some((_, capabilities))) => *capabilities, } } diff --git a/primitives/finality-grandpa/Cargo.toml b/primitives/finality-grandpa/Cargo.toml index b5ce970c0d82c..378f715eccae9 100644 --- a/primitives/finality-grandpa/Cargo.toml +++ b/primitives/finality-grandpa/Cargo.toml @@ -16,18 +16,24 @@ targets = ["x86_64-unknown-linux-gnu"] [dependencies] sp-application-crypto = { version = "2.0.0-dev", default-features = false, path = "../application-crypto" } codec = { package = "parity-scale-codec", version = "1.3.0", default-features = false, features = ["derive"] } -sp-std = { version = "2.0.0-dev", default-features = false, path = "../std" } +grandpa = { package = "finality-grandpa", version = "0.12.2", default-features = false, features = ["derive-codec"] } +log = { version = "0.4.8", optional = true } serde = { version = "1.0.101", optional = true, features = ["derive"] } sp-api = { version = "2.0.0-dev", default-features = false, path = "../api" } +sp-core = { version = "2.0.0-dev", default-features = false, path = "../core" } sp-runtime = { version = "2.0.0-dev", default-features = false, path = "../runtime" } +sp-std = { version = "2.0.0-dev", default-features = false, path = "../std" } [features] default = ["std"] std = [ "sp-application-crypto/std", "codec/std", - "sp-std/std", + "grandpa/std", + "log", "serde", "sp-api/std", + "sp-core/std", "sp-runtime/std", + "sp-std/std", ] diff --git a/primitives/finality-grandpa/src/lib.rs b/primitives/finality-grandpa/src/lib.rs index 9dcb1c2363c15..a84ce57c8d1f6 100644 --- a/primitives/finality-grandpa/src/lib.rs +++ b/primitives/finality-grandpa/src/lib.rs @@ -23,11 +23,18 @@ extern crate alloc; #[cfg(feature = "std")] use serde::Serialize; + use codec::{Encode, Decode, Input, Codec}; -use sp_runtime::{ConsensusEngineId, RuntimeDebug}; +use sp_runtime::{ConsensusEngineId, RuntimeDebug, traits::NumberFor}; use sp_std::borrow::Cow; use sp_std::vec::Vec; +#[cfg(feature = "std")] +use log::debug; + +/// Key type for GRANDPA module. +pub const KEY_TYPE: sp_core::crypto::KeyTypeId = sp_application_crypto::key_types::GRANDPA; + mod app { use sp_application_crypto::{app_crypto, key_types::GRANDPA, ed25519}; app_crypto!(ed25519, GRANDPA); @@ -157,6 +164,242 @@ impl ConsensusLog { } } +/// Proof of voter misbehavior on a given set id. Misbehavior/equivocation in +/// GRANDPA happens when a voter votes on the same round (either at prevote or +/// precommit stage) for different blocks. Proving is achieved by collecting the +/// signed messages of conflicting votes. +#[derive(Clone, Debug, Decode, Encode, PartialEq)] +pub struct EquivocationProof { + set_id: SetId, + equivocation: Equivocation, +} + +impl EquivocationProof { + /// Create a new `EquivocationProof` for the given set id and using the + /// given equivocation as proof. + pub fn new(set_id: SetId, equivocation: Equivocation) -> Self { + EquivocationProof { + set_id, + equivocation, + } + } + + /// Returns the set id at which the equivocation occurred. + pub fn set_id(&self) -> SetId { + self.set_id + } + + /// Returns the round number at which the equivocation occurred. + pub fn round(&self) -> RoundNumber { + match self.equivocation { + Equivocation::Prevote(ref equivocation) => equivocation.round_number, + Equivocation::Precommit(ref equivocation) => equivocation.round_number, + } + } + + /// Returns the authority id of the equivocator. + pub fn offender(&self) -> &AuthorityId { + self.equivocation.offender() + } +} + +/// Wrapper object for GRANDPA equivocation proofs, useful for unifying prevote +/// and precommit equivocations under a common type. +#[derive(Clone, Debug, Decode, Encode, PartialEq)] +pub enum Equivocation { + /// Proof of equivocation at prevote stage. + Prevote(grandpa::Equivocation, AuthoritySignature>), + /// Proof of equivocation at precommit stage. + Precommit(grandpa::Equivocation, AuthoritySignature>), +} + +impl From, AuthoritySignature>> + for Equivocation +{ + fn from( + equivocation: grandpa::Equivocation< + AuthorityId, + grandpa::Prevote, + AuthoritySignature, + >, + ) -> Self { + Equivocation::Prevote(equivocation) + } +} + +impl From, AuthoritySignature>> + for Equivocation +{ + fn from( + equivocation: grandpa::Equivocation< + AuthorityId, + grandpa::Precommit, + AuthoritySignature, + >, + ) -> Self { + Equivocation::Precommit(equivocation) + } +} + +impl Equivocation { + /// Returns the authority id of the equivocator. + pub fn offender(&self) -> &AuthorityId { + match self { + Equivocation::Prevote(ref equivocation) => &equivocation.identity, + Equivocation::Precommit(ref equivocation) => &equivocation.identity, + } + } +} + +/// Verifies the equivocation proof by making sure that both votes target +/// different blocks and that its signatures are valid. +pub fn check_equivocation_proof(report: EquivocationProof) -> Result<(), ()> +where + H: Clone + Encode + PartialEq, + N: Clone + Encode + PartialEq, +{ + // NOTE: the bare `Prevote` and `Precommit` types don't share any trait, + // this is implemented as a macro to avoid duplication. + macro_rules! check { + ( $equivocation:expr, $message:expr ) => { + // if both votes have the same target the equivocation is invalid. + if $equivocation.first.0.target_hash == $equivocation.second.0.target_hash && + $equivocation.first.0.target_number == $equivocation.second.0.target_number + { + return Err(()); + } + + // check signatures on both votes are valid + check_message_signature( + &$message($equivocation.first.0), + &$equivocation.identity, + &$equivocation.first.1, + $equivocation.round_number, + report.set_id, + )?; + + check_message_signature( + &$message($equivocation.second.0), + &$equivocation.identity, + &$equivocation.second.1, + $equivocation.round_number, + report.set_id, + )?; + + return Ok(()); + }; + } + + match report.equivocation { + Equivocation::Prevote(equivocation) => { + check!(equivocation, grandpa::Message::Prevote); + } + Equivocation::Precommit(equivocation) => { + check!(equivocation, grandpa::Message::Precommit); + } + } +} + +/// Encode round message localized to a given round and set id. +pub fn localized_payload(round: RoundNumber, set_id: SetId, message: &E) -> Vec { + let mut buf = Vec::new(); + localized_payload_with_buffer(round, set_id, message, &mut buf); + buf +} + +/// Encode round message localized to a given round and set id using the given +/// buffer. The given buffer will be cleared and the resulting encoded payload +/// will always be written to the start of the buffer. +pub fn localized_payload_with_buffer( + round: RoundNumber, + set_id: SetId, + message: &E, + buf: &mut Vec, +) { + buf.clear(); + (message, round, set_id).encode_to(buf) +} + +/// Check a message signature by encoding the message as a localized payload and +/// verifying the provided signature using the expected authority id. +pub fn check_message_signature( + message: &grandpa::Message, + id: &AuthorityId, + signature: &AuthoritySignature, + round: RoundNumber, + set_id: SetId, +) -> Result<(), ()> +where + H: Encode, + N: Encode, +{ + check_message_signature_with_buffer(message, id, signature, round, set_id, &mut Vec::new()) +} + +/// Check a message signature by encoding the message as a localized payload and +/// verifying the provided signature using the expected authority id. +/// The encoding necessary to verify the signature will be done using the given +/// buffer, the original content of the buffer will be cleared. +pub fn check_message_signature_with_buffer( + message: &grandpa::Message, + id: &AuthorityId, + signature: &AuthoritySignature, + round: RoundNumber, + set_id: SetId, + buf: &mut Vec, +) -> Result<(), ()> +where + H: Encode, + N: Encode, +{ + localized_payload_with_buffer(round, set_id, message, buf); + + #[cfg(not(feature = "std"))] + let verify = || { + use sp_application_crypto::RuntimeAppPublic; + id.verify(&buf, signature) + }; + + #[cfg(feature = "std")] + let verify = || { + use sp_application_crypto::Pair; + AuthorityPair::verify(signature, &buf, &id) + }; + + if verify() { + Ok(()) + } else { + #[cfg(feature = "std")] + debug!(target: "afg", "Bad signature on message from {:?}", id); + + Err(()) + } +} + +/// Localizes the message to the given set and round and signs the payload. +#[cfg(feature = "std")] +pub fn sign_message( + message: grandpa::Message, + pair: &AuthorityPair, + round: RoundNumber, + set_id: SetId, +) -> grandpa::SignedMessage +where + H: Encode, + N: Encode, +{ + use sp_core::Pair; + + let encoded = localized_payload(round, set_id, &message); + let signature = pair.sign(&encoded[..]); + + grandpa::SignedMessage { + message, + signature, + id: pair.public(), + } +} + /// WASM function call to check for pending changes. pub const PENDING_CHANGE_CALL: &str = "grandpa_pending_change"; /// WASM function call to get current GRANDPA authorities. @@ -211,6 +454,29 @@ impl<'a> Decode for VersionedAuthorityList<'a> { } } +/// An opaque type used to represent the key ownership proof at the runtime API +/// boundary. The inner value is an encoded representation of the actual key +/// ownership proof which will be parameterized when defining the runtime. At +/// the runtime API boundary this type is unknown and as such we keep this +/// opaque representation, implementors of the runtime API will have to make +/// sure that all usages of `OpaqueKeyOwnershipProof` refer to the same type. +#[derive(Decode, Encode, PartialEq)] +pub struct OpaqueKeyOwnershipProof(Vec); + +impl OpaqueKeyOwnershipProof { + /// Create a new `OpaqueKeyOwnershipProof` using the given encoded + /// representation. + pub fn new(inner: Vec) -> OpaqueKeyOwnershipProof { + OpaqueKeyOwnershipProof(inner) + } + + /// Try to decode this `OpaqueKeyOwnershipProof` into the given concrete key + /// ownership proof type. + pub fn decode(self) -> Option { + codec::Decode::decode(&mut &self.0[..]).ok() + } +} + sp_api::decl_runtime_apis! { /// APIs for integrating the GRANDPA finality gadget into runtimes. /// This should be implemented on the runtime side. @@ -230,5 +496,32 @@ sp_api::decl_runtime_apis! { /// used to finalize descendants of this block (B+1, B+2, ...). The block B itself /// is finalized by the authorities from block B-1. fn grandpa_authorities() -> AuthorityList; + + /// Submits an extrinsic to report an equivocation. The caller must + /// provide the equivocation proof and a key ownership proof (should be + /// obtained using `generate_key_ownership_proof`). This method will + /// sign the extrinsic with any reporting keys available in the keystore + /// and will push the transaction to the pool. + /// Only useful in an offchain context. + fn submit_report_equivocation_extrinsic( + equivocation_proof: EquivocationProof>, + key_owner_proof: OpaqueKeyOwnershipProof, + ) -> Option<()>; + + /// Generates a proof of key ownership for the given authority in the + /// given set. An example usage of this module is coupled with the + /// session historical module to prove that a given authority key is + /// tied to a given staking identity during a specific session. Proofs + /// of key ownership are necessary for submitting equivocation reports. + /// NOTE: even though the API takes a `set_id` as parameter the current + /// implementations ignore this parameter and instead rely on this + /// method being called at the correct block height, i.e. any point at + /// which the given set id is live on-chain. Future implementations will + /// instead use indexed data through an offchain worker, not requiring + /// older states to be available. + fn generate_key_ownership_proof( + set_id: SetId, + authority_id: AuthorityId, + ) -> Option; } } diff --git a/primitives/session/Cargo.toml b/primitives/session/Cargo.toml index 6d210b341f43b..df20a3832cfe0 100644 --- a/primitives/session/Cargo.toml +++ b/primitives/session/Cargo.toml @@ -12,11 +12,20 @@ description = "Primitives for sessions" targets = ["x86_64-unknown-linux-gnu"] [dependencies] +codec = { package = "parity-scale-codec", version = "1.3.0", default-features = false, features = ["derive"] } sp-api = { version = "2.0.0-dev", default-features = false, path = "../api" } -sp-std = { version = "2.0.0-dev", default-features = false, path = "../std" } sp-core = { version = "2.0.0-dev", default-features = false, path = "../core" } +sp-std = { version = "2.0.0-dev", default-features = false, path = "../std" } +sp-staking = { version = "2.0.0-dev", default-features = false, path = "../staking" } sp-runtime = { version = "2.0.0-dev", optional = true, path = "../runtime" } [features] default = [ "std" ] -std = [ "sp-api/std", "sp-std/std", "sp-runtime", "sp-core/std" ] +std = [ + "codec/std", + "sp-api/std", + "sp-core/std", + "sp-std/std", + "sp-staking/std", + "sp-runtime/std", +] diff --git a/primitives/session/src/lib.rs b/primitives/session/src/lib.rs index 8e2a68d050983..720dfbfcaddfb 100644 --- a/primitives/session/src/lib.rs +++ b/primitives/session/src/lib.rs @@ -18,14 +18,17 @@ #![cfg_attr(not(feature = "std"), no_std)] -use sp_std::vec::Vec; +use codec::{Encode, Decode}; #[cfg(feature = "std")] use sp_runtime::{generic::BlockId, traits::Block as BlockT}; #[cfg(feature = "std")] use sp_api::ProvideRuntimeApi; +use sp_core::RuntimeDebug; use sp_core::crypto::KeyTypeId; +use sp_staking::SessionIndex; +use sp_std::vec::Vec; sp_api::decl_runtime_apis! { /// Session keys runtime api. @@ -46,6 +49,32 @@ sp_api::decl_runtime_apis! { } } +/// Number of validators in a given session. +pub type ValidatorCount = u32; + +/// Proof of membership of a specific key in a given session. +#[derive(Encode, Decode, Clone, Eq, PartialEq, Default, RuntimeDebug)] +pub struct MembershipProof { + /// The session index on which the specific key is a member. + pub session: SessionIndex, + /// Trie nodes of a merkle proof of session membership. + pub trie_nodes: Vec>, + /// The validator count of the session on which the specific key is a member. + pub validator_count: ValidatorCount, +} + +impl MembershipProof { + /// Returns a session this proof was generated for. + pub fn session(&self) -> SessionIndex { + self.session + } + + /// Returns the validator count of the session this proof was generated for. + pub fn validator_count(&self) -> ValidatorCount { + self.validator_count + } +} + /// Generate the initial session keys with the given seeds, at the given block and store them in /// the client's keystore. #[cfg(feature = "std")] diff --git a/test-utils/runtime/Cargo.toml b/test-utils/runtime/Cargo.toml index f4582d0b70918..00aa157529127 100644 --- a/test-utils/runtime/Cargo.toml +++ b/test-utils/runtime/Cargo.toml @@ -36,6 +36,7 @@ pallet-babe = { version = "2.0.0-dev", default-features = false, path = "../../f frame-system = { version = "2.0.0-dev", default-features = false, path = "../../frame/system" } frame-system-rpc-runtime-api = { version = "2.0.0-dev", default-features = false, path = "../../frame/system/rpc/runtime-api" } pallet-timestamp = { version = "2.0.0-dev", default-features = false, path = "../../frame/timestamp" } +sp-finality-grandpa = { version = "2.0.0-dev", default-features = false, path = "../../primitives/finality-grandpa" } sp-trie = { version = "2.0.0-dev", default-features = false, path = "../../primitives/trie" } sp-transaction-pool = { version = "2.0.0-dev", default-features = false, path = "../../primitives/transaction-pool" } trie-db = { version = "0.20.1", default-features = false } @@ -88,6 +89,7 @@ std = [ "frame-system/std", "pallet-timestamp/std", "sc-service", + "sp-finality-grandpa/std", "sp-trie/std", "sp-transaction-pool/std", "trie-db/std", diff --git a/test-utils/runtime/src/lib.rs b/test-utils/runtime/src/lib.rs index 745eb8f745821..1003a0c593d5d 100644 --- a/test-utils/runtime/src/lib.rs +++ b/test-utils/runtime/src/lib.rs @@ -33,14 +33,15 @@ use sp_trie::trie_types::{TrieDB, TrieDBMut}; use sp_api::{decl_runtime_apis, impl_runtime_apis}; use sp_runtime::{ - ApplyExtrinsicResult, create_runtime_str, Perbill, impl_opaque_keys, + create_runtime_str, impl_opaque_keys, + ApplyExtrinsicResult, Perbill, transaction_validity::{ TransactionValidity, ValidTransaction, TransactionValidityError, InvalidTransaction, TransactionSource, }, traits::{ BlindCheckable, BlakeTwo256, Block as BlockT, Extrinsic as ExtrinsicT, - GetNodeBlockType, GetRuntimeBlockType, Verify, IdentityLookup, + GetNodeBlockType, GetRuntimeBlockType, NumberFor, Verify, IdentityLookup, }, }; use sp_version::RuntimeVersion; @@ -671,6 +672,29 @@ cfg_if! { } } + impl sp_finality_grandpa::GrandpaApi for Runtime { + fn grandpa_authorities() -> sp_finality_grandpa::AuthorityList { + Vec::new() + } + + fn submit_report_equivocation_extrinsic( + _equivocation_proof: sp_finality_grandpa::EquivocationProof< + ::Hash, + NumberFor, + >, + _key_owner_proof: sp_finality_grandpa::OpaqueKeyOwnershipProof, + ) -> Option<()> { + None + } + + fn generate_key_ownership_proof( + _set_id: sp_finality_grandpa::SetId, + _authority_id: sp_finality_grandpa::AuthorityId, + ) -> Option { + None + } + } + impl frame_system_rpc_runtime_api::AccountNonceApi for Runtime { fn account_nonce(_account: AccountId) -> Index { 0