mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-04-26 05:17:58 +00:00
grandpa: report equivocations (#3868)
* session: runtime api for generating session membership proofs * grandpa: add runtime api for creating equivocation report txs * grandpa: submit signed equivocation report transactions * grandpa: use proper equivocation report type * grandpa: report equivocations * grandpa: validate equivocation proof * grandpa: update to finality-grandpa 0.9.1 * grandpa: fix encoding of session membership proof * grandpa: initialize set id session mapping for genesis session * grandpa: fix bug in set_id session validation * fix compilation * cleanup from merge conflicts * cleanup crate tomls * grandpa: refactor equivocation handling to separate trait * node-template: fix compilation * fix test compilation * bump finality-grandpa to v0.10.2 * rpc: fix runtime version test * CHERRY-PICK #4200: Add documentation to SubmitSignedTransaction and actually make it work Squashed commit of the following: commit 4f2cb0b1c588a06f2f3b478bb4b28b5cb29d54b9 Author: Tomasz Drwięga <tomasz@parity.io> Date: Tue Dec 3 16:29:33 2019 +0100 Split the method to avoid confusing type error message. commit c5bf24eeaaf902add89ed1b046b22c4a4aaeb2cd Author: Tomasz Drwięga <tomasz@parity.io> Date: Tue Dec 3 16:19:55 2019 +0100 Make accounts optional, fix logic. commit 97db1ef556e023cf6847e5ffdb036c0e3ea6fb0a Author: Tomasz Drwięga <tomasz@parity.io> Date: Tue Dec 3 10:06:20 2019 +0100 Remove warning. commit 535f5c116d1a2e826eaf90c3f7e6798e443d61d8 Merge: 5162572170f1a5f651Author: Tomasz Drwięga <tomasz@parity.io> Date: Tue Dec 3 07:08:05 2019 +0100 Merge branch 'master' into td-signed-transactions commit 516257217bac89fcebd083712f4ea68b7b23b55a Merge: ac98248c62e68c80c2Author: Tomasz Drwięga <tomasz@parity.io> Date: Mon Dec 2 13:57:25 2019 +0100 Merge branch 'master' into td-signed-transactions commit ac98248c6c56cff381130645a82a13d29933cf83 Author: Tomasz Drwięga <tomasz@parity.io> Date: Mon Nov 25 17:34:52 2019 +0100 Forgotten import. commit 67a3c19031506c28e31c6bc4a90fff62d467dd58 Author: Tomasz Drwięga <tomasz@parity.io> Date: Mon Nov 25 17:32:10 2019 +0100 Fix naming and bounds. commit 93e768ea9df97a4629fca1f9bc4b108fdb33f876 Author: Tomasz Drwięga <tomasz@parity.io> Date: Mon Nov 25 17:01:05 2019 +0100 Add documentation to signed transactions and actually make them work. * grandpa: skip block initialization on report submission method * primitives: allow transaction pool access by default for offchain calls * grandpa: unused parameters * grandpa: remove unused method * grandpa: enable equivocation reporting * grandpa: add workaround for parameter encoding * grandpa: fix localized_payload calls in tests * fix submit_report_equivocation_extrinsic in runtimes * node: fix submit transaction test compilation * node: bump spec_version * rpc: fix api version test * grandpa: allow custom equivocation offence type * grandpa: add test for authorities::next_change_height * grandpa: cleanup report_equivocation function * node: move reporting app crypto to node-primitives * grandpa: move equivocation traits to own module * grandpa: rename app-crypto crate import * grandpa: export equivocation types * node: bump spec_version * grandpa: rename EquivocationReport to EquivocationProof * grandpa: add missing docs to primitives * grandpa: add missing docs to equivocation * node: fix compilation * grandpa: add missing docs to pallet * node: bump spec_version * fix whitespace * grandpa: return error on offence reporting * grandpa: expose session and validator count in proofs through traits * grandpa: use strong key in module KeyOwnerProofSystem * grandpa: move key ownership proof to grandpa runtime api * grandpa: remove unnecessary cloning when checking equivocation proof * grandpa: make report_equivocation a method in Environment * support: implement KeyOwnerProofSystem for () * grandpa: move KeyOwnerProofSystem to module trait * test-utils: fix runtime compilation * grandpa: fix test compilation * grandpa: fix test compilation after merge * grandpa: simplify transaction submission types * grandpa: validate equivocation report in signed extension * client: fix test * node: use ValidateEquivocationReport signed extension * grandpa: expose key ownership proof under opaque type * grandpa: better docs on key ownership proofs * grandpa: add note about signed extension * grandpa: add ValidateEquivocationReport::new * grandpa: remove skip_initialize_block from runtime api * grandpa: use new offchain transaction submission API * grandpa: take set_id in generate_key_ownership_proof * grandpa: update to finality-grandpa v0.12.2 * grandpa: cleanup usages of AuthoritySet::current * grandpa: fix test * grandpa: add mocking utilities for equivocation reporting * grandpa: add test for equivocation reporting * grandpa: move SetIdSession initialization * grandpa: add more tests * node: enable historical session manager * node: bump spec_version * node: use strong key types in KeyOwnerProofSystem definitions * grandpa: export GrandpaEquivocationOffence type
This commit is contained in:
Generated
+23
-2
@@ -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",
|
||||
|
||||
@@ -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 =
|
||||
<Self::KeyOwnerProofSystem as KeyOwnerProofSystem<(KeyTypeId, GrandpaId)>>::Proof;
|
||||
|
||||
type KeyOwnerIdentification = <Self::KeyOwnerProofSystem as KeyOwnerProofSystem<(
|
||||
KeyTypeId,
|
||||
GrandpaId,
|
||||
)>>::IdentificationTuple;
|
||||
|
||||
type HandleEquivocation = ();
|
||||
}
|
||||
|
||||
parameter_types! {
|
||||
@@ -360,7 +373,7 @@ impl_runtime_apis! {
|
||||
|
||||
fn decode_session_keys(
|
||||
encoded: Vec<u8>,
|
||||
) -> Option<Vec<(Vec<u8>, sp_core::crypto::KeyTypeId)>> {
|
||||
) -> Option<Vec<(Vec<u8>, 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<
|
||||
<Block as BlockT>::Hash,
|
||||
NumberFor<Block>,
|
||||
>,
|
||||
_key_owner_proof: fg_primitives::OpaqueKeyOwnershipProof,
|
||||
) -> Option<()> {
|
||||
None
|
||||
}
|
||||
|
||||
fn generate_key_ownership_proof(
|
||||
_set_id: fg_primitives::SetId,
|
||||
_authority_id: GrandpaId,
|
||||
) -> Option<fg_primitives::OpaqueKeyOwnershipProof> {
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
@@ -62,3 +62,34 @@ pub type Header = generic::Header<BlockNumber, BlakeTwo256>;
|
||||
pub type Block = generic::Block<Header, OpaqueExtrinsic>;
|
||||
/// Block ID.
|
||||
pub type BlockId = generic::BlockId<Block>;
|
||||
|
||||
/// 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<<Signature as Verify>::Signer, Signature> for ReporterAppCrypto {
|
||||
type RuntimeAppPublic = ReporterId;
|
||||
type GenericSignature = sp_core::sr25519::Signature;
|
||||
type GenericPublic = sp_core::sr25519::Public;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 = <Self as frame_system::Trait>::AccountId;
|
||||
type ValidatorIdOf = pallet_staking::StashOf<Self>;
|
||||
type ShouldEndSession = Babe;
|
||||
type SessionManager = Staking;
|
||||
type SessionManager = pallet_session::historical::NoteHistoricalRoot<Self, Staking>;
|
||||
type SessionHandler = <SessionKeys as OpaqueKeys>::KeyTypeIdProviders;
|
||||
type Keys = SessionKeys;
|
||||
type DisabledValidatorsThreshold = DisabledValidatorsThreshold;
|
||||
@@ -529,6 +533,7 @@ impl<LocalCall> frame_system::offchain::CreateSignedTransaction<LocalCall> for R
|
||||
frame_system::CheckNonce::<Runtime>::from(nonce),
|
||||
frame_system::CheckWeight::<Runtime>::new(),
|
||||
pallet_transaction_payment::ChargeTransactionPayment::<Runtime>::from(tip),
|
||||
pallet_grandpa::ValidateEquivocationReport::<Runtime>::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 =
|
||||
<Self::KeyOwnerProofSystem as KeyOwnerProofSystem<(KeyTypeId, GrandpaId)>>::Proof;
|
||||
|
||||
type KeyOwnerIdentification = <Self::KeyOwnerProofSystem as KeyOwnerProofSystem<(
|
||||
KeyTypeId,
|
||||
GrandpaId,
|
||||
)>>::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<T>, ValidateUnsigned, Config<T>},
|
||||
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<T>},
|
||||
Society: pallet_society::{Module, Call, Storage, Event<T>, Config<T>},
|
||||
@@ -720,6 +744,7 @@ pub type SignedExtra = (
|
||||
frame_system::CheckNonce<Runtime>,
|
||||
frame_system::CheckWeight<Runtime>,
|
||||
pallet_transaction_payment::ChargeTransactionPayment<Runtime>,
|
||||
pallet_grandpa::ValidateEquivocationReport<Runtime>,
|
||||
);
|
||||
/// Unchecked extrinsic type as expected by this runtime.
|
||||
pub type UncheckedExtrinsic = generic::UncheckedExtrinsic<Address, Call, Signature, SignedExtra>;
|
||||
@@ -792,6 +817,32 @@ impl_runtime_apis! {
|
||||
fn grandpa_authorities() -> GrandpaAuthorityList {
|
||||
Grandpa::grandpa_authorities()
|
||||
}
|
||||
|
||||
fn submit_report_equivocation_extrinsic(
|
||||
equivocation_proof: fg_primitives::EquivocationProof<
|
||||
<Block as BlockT>::Hash,
|
||||
NumberFor<Block>,
|
||||
>,
|
||||
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<fg_primitives::OpaqueKeyOwnershipProof> {
|
||||
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<Block> for Runtime {
|
||||
@@ -880,7 +931,7 @@ impl_runtime_apis! {
|
||||
|
||||
fn decode_session_keys(
|
||||
encoded: Vec<u8>,
|
||||
) -> Option<Vec<(Vec<u8>, sp_core::crypto::KeyTypeId)>> {
|
||||
) -> Option<Vec<(Vec<u8>, KeyTypeId)>> {
|
||||
SessionKeys::decode_into_raw_public_keys(&encoded)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -708,6 +708,7 @@ fn create_extrinsic<C: Crypto>(
|
||||
frame_system::CheckNonce::<Runtime>::from(i),
|
||||
frame_system::CheckWeight::<Runtime>::new(),
|
||||
pallet_transaction_payment::ChargeTransactionPayment::<Runtime>::from(f),
|
||||
pallet_grandpa::ValidateEquivocationReport::<Runtime>::new(),
|
||||
)
|
||||
};
|
||||
let raw_payload = SignedPayload::from_raw(
|
||||
@@ -720,6 +721,7 @@ fn create_extrinsic<C: Crypto>(
|
||||
(),
|
||||
(),
|
||||
(),
|
||||
(),
|
||||
),
|
||||
);
|
||||
let signature = raw_payload.using_encoded(|payload| signer.sign(payload)).into_runtime();
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -100,15 +100,17 @@ pub(crate) struct Status<H, N> {
|
||||
/// A set of authorities.
|
||||
#[derive(Debug, Clone, Encode, Decode, PartialEq)]
|
||||
pub(crate) struct AuthoritySet<H, N> {
|
||||
/// 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<H, N, PendingChange<H, N>>,
|
||||
// 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<PendingChange<H, N>>,
|
||||
}
|
||||
|
||||
@@ -162,9 +164,55 @@ where H: PartialEq,
|
||||
|
||||
impl<H: Eq, N> AuthoritySet<H, N>
|
||||
where
|
||||
N: Add<Output=N> + Ord + Clone + Debug,
|
||||
H: Clone + Debug
|
||||
N: Add<Output = N> + 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<F, E>(
|
||||
&self,
|
||||
best_hash: &H,
|
||||
is_descendent_of: &F,
|
||||
) -> Result<Option<(H, N)>, fork_tree::Error<E>>
|
||||
where
|
||||
F: Fn(&H, &H) -> Result<bool, E>,
|
||||
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<F, E>(
|
||||
&mut self,
|
||||
pending: PendingChange<H, N>,
|
||||
@@ -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
|
||||
|
||||
@@ -154,7 +154,7 @@ fn migrate_from_version0<Block: BlockT, B, G>(
|
||||
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<Block: BlockT, B, G>(
|
||||
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<Block: BlockT, B, G>(
|
||||
.expect("state is for completed round; completed rounds must have a prevote ghost; qed.");
|
||||
|
||||
VoterSetState::live(
|
||||
set.current().0,
|
||||
set.set_id,
|
||||
&set,
|
||||
base,
|
||||
)
|
||||
|
||||
@@ -814,7 +814,7 @@ impl<Block: BlockT> Inner<Block> {
|
||||
return Action::Discard(cost::UNKNOWN_VOTER);
|
||||
}
|
||||
|
||||
if let Err(()) = super::check_message_sig::<Block>(
|
||||
if let Err(()) = sp_finality_grandpa::check_message_signature(
|
||||
&full.message.message,
|
||||
&full.message.id,
|
||||
&full.message.signature,
|
||||
|
||||
@@ -610,30 +610,6 @@ impl<B: BlockT, N: Network<B>> Clone for NetworkBridge<B, N> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Encode round message localized to a given round and set id.
|
||||
pub(crate) fn localized_payload<E: Encode>(
|
||||
round: RoundNumber,
|
||||
set_id: SetIdNumber,
|
||||
message: &E,
|
||||
) -> Vec<u8> {
|
||||
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<E: Encode>(
|
||||
round: RoundNumber,
|
||||
set_id: SetIdNumber,
|
||||
message: &E,
|
||||
buf: &mut Vec<u8>,
|
||||
) {
|
||||
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<Block: BlockT>(
|
||||
message: &Message<Block>,
|
||||
id: &AuthorityId,
|
||||
signature: &AuthoritySignature,
|
||||
round: RoundNumber,
|
||||
set_id: SetIdNumber,
|
||||
) -> Result<(), ()> {
|
||||
check_message_sig_with_buffer::<Block>(
|
||||
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<Block: BlockT>(
|
||||
message: &Message<Block>,
|
||||
id: &AuthorityId,
|
||||
signature: &AuthoritySignature,
|
||||
round: RoundNumber,
|
||||
set_id: SetIdNumber,
|
||||
buf: &mut Vec<u8>,
|
||||
) -> 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<Block: BlockT> Sink<Message<Block>> for OutgoingMessages<Block>
|
||||
}
|
||||
|
||||
// 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::<Block> {
|
||||
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::<Block> {
|
||||
message: signed.clone(),
|
||||
@@ -828,7 +760,7 @@ fn check_compact_commit<Block: BlockT>(
|
||||
use crate::communication::gossip::Misbehavior;
|
||||
use finality_grandpa::Message as GrandpaMessage;
|
||||
|
||||
if let Err(()) = check_message_sig_with_buffer::<Block>(
|
||||
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<Block: BlockT>(
|
||||
for (msg, id, sig) in messages {
|
||||
signatures_checked += 1;
|
||||
|
||||
if let Err(()) = check_message_sig_with_buffer::<B>(
|
||||
if let Err(()) = sp_finality_grandpa::check_message_signature_with_buffer(
|
||||
&msg,
|
||||
id,
|
||||
sig,
|
||||
|
||||
@@ -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())
|
||||
);
|
||||
|
||||
|
||||
@@ -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<Block> = finality_grandpa::HistoricalVotes<
|
||||
@@ -124,7 +127,7 @@ impl<Block: BlockT> CompletedRounds<Block> {
|
||||
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<Backend, Block: BlockT, C, N: NetworkT<Block>, SC,
|
||||
pub(crate) _phantom: PhantomData<Backend>,
|
||||
}
|
||||
|
||||
impl<Backend, Block: BlockT, C, N: NetworkT<Block>, SC, VR> Environment<Backend, Block, C, N, SC, VR> {
|
||||
impl<BE, Block: BlockT, C, N: NetworkT<Block>, SC, VR> Environment<BE, Block, C, N, SC, VR> {
|
||||
/// 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<Backend, Block: BlockT, C, N: NetworkT<Block>, SC, VR> Environment<Backend,
|
||||
}
|
||||
}
|
||||
|
||||
impl<BE, Block, C, N, SC, VR> Environment<BE, Block, C, N, SC, VR>
|
||||
where
|
||||
Block: BlockT,
|
||||
BE: Backend<Block>,
|
||||
C: crate::ClientForGrandpa<Block, BE>,
|
||||
C::Api: GrandpaApi<Block, Error = sp_blockchain::Error>,
|
||||
N: NetworkT<Block>,
|
||||
SC: SelectChain<Block> + '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<Block::Hash, NumberFor<Block>>,
|
||||
) -> 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<BE, Block: BlockT, C, N, SC, VR>
|
||||
finality_grandpa::Chain<Block::Hash, NumberFor<Block>>
|
||||
for Environment<BE, Block, C, N, SC, VR>
|
||||
@@ -437,7 +532,7 @@ where
|
||||
Block: 'static,
|
||||
BE: Backend<Block>,
|
||||
C: crate::ClientForGrandpa<Block, BE>,
|
||||
N: NetworkT<Block> + 'static + Send,
|
||||
N: NetworkT<Block> + 'static + Send,
|
||||
SC: SelectChain<Block> + 'static,
|
||||
VR: VotingRule<Block, C>,
|
||||
NumberFor<Block>: 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<Block>,
|
||||
C: crate::ClientForGrandpa<Block, B> + 'static,
|
||||
N: NetworkT<Block> + 'static + Send + Sync,
|
||||
C::Api: GrandpaApi<Block, Error = sp_blockchain::Error>,
|
||||
N: NetworkT<Block> + 'static + Send + Sync,
|
||||
SC: SelectChain<Block> + 'static,
|
||||
VR: VotingRule<Block, C>,
|
||||
NumberFor<Block>: BlockNumberOps,
|
||||
@@ -925,19 +1021,23 @@ where
|
||||
fn prevote_equivocation(
|
||||
&self,
|
||||
_round: RoundNumber,
|
||||
equivocation: ::finality_grandpa::Equivocation<Self::Id, Prevote<Block>, Self::Signature>
|
||||
equivocation: finality_grandpa::Equivocation<Self::Id, Prevote<Block>, 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::Id, Precommit<Block>, Self::Signature>
|
||||
equivocation: finality_grandpa::Equivocation<Self::Id, Precommit<Block>, 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<Block: BlockT> GrandpaJustification<Block> {
|
||||
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::<Block>(
|
||||
if let Err(_) = sp_finality_grandpa::check_message_signature_with_buffer(
|
||||
&finality_grandpa::Message::Precommit(signed.precommit.clone()),
|
||||
&signed.id,
|
||||
&signed.signature,
|
||||
|
||||
@@ -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<Block> = finality_grandpa::Message<<Block as BlockT>::Hash, NumberFor<Block>>;
|
||||
|
||||
/// A signed message.
|
||||
pub type SignedMessage<Block> = finality_grandpa::SignedMessage<
|
||||
<Block as BlockT>::Hash,
|
||||
@@ -687,6 +688,7 @@ pub fn run_grandpa_voter<Block: BlockT, BE: 'static, C, N, SC, VR>(
|
||||
NumberFor<Block>: BlockNumberOps,
|
||||
DigestFor<Block>: Encode,
|
||||
C: ClientForGrandpa<Block, BE> + 'static,
|
||||
C::Api: GrandpaApi<Block, Error = sp_blockchain::Error>,
|
||||
{
|
||||
let GrandpaParams {
|
||||
mut config,
|
||||
@@ -802,6 +804,7 @@ where
|
||||
Block: BlockT,
|
||||
B: Backend<Block> + 'static,
|
||||
C: ClientForGrandpa<Block, B> + 'static,
|
||||
C::Api: GrandpaApi<Block, Error = sp_blockchain::Error>,
|
||||
N: NetworkT<Block> + Sync,
|
||||
NumberFor<Block>: BlockNumberOps,
|
||||
SC: SelectChain<Block> + '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<Block>: BlockNumberOps,
|
||||
SC: SelectChain<Block> + 'static,
|
||||
C: ClientForGrandpa<Block, B> + 'static,
|
||||
C::Api: GrandpaApi<Block, Error = sp_blockchain::Error>,
|
||||
VR: VotingRule<Block, C> + Clone + 'static,
|
||||
{
|
||||
type Output = Result<(), Error>;
|
||||
|
||||
@@ -175,6 +175,7 @@ where
|
||||
select_chain: _,
|
||||
persistent_data,
|
||||
voter_commands_rx,
|
||||
..
|
||||
} = link;
|
||||
|
||||
let network = NetworkBridge::new(
|
||||
|
||||
@@ -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<Hash, BlockNumber>,
|
||||
_key_owner_proof: OpaqueKeyOwnershipProof,
|
||||
) -> Option<()> {
|
||||
None
|
||||
}
|
||||
|
||||
fn generate_key_ownership_proof(
|
||||
_set_id: SetId,
|
||||
_authority_id: AuthorityId,
|
||||
) -> Option<OpaqueKeyOwnershipProof> {
|
||||
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 {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
//!
|
||||
//! 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<T>(sp_std::marker::PhantomData<T>);
|
||||
|
||||
impl<T> Default for ValidateEquivocationReport<T> {
|
||||
fn default() -> ValidateEquivocationReport<T> {
|
||||
ValidateEquivocationReport::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> ValidateEquivocationReport<T> {
|
||||
pub fn new() -> ValidateEquivocationReport<T> {
|
||||
ValidateEquivocationReport(Default::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> sp_std::fmt::Debug for ValidateEquivocationReport<T> {
|
||||
fn fmt(&self, f: &mut sp_std::fmt::Formatter) -> sp_std::fmt::Result {
|
||||
write!(f, "ValidateEquivocationReport<T>")
|
||||
}
|
||||
}
|
||||
|
||||
/// 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<ReportEquivocationValidityError> for TransactionValidityError {
|
||||
fn from(e: ReportEquivocationValidityError) -> TransactionValidityError {
|
||||
TransactionValidityError::from(InvalidTransaction::Custom(e as u8))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: super::Trait + Send + Sync> SignedExtension for ValidateEquivocationReport<T>
|
||||
where
|
||||
<T as frame_system::Trait>::Call: IsSubType<super::Module<T>, T>,
|
||||
{
|
||||
const IDENTIFIER: &'static str = "ValidateEquivocationReport";
|
||||
type AccountId = T::AccountId;
|
||||
type Call = <T as frame_system::Trait>::Call;
|
||||
type AdditionalSigned = ();
|
||||
type Pre = ();
|
||||
|
||||
fn additional_signed(
|
||||
&self,
|
||||
) -> sp_std::result::Result<Self::AdditionalSigned, TransactionValidityError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate(
|
||||
&self,
|
||||
_who: &Self::AccountId,
|
||||
call: &Self::Call,
|
||||
_info: &DispatchInfoOf<Self::Call>,
|
||||
_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) = <super::Module<T>>::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) = <super::Module<T>>::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<T: super::Trait> {
|
||||
/// The offence type used for reporting offences on valid equivocation reports.
|
||||
type Offence: GrandpaOffence<T::KeyOwnerIdentification>;
|
||||
|
||||
/// Report an offence proved by the given reporters.
|
||||
fn report_offence(
|
||||
reporters: Vec<T::AccountId>,
|
||||
offence: Self::Offence,
|
||||
) -> Result<(), OffenceError>;
|
||||
|
||||
/// Create and dispatch an equivocation report extrinsic.
|
||||
fn submit_equivocation_report(
|
||||
equivocation_proof: EquivocationProof<T::Hash, T::BlockNumber>,
|
||||
key_owner_proof: T::KeyOwnerProof,
|
||||
) -> DispatchResult;
|
||||
}
|
||||
|
||||
impl<T: super::Trait> HandleEquivocation<T> for () {
|
||||
type Offence = GrandpaEquivocationOffence<T::KeyOwnerIdentification>;
|
||||
|
||||
fn report_offence(
|
||||
_reporters: Vec<T::AccountId>,
|
||||
_offence: GrandpaEquivocationOffence<T::KeyOwnerIdentification>,
|
||||
) -> Result<(), OffenceError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn submit_equivocation_report(
|
||||
_equivocation_proof: EquivocationProof<T::Hash, T::BlockNumber>,
|
||||
_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<I, C, S, R, O = GrandpaEquivocationOffence<I>> {
|
||||
_phantom: sp_std::marker::PhantomData<(I, C, S, R, O)>,
|
||||
}
|
||||
|
||||
impl<I, C, S, R, O> Default for EquivocationHandler<I, C, S, R, O> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
_phantom: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, C, S, R, O> HandleEquivocation<T>
|
||||
for EquivocationHandler<T::KeyOwnerIdentification, C, S, R, O>
|
||||
where
|
||||
// A signed transaction creator. Used for signing and submitting equivocation reports.
|
||||
T: super::Trait + CreateSignedTransaction<super::Call<T>>,
|
||||
// Application-specific crypto bindings.
|
||||
C: AppCrypto<T::Public, T::Signature>,
|
||||
// The offence type that should be used when reporting.
|
||||
O: GrandpaOffence<T::KeyOwnerIdentification>,
|
||||
// A system for reporting offences after valid equivocation reports are
|
||||
// processed.
|
||||
R: ReportOffence<T::AccountId, T::KeyOwnerIdentification, O>,
|
||||
{
|
||||
type Offence = O;
|
||||
|
||||
fn report_offence(reporters: Vec<T::AccountId>, offence: O) -> Result<(), OffenceError> {
|
||||
R::report_offence(reporters, offence)
|
||||
}
|
||||
|
||||
fn submit_equivocation_report(
|
||||
equivocation_proof: EquivocationProof<T::Hash, T::BlockNumber>,
|
||||
key_owner_proof: T::KeyOwnerProof,
|
||||
) -> DispatchResult {
|
||||
use frame_system::offchain::SendSignedTransaction;
|
||||
|
||||
let signer = Signer::<T, C>::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<FullIdentification> {
|
||||
/// 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<FullIdentification>: Offence<FullIdentification> {
|
||||
/// 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<FullIdentification: Clone> GrandpaOffence<FullIdentification>
|
||||
for GrandpaEquivocationOffence<FullIdentification>
|
||||
{
|
||||
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<FullIdentification: Clone> Offence<FullIdentification>
|
||||
for GrandpaEquivocationOffence<FullIdentification>
|
||||
{
|
||||
const ID: Kind = *b"grandpa:equivoca";
|
||||
type TimeSlot = GrandpaTimeSlot;
|
||||
|
||||
fn offenders(&self) -> Vec<FullIdentification> {
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -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 fg_primitives::{
|
||||
GRANDPA_AUTHORITIES_KEY, GRANDPA_ENGINE_ID, ScheduledChange, ConsensusLog, SetId, RoundNumber,
|
||||
};
|
||||
pub use fg_primitives::{AuthorityId, AuthorityList, AuthorityWeight, VersionedAuthorityList};
|
||||
use frame_system::{self as system, ensure_signed, DigestOf};
|
||||
|
||||
use codec::{self as codec, Decode, Encode};
|
||||
pub use fg_primitives::{AuthorityId, AuthorityList, AuthorityWeight, VersionedAuthorityList};
|
||||
use fg_primitives::{
|
||||
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,
|
||||
};
|
||||
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<Event> + Into<<Self as frame_system::Trait>::Event>;
|
||||
|
||||
/// The function call.
|
||||
type Call: From<Call<Self>>;
|
||||
|
||||
/// 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<Self>;
|
||||
}
|
||||
|
||||
/// A stored pending change, old format.
|
||||
@@ -173,7 +211,9 @@ decl_storage! {
|
||||
}
|
||||
add_extra_genesis {
|
||||
config(authorities): AuthorityList;
|
||||
build(|config| Module::<T>::initialize_authorities(&config.authorities))
|
||||
build(|config| {
|
||||
Module::<T>::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<u8>) {
|
||||
ensure_signed(origin)?;
|
||||
// FIXME: https://github.com/paritytech/substrate/issues/1112
|
||||
fn report_equivocation(
|
||||
origin,
|
||||
equivocation_proof: EquivocationProof<T::Hash, T::BlockNumber>,
|
||||
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],
|
||||
<T::HandleEquivocation as HandleEquivocation<T>>::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<T: Trait> Module<T> {
|
||||
<frame_system::Module<T>>::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<T: Trait> Module<T> {
|
||||
);
|
||||
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<T::Hash, T::BlockNumber>,
|
||||
key_owner_proof: T::KeyOwnerProof,
|
||||
) -> Option<()> {
|
||||
T::HandleEquivocation::submit_equivocation_report(equivocation_proof, key_owner_proof)
|
||||
.ok()?;
|
||||
|
||||
Some(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -411,7 +510,7 @@ impl<T: Trait> pallet_session::OneSessionHandler<T::AccountId> for Module<T>
|
||||
where I: Iterator<Item=(&'a T::AccountId, AuthorityId)>
|
||||
{
|
||||
let authorities = validators.map(|(_, k)| (k, 1)).collect::<Vec<_>>();
|
||||
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<T: Trait> pallet_finality_tracker::OnFinalizationStalled<T::BlockNumber> fo
|
||||
<Stalled<T>>::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<FullIdentification> {
|
||||
/// 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<FullIdentification: Clone> Offence<FullIdentification> for GrandpaEquivocationOffence<FullIdentification> {
|
||||
const ID: Kind = *b"grandpa:equivoca";
|
||||
type TimeSlot = GrandpaTimeSlot;
|
||||
|
||||
fn offenders(&self) -> Vec<FullIdentification> {
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<u64>) -> DigestItem<H256> {
|
||||
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)]
|
||||
impl_opaque_keys! {
|
||||
pub struct TestSessionKeys {
|
||||
pub grandpa_authority: super::Module<Test>,
|
||||
}
|
||||
}
|
||||
|
||||
impl_outer_event! {
|
||||
pub enum TestEvent for Test {
|
||||
system<T>,
|
||||
balances<T>,
|
||||
grandpa,
|
||||
offences,
|
||||
session,
|
||||
staking<T>,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Eq, PartialEq)]
|
||||
pub struct Test;
|
||||
|
||||
impl Trait for Test {
|
||||
type Event = TestEvent;
|
||||
}
|
||||
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<u128>;
|
||||
type OnNewAccount = ();
|
||||
type OnKilledAccount = ();
|
||||
}
|
||||
|
||||
impl<C> system::offchain::SendTransactionTypes<C> for Test
|
||||
where
|
||||
Call: From<C>,
|
||||
{
|
||||
type OverarchingCall = Call;
|
||||
type Extrinsic = TestXt<Call, ()>;
|
||||
}
|
||||
|
||||
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<Self>;
|
||||
type ShouldEndSession = session::PeriodicSessions<Period, Offset>;
|
||||
type NextSessionRotation = session::PeriodicSessions<Period, Offset>;
|
||||
type SessionManager = session::historical::NoteHistoricalRoot<Self, Staking>;
|
||||
type SessionHandler = <TestSessionKeys as OpaqueKeys>::KeyTypeIdProviders;
|
||||
type Keys = TestSessionKeys;
|
||||
type DisabledValidatorsThreshold = DisabledValidatorsThreshold;
|
||||
}
|
||||
|
||||
impl session::historical::Trait for Test {
|
||||
type FullIdentification = staking::Exposure<u64, u128>;
|
||||
type FullIdentificationOf = staking::ExposureOf<Self>;
|
||||
}
|
||||
|
||||
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<u128, u128> for CurrencyToVoteHandler {
|
||||
fn convert(x: u128) -> u128 {
|
||||
x
|
||||
}
|
||||
}
|
||||
|
||||
impl Convert<u128, u64> 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<Self::AccountId>;
|
||||
type SessionInterface = Self;
|
||||
type UnixTime = timestamp::Module<Test>;
|
||||
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<Self>;
|
||||
type OnOffenceHandler = Staking;
|
||||
}
|
||||
|
||||
impl Trait for Test {
|
||||
type Event = TestEvent;
|
||||
type Call = Call;
|
||||
|
||||
type KeyOwnerProofSystem = Historical;
|
||||
|
||||
type KeyOwnerProof =
|
||||
<Self::KeyOwnerProofSystem as KeyOwnerProofSystem<(KeyTypeId, AuthorityId)>>::Proof;
|
||||
|
||||
type KeyOwnerIdentification = <Self::KeyOwnerProofSystem as KeyOwnerProofSystem<(
|
||||
KeyTypeId,
|
||||
AuthorityId,
|
||||
)>>::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<ReporterId, sp_core::ed25519::Signature>
|
||||
for ReporterAppCrypto
|
||||
{
|
||||
type RuntimeAppPublic = ReporterId;
|
||||
type GenericSignature = sp_core::ed25519::Signature;
|
||||
type GenericPublic = sp_core::ed25519::Public;
|
||||
}
|
||||
}
|
||||
|
||||
type Extrinsic = TestXt<Call, ()>;
|
||||
|
||||
impl<LocalCall> system::offchain::CreateSignedTransaction<LocalCall> for Test
|
||||
where
|
||||
Call: From<LocalCall>,
|
||||
{
|
||||
fn create_transaction<C: system::offchain::AppCrypto<Self::Public, Self::Signature>>(
|
||||
call: Call,
|
||||
_public: reporting_keys::ReporterId,
|
||||
_account: <Test as system::Trait>::AccountId,
|
||||
nonce: <Test as system::Trait>::Index,
|
||||
) -> Option<(Call, <Extrinsic as ExtrinsicT>::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<T>,
|
||||
grandpa,
|
||||
}
|
||||
pub type Balances = pallet_balances::Module<Test>;
|
||||
pub type Historical = pallet_session::historical::Module<Test>;
|
||||
pub type Offences = pallet_offences::Module<Test>;
|
||||
pub type Session = pallet_session::Module<Test>;
|
||||
pub type Staking = pallet_staking::Module<Test>;
|
||||
pub type System = frame_system::Module<Test>;
|
||||
pub type Timestamp = pallet_timestamp::Module<Test>;
|
||||
pub type Grandpa = Module<Test>;
|
||||
|
||||
pub fn grandpa_log(log: ConsensusLog<u64>) -> DigestItem<H256> {
|
||||
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::<Test>().unwrap();
|
||||
GenesisConfig {
|
||||
authorities: to_authorities(authorities),
|
||||
}.assimilate_storage::<Test>(&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::<Test>()
|
||||
.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::<u64>::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::<Test> { keys: session_keys }
|
||||
.assimilate_storage(&mut t)
|
||||
.unwrap();
|
||||
|
||||
balances::GenesisConfig::<Test> { balances }
|
||||
.assimilate_storage(&mut t)
|
||||
.unwrap();
|
||||
|
||||
let staking_config = staking::GenesisConfig::<Test> {
|
||||
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<Test>;
|
||||
pub type Grandpa = Module<Test>;
|
||||
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<H256, u64>,
|
||||
key_owner_proof: sp_session::MembershipProof,
|
||||
) -> Result<GrandpaCall<Test>, TransactionValidityError> {
|
||||
let inner = GrandpaCall::report_equivocation(equivocation_proof, key_owner_proof);
|
||||
let call = Call::Grandpa(inner.clone());
|
||||
|
||||
ValidateEquivocationReport::<Test>::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<H256, u64> {
|
||||
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),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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<T: Trait, I> crate::SessionManager<T::ValidatorId> for NoteHistoricalRoot<T
|
||||
});
|
||||
|
||||
if let Some(new_validators) = new_validators_and_id {
|
||||
let count = new_validators.len() as u32;
|
||||
let count = new_validators.len() as ValidatorCount;
|
||||
match ProvingTrie::<T>::generate_for(new_validators) {
|
||||
Ok(trie) => <HistoricalSessions<T>>::insert(new_index, &(trie.root, count)),
|
||||
Err(reason) => {
|
||||
@@ -253,54 +252,58 @@ impl<T: Trait> ProvingTrie<T> {
|
||||
|
||||
}
|
||||
|
||||
/// Proof of ownership of a specific key.
|
||||
#[derive(Encode, Decode, Clone, Eq, PartialEq, RuntimeDebug)]
|
||||
pub struct Proof {
|
||||
session: SessionIndex,
|
||||
trie_nodes: Vec<Vec<u8>>,
|
||||
}
|
||||
|
||||
impl Proof {
|
||||
/// Returns a session this proof was generated for.
|
||||
pub fn session(&self) -> SessionIndex {
|
||||
self.session
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Trait, D: AsRef<[u8]>> frame_support::traits::KeyOwnerProofSystem<(KeyTypeId, D)>
|
||||
for Module<T>
|
||||
{
|
||||
type Proof = Proof;
|
||||
type Proof = MembershipProof;
|
||||
type IdentificationTuple = IdentificationTuple<T>;
|
||||
|
||||
fn prove(key: (KeyTypeId, D)) -> Option<Self::Proof> {
|
||||
let session = <SessionModule<T>>::current_index();
|
||||
let validators = <SessionModule<T>>::validators().into_iter()
|
||||
let validators = <SessionModule<T>>::validators()
|
||||
.into_iter()
|
||||
.filter_map(|validator| {
|
||||
T::FullIdentificationOf::convert(validator.clone())
|
||||
.map(|full_id| (validator, full_id))
|
||||
});
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let count = validators.len() as ValidatorCount;
|
||||
|
||||
let trie = ProvingTrie::<T>::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<IdentificationTuple<T>> {
|
||||
fn check_proof(key: (KeyTypeId, D), proof: Self::Proof) -> Option<IdentificationTuple<T>> {
|
||||
let (id, data) = key;
|
||||
|
||||
if proof.session == <SessionModule<T>>::current_index() {
|
||||
<SessionModule<T>>::key_owner(id, data.as_ref()).and_then(|owner|
|
||||
T::FullIdentificationOf::convert(owner.clone()).map(move |id| (owner, id))
|
||||
)
|
||||
} else {
|
||||
let (root, _) = <HistoricalSessions<T>>::get(&proof.session)?;
|
||||
let trie = ProvingTrie::<T>::from_nodes(root, &proof.trie_nodes);
|
||||
<SessionModule<T>>::key_owner(id, data.as_ref()).and_then(|owner| {
|
||||
T::FullIdentificationOf::convert(owner.clone()).and_then(move |id| {
|
||||
let count = <SessionModule<T>>::validators().len() as ValidatorCount;
|
||||
|
||||
if count != proof.validator_count {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some((owner, id))
|
||||
})
|
||||
})
|
||||
} else {
|
||||
let (root, count) = <HistoricalSessions<T>>::get(&proof.session)?;
|
||||
|
||||
if count != proof.validator_count {
|
||||
return None;
|
||||
}
|
||||
|
||||
let trie = ProvingTrie::<T>::from_nodes(root, &proof.trie_nodes);
|
||||
trie.query(id, data.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")]
|
||||
|
||||
@@ -288,6 +288,21 @@ pub trait KeyOwnerProofSystem<Key> {
|
||||
fn check_proof(key: Key, proof: Self::Proof) -> Option<Self::IdentificationTuple>;
|
||||
}
|
||||
|
||||
impl<Key> KeyOwnerProofSystem<Key> 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<Self::Proof> {
|
||||
None
|
||||
}
|
||||
|
||||
fn check_proof(_key: Key, _proof: Self::Proof) -> Option<Self::IdentificationTuple> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Handler for when some currency "account" decreased in balance for
|
||||
/// some reason.
|
||||
///
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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<N: Codec> ConsensusLog<N> {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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<H, N> {
|
||||
set_id: SetId,
|
||||
equivocation: Equivocation<H, N>,
|
||||
}
|
||||
|
||||
impl<H, N> EquivocationProof<H, N> {
|
||||
/// Create a new `EquivocationProof` for the given set id and using the
|
||||
/// given equivocation as proof.
|
||||
pub fn new(set_id: SetId, equivocation: Equivocation<H, N>) -> 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<H, N> {
|
||||
/// Proof of equivocation at prevote stage.
|
||||
Prevote(grandpa::Equivocation<AuthorityId, grandpa::Prevote<H, N>, AuthoritySignature>),
|
||||
/// Proof of equivocation at precommit stage.
|
||||
Precommit(grandpa::Equivocation<AuthorityId, grandpa::Precommit<H, N>, AuthoritySignature>),
|
||||
}
|
||||
|
||||
impl<H, N> From<grandpa::Equivocation<AuthorityId, grandpa::Prevote<H, N>, AuthoritySignature>>
|
||||
for Equivocation<H, N>
|
||||
{
|
||||
fn from(
|
||||
equivocation: grandpa::Equivocation<
|
||||
AuthorityId,
|
||||
grandpa::Prevote<H, N>,
|
||||
AuthoritySignature,
|
||||
>,
|
||||
) -> Self {
|
||||
Equivocation::Prevote(equivocation)
|
||||
}
|
||||
}
|
||||
|
||||
impl<H, N> From<grandpa::Equivocation<AuthorityId, grandpa::Precommit<H, N>, AuthoritySignature>>
|
||||
for Equivocation<H, N>
|
||||
{
|
||||
fn from(
|
||||
equivocation: grandpa::Equivocation<
|
||||
AuthorityId,
|
||||
grandpa::Precommit<H, N>,
|
||||
AuthoritySignature,
|
||||
>,
|
||||
) -> Self {
|
||||
Equivocation::Precommit(equivocation)
|
||||
}
|
||||
}
|
||||
|
||||
impl<H, N> Equivocation<H, N> {
|
||||
/// 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<H, N>(report: EquivocationProof<H, N>) -> 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<E: Encode>(round: RoundNumber, set_id: SetId, message: &E) -> Vec<u8> {
|
||||
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<E: Encode>(
|
||||
round: RoundNumber,
|
||||
set_id: SetId,
|
||||
message: &E,
|
||||
buf: &mut Vec<u8>,
|
||||
) {
|
||||
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<H, N>(
|
||||
message: &grandpa::Message<H, N>,
|
||||
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<H, N>(
|
||||
message: &grandpa::Message<H, N>,
|
||||
id: &AuthorityId,
|
||||
signature: &AuthoritySignature,
|
||||
round: RoundNumber,
|
||||
set_id: SetId,
|
||||
buf: &mut Vec<u8>,
|
||||
) -> 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<H, N>(
|
||||
message: grandpa::Message<H, N>,
|
||||
pair: &AuthorityPair,
|
||||
round: RoundNumber,
|
||||
set_id: SetId,
|
||||
) -> grandpa::SignedMessage<H, N, AuthoritySignature, AuthorityId>
|
||||
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<u8>);
|
||||
|
||||
impl OpaqueKeyOwnershipProof {
|
||||
/// Create a new `OpaqueKeyOwnershipProof` using the given encoded
|
||||
/// representation.
|
||||
pub fn new(inner: Vec<u8>) -> OpaqueKeyOwnershipProof {
|
||||
OpaqueKeyOwnershipProof(inner)
|
||||
}
|
||||
|
||||
/// Try to decode this `OpaqueKeyOwnershipProof` into the given concrete key
|
||||
/// ownership proof type.
|
||||
pub fn decode<T: Decode>(self) -> Option<T> {
|
||||
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<Block::Hash, NumberFor<Block>>,
|
||||
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<OpaqueKeyOwnershipProof>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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<Vec<u8>>,
|
||||
/// 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")]
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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<Block> for Runtime {
|
||||
fn grandpa_authorities() -> sp_finality_grandpa::AuthorityList {
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
fn submit_report_equivocation_extrinsic(
|
||||
_equivocation_proof: sp_finality_grandpa::EquivocationProof<
|
||||
<Block as BlockT>::Hash,
|
||||
NumberFor<Block>,
|
||||
>,
|
||||
_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<sp_finality_grandpa::OpaqueKeyOwnershipProof> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl frame_system_rpc_runtime_api::AccountNonceApi<Block, AccountId, Index> for Runtime {
|
||||
fn account_nonce(_account: AccountId) -> Index {
|
||||
0
|
||||
|
||||
Reference in New Issue
Block a user