diff --git a/Cargo.lock b/Cargo.lock index 642fe88db006f..197ea22572e8b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9688,6 +9688,7 @@ dependencies = [ "log", "pallet-authorship", "pallet-balances", + "pallet-mmr", "pallet-offences", "pallet-session", "pallet-staking", @@ -9699,6 +9700,7 @@ dependencies = [ "sp-consensus-beefy", "sp-core", "sp-io", + "sp-mmr-primitives", "sp-runtime", "sp-session", "sp-staking", @@ -9722,9 +9724,11 @@ dependencies = [ "scale-info", "serde", "sp-api", + "sp-application-crypto", "sp-consensus-beefy", "sp-core", "sp-io", + "sp-mmr-primitives", "sp-runtime", "sp-staking", "sp-state-machine", @@ -19578,6 +19582,7 @@ dependencies = [ "sp-core", "sp-debug-derive 14.0.0", "sp-runtime", + "sp-std 14.0.0", "thiserror", ] diff --git a/polkadot/node/service/src/fake_runtime_api.rs b/polkadot/node/service/src/fake_runtime_api.rs index 03c4836020d98..3b6e38681b120 100644 --- a/polkadot/node/service/src/fake_runtime_api.rs +++ b/polkadot/node/service/src/fake_runtime_api.rs @@ -232,7 +232,7 @@ sp_api::impl_runtime_apis! { } } - impl beefy_primitives::BeefyApi for Runtime { + impl beefy_primitives::BeefyApi for Runtime { fn beefy_genesis() -> Option { unimplemented!() } @@ -241,7 +241,7 @@ sp_api::impl_runtime_apis! { unimplemented!() } - fn submit_report_equivocation_unsigned_extrinsic( + fn submit_report_vote_equivocation_unsigned_extrinsic( _: beefy_primitives::DoubleVotingProof< BlockNumber, BeefyId, @@ -252,6 +252,13 @@ sp_api::impl_runtime_apis! { unimplemented!() } + fn submit_report_fork_equivocation_unsigned_extrinsic( + _: beefy_primitives::ForkEquivocationProof::Header, Hash>, + _: Vec, + ) -> Option<()> { + unimplemented!() + } + fn generate_key_ownership_proof( _: beefy_primitives::ValidatorSetId, _: BeefyId, @@ -289,6 +296,18 @@ sp_api::impl_runtime_apis! { ) -> Result<(), sp_mmr_primitives::Error> { unimplemented!() } + + fn generate_ancestry_proof( + _: u32, + _: Option, + ) -> Result, sp_mmr_primitives::Error> { + unimplemented!() + } + fn verify_ancestry_proof( + _: sp_mmr_primitives::AncestryProof, + ) -> Result<(), sp_mmr_primitives::Error> { + unimplemented!() + } } impl grandpa_primitives::GrandpaApi for Runtime { diff --git a/polkadot/runtime/rococo/src/lib.rs b/polkadot/runtime/rococo/src/lib.rs index c22d5c39b2330..829fed1270891 100644 --- a/polkadot/runtime/rococo/src/lib.rs +++ b/polkadot/runtime/rococo/src/lib.rs @@ -1270,6 +1270,7 @@ impl pallet_beefy::Config for Runtime { type MaxNominators = ConstU32<0>; type MaxSetIdSessionEntries = BeefySetIdSessionEntries; type OnNewValidatorSet = MmrLeaf; + type CheckForkEquivocationProof = MmrLeaf; type WeightInfo = (); type KeyOwnerProof = >::Proof; type EquivocationReportSystem = @@ -2083,8 +2084,8 @@ sp_api::impl_runtime_apis! { } } - #[api_version(3)] - impl beefy_primitives::BeefyApi for Runtime { + #[api_version(4)] + impl beefy_primitives::BeefyApi for Runtime { fn beefy_genesis() -> Option { pallet_beefy::GenesisBlock::::get() } @@ -2093,8 +2094,8 @@ sp_api::impl_runtime_apis! { Beefy::validator_set() } - fn submit_report_equivocation_unsigned_extrinsic( - equivocation_proof: beefy_primitives::DoubleVotingProof< + fn submit_report_vote_equivocation_unsigned_extrinsic( + vote_equivocation_proof: beefy_primitives::DoubleVotingProof< BlockNumber, BeefyId, BeefySignature, @@ -2103,12 +2104,22 @@ sp_api::impl_runtime_apis! { ) -> Option<()> { let key_owner_proof = key_owner_proof.decode()?; - Beefy::submit_unsigned_equivocation_report( - equivocation_proof, + Beefy::submit_unsigned_vote_equivocation_report( + vote_equivocation_proof, key_owner_proof, ) } + fn submit_report_fork_equivocation_unsigned_extrinsic( + fork_equivocation_proof: beefy_primitives::ForkEquivocationProof, + key_owner_proofs: Vec, + ) -> Option<()> { + Beefy::submit_unsigned_fork_equivocation_report( + fork_equivocation_proof, + key_owner_proofs, + ) + } + fn generate_key_ownership_proof( _set_id: beefy_primitives::ValidatorSetId, authority_id: BeefyId, @@ -2121,7 +2132,7 @@ sp_api::impl_runtime_apis! { } } - #[api_version(2)] + #[api_version(3)] impl mmr::MmrApi for Runtime { fn mmr_root() -> Result { Ok(pallet_mmr::RootHash::::get()) @@ -2166,6 +2177,19 @@ sp_api::impl_runtime_apis! { let nodes = leaves.into_iter().map(|leaf|mmr::DataOrHash::Data(leaf.into_opaque_leaf())).collect(); pallet_mmr::verify_leaves_proof::(root, nodes, proof) } + + fn generate_ancestry_proof( + prev_best_block: BlockNumber, + best_known_block_number: Option + ) -> Result, mmr::Error> { + Mmr::generate_ancestry_proof(prev_best_block, best_known_block_number) + } + + fn verify_ancestry_proof( + ancestry_proof: sp_mmr_primitives::AncestryProof, + ) -> Result<(), mmr::Error> { + Mmr::verify_ancestry_proof(ancestry_proof) + } } impl fg_primitives::GrandpaApi for Runtime { diff --git a/polkadot/runtime/test-runtime/src/lib.rs b/polkadot/runtime/test-runtime/src/lib.rs index 9eb0fcca6678b..ad6b1c90345aa 100644 --- a/polkadot/runtime/test-runtime/src/lib.rs +++ b/polkadot/runtime/test-runtime/src/lib.rs @@ -1002,7 +1002,7 @@ sp_api::impl_runtime_apis! { } } - impl beefy_primitives::BeefyApi for Runtime { + impl beefy_primitives::BeefyApi for Runtime { fn beefy_genesis() -> Option { // dummy implementation due to lack of BEEFY pallet. None @@ -1013,8 +1013,8 @@ sp_api::impl_runtime_apis! { None } - fn submit_report_equivocation_unsigned_extrinsic( - _equivocation_proof: beefy_primitives::DoubleVotingProof< + fn submit_report_vote_equivocation_unsigned_extrinsic( + _vote_equivocation_proof: beefy_primitives::DoubleVotingProof< BlockNumber, BeefyId, BeefySignature, @@ -1024,6 +1024,13 @@ sp_api::impl_runtime_apis! { None } + fn submit_report_fork_equivocation_unsigned_extrinsic( + _fork_equivocation_proof: beefy_primitives::ForkEquivocationProof, + _key_owner_proofs: Vec, + ) -> Option<()> { + None + } + fn generate_key_ownership_proof( _set_id: beefy_primitives::ValidatorSetId, _authority_id: BeefyId, @@ -1061,6 +1068,19 @@ sp_api::impl_runtime_apis! { ) -> Result<(), mmr::Error> { Err(mmr::Error::PalletNotIncluded) } + + fn generate_ancestry_proof( + _prev_best_block: BlockNumber, + _best_known_block_number: Option + ) -> Result, mmr::Error> { + Err(mmr::Error::PalletNotIncluded) + } + + fn verify_ancestry_proof( + _ancestry_proof: mmr::AncestryProof, + ) -> Result<(), mmr::Error> { + Err(mmr::Error::PalletNotIncluded) + } } impl fg_primitives::GrandpaApi for Runtime { diff --git a/polkadot/runtime/westend/src/lib.rs b/polkadot/runtime/westend/src/lib.rs index b62c6d08201c4..0ba424da7ee80 100644 --- a/polkadot/runtime/westend/src/lib.rs +++ b/polkadot/runtime/westend/src/lib.rs @@ -326,6 +326,7 @@ impl pallet_beefy::Config for Runtime { type MaxNominators = MaxNominators; type MaxSetIdSessionEntries = BeefySetIdSessionEntries; type OnNewValidatorSet = BeefyMmrLeaf; + type CheckForkEquivocationProof = BeefyMmrLeaf; type WeightInfo = (); type KeyOwnerProof = sp_session::MembershipProof; type EquivocationReportSystem = @@ -1964,7 +1965,8 @@ sp_api::impl_runtime_apis! { } } - impl beefy_primitives::BeefyApi for Runtime { + #[api_version(4)] + impl beefy_primitives::BeefyApi for Runtime { fn beefy_genesis() -> Option { pallet_beefy::GenesisBlock::::get() } @@ -1973,8 +1975,8 @@ sp_api::impl_runtime_apis! { Beefy::validator_set() } - fn submit_report_equivocation_unsigned_extrinsic( - equivocation_proof: beefy_primitives::DoubleVotingProof< + fn submit_report_vote_equivocation_unsigned_extrinsic( + vote_equivocation_proof: beefy_primitives::DoubleVotingProof< BlockNumber, BeefyId, BeefySignature, @@ -1983,12 +1985,22 @@ sp_api::impl_runtime_apis! { ) -> Option<()> { let key_owner_proof = key_owner_proof.decode()?; - Beefy::submit_unsigned_equivocation_report( - equivocation_proof, + Beefy::submit_unsigned_vote_equivocation_report( + vote_equivocation_proof, key_owner_proof, ) } + fn submit_report_fork_equivocation_unsigned_extrinsic( + fork_equivocation_proof: beefy_primitives::ForkEquivocationProof, + key_owner_proofs: Vec, + ) -> Option<()> { + Beefy::submit_unsigned_fork_equivocation_report( + fork_equivocation_proof, + key_owner_proofs, + ) + } + fn generate_key_ownership_proof( _set_id: beefy_primitives::ValidatorSetId, authority_id: BeefyId, @@ -2045,6 +2057,19 @@ sp_api::impl_runtime_apis! { let nodes = leaves.into_iter().map(|leaf|mmr::DataOrHash::Data(leaf.into_opaque_leaf())).collect(); pallet_mmr::verify_leaves_proof::(root, nodes, proof) } + + fn generate_ancestry_proof( + prev_best_block: BlockNumber, + best_known_block_number: Option + ) -> Result, mmr::Error> { + Mmr::generate_ancestry_proof(prev_best_block, best_known_block_number) + } + + fn verify_ancestry_proof( + ancestry_proof: sp_mmr_primitives::AncestryProof, + ) -> Result<(), mmr::Error> { + Mmr::verify_ancestry_proof(ancestry_proof) + } } impl pallet_beefy_mmr::BeefyMmrApi for RuntimeApi { diff --git a/substrate/bin/node/runtime/src/lib.rs b/substrate/bin/node/runtime/src/lib.rs index 017ee9100f9e5..3f9805a1acbda 100644 --- a/substrate/bin/node/runtime/src/lib.rs +++ b/substrate/bin/node/runtime/src/lib.rs @@ -2574,6 +2574,7 @@ impl pallet_beefy::Config for Runtime { type MaxNominators = ConstU32<0>; type MaxSetIdSessionEntries = BeefySetIdSessionEntries; type OnNewValidatorSet = MmrLeaf; + type CheckForkEquivocationProof = MmrLeaf; type WeightInfo = (); type KeyOwnerProof = >::Proof; type EquivocationReportSystem = @@ -3042,8 +3043,8 @@ impl_runtime_apis! { } } - #[api_version(3)] - impl sp_consensus_beefy::BeefyApi for Runtime { + #[api_version(4)] + impl sp_consensus_beefy::BeefyApi for Runtime { fn beefy_genesis() -> Option { pallet_beefy::GenesisBlock::::get() } @@ -3052,8 +3053,8 @@ impl_runtime_apis! { Beefy::validator_set() } - fn submit_report_equivocation_unsigned_extrinsic( - equivocation_proof: sp_consensus_beefy::DoubleVotingProof< + fn submit_report_vote_equivocation_unsigned_extrinsic( + vote_equivocation_proof: sp_consensus_beefy::DoubleVotingProof< BlockNumber, BeefyId, BeefySignature, @@ -3062,12 +3063,22 @@ impl_runtime_apis! { ) -> Option<()> { let key_owner_proof = key_owner_proof.decode()?; - Beefy::submit_unsigned_equivocation_report( - equivocation_proof, + Beefy::submit_unsigned_vote_equivocation_report( + vote_equivocation_proof, key_owner_proof, ) } + fn submit_report_fork_equivocation_unsigned_extrinsic( + fork_equivocation_proof: sp_consensus_beefy::ForkEquivocationProof, + key_owner_proofs: Vec, + ) -> Option<()> { + Beefy::submit_unsigned_fork_equivocation_report( + fork_equivocation_proof, + key_owner_proofs, + ) + } + fn generate_key_ownership_proof( _set_id: sp_consensus_beefy::ValidatorSetId, authority_id: BeefyId, @@ -3126,6 +3137,19 @@ impl_runtime_apis! { let nodes = leaves.into_iter().map(|leaf|mmr::DataOrHash::Data(leaf.into_opaque_leaf())).collect(); pallet_mmr::verify_leaves_proof::(root, nodes, proof) } + + fn generate_ancestry_proof( + prev_best_block: BlockNumber, + best_known_block_number: Option + ) -> Result, mmr::Error> { + Mmr::generate_ancestry_proof(prev_best_block, best_known_block_number) + } + + fn verify_ancestry_proof( + ancestry_proof: mmr::AncestryProof, + ) -> Result<(), mmr::Error> { + Mmr::verify_ancestry_proof(ancestry_proof) + } } impl sp_mixnet::runtime_api::MixnetApi for Runtime { diff --git a/substrate/client/consensus/beefy/Cargo.toml b/substrate/client/consensus/beefy/Cargo.toml index 193acbe52a1e8..3008ef63c5dfb 100644 --- a/substrate/client/consensus/beefy/Cargo.toml +++ b/substrate/client/consensus/beefy/Cargo.toml @@ -41,6 +41,7 @@ sp-crypto-hashing = { path = "../../../primitives/crypto/hashing" } sp-keystore = { path = "../../../primitives/keystore" } sp-runtime = { path = "../../../primitives/runtime" } tokio = "1.37" +sp-mmr-primitives = { path = "../../../primitives/merkle-mountain-range" } [dev-dependencies] @@ -50,7 +51,6 @@ sc-block-builder = { path = "../../block-builder" } sc-network-test = { path = "../../network/test" } sp-consensus-grandpa = { path = "../../../primitives/consensus/grandpa" } sp-keyring = { path = "../../../primitives/keyring" } -sp-mmr-primitives = { path = "../../../primitives/merkle-mountain-range" } sp-tracing = { path = "../../../primitives/tracing" } substrate-test-runtime-client = { path = "../../../test-utils/runtime/client" } diff --git a/substrate/client/consensus/beefy/src/communication/gossip.rs b/substrate/client/consensus/beefy/src/communication/gossip.rs index 947fe13856f47..08f01f5997410 100644 --- a/substrate/client/consensus/beefy/src/communication/gossip.rs +++ b/substrate/client/consensus/beefy/src/communication/gossip.rs @@ -26,10 +26,13 @@ use sp_runtime::traits::{Block, Hash, Header, NumberFor}; use codec::{Decode, DecodeAll, Encode}; use log::{debug, trace}; use parking_lot::{Mutex, RwLock}; +use sc_client_api::Backend; +use sp_api::ProvideRuntimeApi; use wasm_timer::Instant; use crate::{ communication::{benefit, cost, peers::KnownPeers}, + fisherman::Fisherman, justification::{ proof_block_num_and_set_id, verify_with_validator_set, BeefyVersionedFinalityProof, }, @@ -38,8 +41,9 @@ use crate::{ }; use sp_consensus_beefy::{ ecdsa_crypto::{AuthorityId, Signature}, - ValidatorSet, ValidatorSetId, VoteMessage, + BeefyApi, MmrRootHash, PayloadProvider, ValidatorSet, ValidatorSetId, VoteMessage, }; +use sp_mmr_primitives::MmrApi; // Timeout for rebroadcasting messages. #[cfg(not(test))] @@ -219,8 +223,11 @@ impl Filter { /// Allows messages for 'rounds >= last concluded' to flow, everything else gets /// rejected/expired. /// +/// Messages for active and expired rounds are validated for expected payloads and attempts +/// to create forks before head of GRANDPA are reported. +/// ///All messaging is handled in a single BEEFY global topic. -pub(crate) struct GossipValidator +pub(crate) struct GossipValidator where B: Block, { @@ -230,13 +237,22 @@ where next_rebroadcast: Mutex, known_peers: Arc>>, network: Arc, + pub(crate) fisherman: Arc>, } -impl GossipValidator +impl GossipValidator where B: Block, + BE: Backend + Send + Sync, + P: PayloadProvider + Send + Sync, + R: ProvideRuntimeApi, + R::Api: BeefyApi + MmrApi>, { - pub(crate) fn new(known_peers: Arc>>, network: Arc) -> Self { + pub(crate) fn new( + known_peers: Arc>>, + network: Arc, + fisherman: Arc>, + ) -> Self { Self { votes_topic: votes_topic::(), justifs_topic: proofs_topic::(), @@ -244,6 +260,7 @@ where next_rebroadcast: Mutex::new(Instant::now() + REBROADCAST_AFTER), known_peers, network, + fisherman, } } @@ -260,7 +277,7 @@ where } } -impl GossipValidator +impl GossipValidator where B: Block, N: NetworkPeers, @@ -271,21 +288,27 @@ where fn validate_vote( &self, - vote: VoteMessage, AuthorityId, Signature>, + vote: &VoteMessage, AuthorityId, Signature>, sender: &PeerId, ) -> Action { let round = vote.commitment.block_number; let set_id = vote.commitment.validator_set_id; self.known_peers.lock().note_vote_for(*sender, round); - // Verify general usefulness of the message. + // Verify general utility of the message. A vote is only useful if for an active round. // We are going to discard old votes right away (without verification). { let filter = self.gossip_filter.read(); match filter.consider_vote(round, set_id) { - Consider::RejectPast => return Action::Discard(cost::OUTDATED_MESSAGE), Consider::RejectFuture => return Action::Discard(cost::FUTURE_MESSAGE), + Consider::RejectPast => { + // TODO: maybe raise cost reputation when seeing votes that are intentional + // spam: votes that trigger fisherman reports, but don't go through either + // because signer is/was not authority or similar reasons. + // The idea is to more quickly disconnect neighbors which are attempting DoS. + return Action::Discard(cost::OUTDATED_MESSAGE) + }, // When we can't evaluate, it's our fault (e.g. filter not initialized yet), we // discard the vote without punishing or rewarding the sending peer. Consider::CannotEvaluate => return Action::DiscardNoReport, @@ -316,18 +339,27 @@ where fn validate_finality_proof( &self, - proof: BeefyVersionedFinalityProof, + proof: &BeefyVersionedFinalityProof, sender: &PeerId, ) -> Action { - let (round, set_id) = proof_block_num_and_set_id::(&proof); + let (round, set_id) = proof_block_num_and_set_id::(proof); self.known_peers.lock().note_vote_for(*sender, round); let action = { let guard = self.gossip_filter.read(); - // Verify general usefulness of the justification. + // Verify general utility of the justification. + // The justification is only useful if it is for an active round: voters should + // broadcast finality proofs once they have seen sufficient affirming votes to build a + // valid one. match guard.consider_finality_proof(round, set_id) { - Consider::RejectPast => return Action::Discard(cost::OUTDATED_MESSAGE), + Consider::RejectPast => { + // TODO: maybe raise cost reputation when seeing votes that are intentional + // spam: votes that trigger fisherman reports, but don't go through either + // because signer is/was not authority or similar reasons. + // The idea is to more quickly disconnect neighbors which are attempting DoS. + return Action::Discard(cost::OUTDATED_MESSAGE) + }, Consider::RejectFuture => return Action::Discard(cost::FUTURE_MESSAGE), // When we can't evaluate, it's our fault (e.g. filter not initialized yet), we // discard the proof without punishing or rewarding the sending peer. @@ -344,7 +376,7 @@ where .validator_set() .map(|validator_set| { if let Err((_, signatures_checked)) = - verify_with_validator_set::(round, validator_set, &proof) + verify_with_validator_set::(round, validator_set, proof) { debug!( target: LOG_TARGET, @@ -369,10 +401,14 @@ where } } -impl Validator for GossipValidator +impl Validator for GossipValidator where B: Block, N: NetworkPeers + Send + Sync, + BE: Backend + Send + Sync, + P: PayloadProvider + Send + Sync, + R: ProvideRuntimeApi + Send + Sync, + R::Api: BeefyApi + MmrApi>, { fn peer_disconnected(&self, _context: &mut dyn ValidatorContext, who: &PeerId) { self.known_peers.lock().remove(who); @@ -385,9 +421,28 @@ where mut data: &[u8], ) -> ValidationResult { let raw = data; + // Have fisherman check the vote or proof for fork equivocations regardless of + // the action on it: + // 1. We check votes/proofs on past rounds and active rounds since they might equivocate + // against grandpa-finalized state. + // 2. We check votes/proofs on future non-active rounds since these should not have been + // voted on yet (from the client's perspective). In case the block is not even in the + // client's history, but is in fact already finalized, the resulting equivocation report + // will be ineffectual. + // The check is best-effort and ignores errors such as state pruned. Accepted votes are also + // checked against vote equivocations in + // `sc_consensus_beefy::worker::BeefyWorker::handle_vote` let action = match GossipMessage::::decode_all(&mut data) { - Ok(GossipMessage::Vote(msg)) => self.validate_vote(msg, sender), - Ok(GossipMessage::FinalityProof(proof)) => self.validate_finality_proof(proof, sender), + Ok(GossipMessage::Vote(msg)) => { + let action = self.validate_vote(&msg, sender); + let _ = self.fisherman.check_vote(msg); + action + }, + Ok(GossipMessage::FinalityProof(proof)) => { + let action = self.validate_finality_proof(&proof, sender); + let _ = self.fisherman.check_proof(proof); + action + }, Err(e) => { debug!(target: LOG_TARGET, "Error decoding message: {}", e); let bytes = raw.len().min(i32::MAX as usize) as i32; @@ -486,8 +541,12 @@ where #[cfg(test)] pub(crate) mod tests { use super::*; - use crate::{communication::peers::PeerReport, keystore::BeefyKeystore}; - use sc_network_test::Block; + use crate::{ + communication::peers::PeerReport, + keystore::BeefyKeystore, + tests::{create_fisherman, BeefyTestNet, TestApi}, + }; + use sc_network_test::{Block, TestNetFactory}; use sp_application_crypto::key_types::BEEFY as BEEFY_KEY_TYPE; use sp_consensus_beefy::{ ecdsa_crypto::Signature, known_payloads, test_utils::Keyring, Commitment, MmrRootHash, @@ -650,16 +709,24 @@ pub(crate) mod tests { BeefyVersionedFinalityProof::::V1(SignedCommitment { commitment, signatures }) } - #[test] - fn should_validate_messages() { - let keys = vec![Keyring::::Alice.public()]; + #[tokio::test] + async fn should_validate_messages() { + let keyring = Keyring::::Alice; + let keys = vec![keyring.public()]; let validator_set = ValidatorSet::::new(keys.clone(), 0).unwrap(); let (network, mut report_stream) = TestNetwork::new(); - let gv = GossipValidator::::new( + let api = TestApi::new(0, &validator_set, MmrRootHash::repeat_byte(0xbf)); + let mut net = BeefyTestNet::new(1); + let backend = net.peer(0).client().as_backend(); + let fisherman = + Arc::new(create_fisherman(&keyring, Arc::new(api.clone()), backend.clone())); + + let gv = GossipValidator::new( Arc::new(Mutex::new(KnownPeers::new())), Arc::new(network), + fisherman, ); let sender = PeerId::random(); let mut context = TestContext; @@ -769,16 +836,25 @@ pub(crate) mod tests { assert_eq!(report_stream.try_next().unwrap().unwrap(), expected_report); } - #[test] - fn messages_allowed_and_expired() { - let keys = vec![Keyring::Alice.public()]; + #[tokio::test] + async fn messages_allowed_and_expired() { + let keyring = Keyring::Alice; + let keys = vec![keyring.public()]; let validator_set = ValidatorSet::::new(keys.clone(), 0).unwrap(); - let gv = GossipValidator::::new( + + let api = TestApi::new(0, &validator_set, MmrRootHash::repeat_byte(0xbf)); + let mut net = BeefyTestNet::new(1); + let backend = net.peer(0).client().as_backend(); + let fisherman = + Arc::new(create_fisherman(&keyring, Arc::new(api.clone()), backend.clone())); + + let gv = GossipValidator::new( Arc::new(Mutex::new(KnownPeers::new())), Arc::new(TestNetwork::new().0), + fisherman, ); gv.update_filter(GossipFilterCfg { start: 0, end: 10, validator_set: &validator_set }); - let sender = sc_network_types::PeerId::random(); + let sender = PeerId::random(); let topic = Default::default(); let intent = MessageIntent::Broadcast; @@ -849,16 +925,25 @@ pub(crate) mod tests { assert!(!expired(topic, &mut encoded_proof)); } - #[test] - fn messages_rebroadcast() { - let keys = vec![Keyring::Alice.public()]; + #[tokio::test] + async fn messages_rebroadcast() { + let keyring = Keyring::Alice; + let keys = vec![keyring.public()]; let validator_set = ValidatorSet::::new(keys.clone(), 0).unwrap(); - let gv = GossipValidator::::new( + + let api = TestApi::new(0, &validator_set, MmrRootHash::repeat_byte(0xbf)); + let mut net = BeefyTestNet::new(1); + let backend = net.peer(0).client().as_backend(); + let fisherman = + Arc::new(create_fisherman(&keyring, Arc::new(api.clone()), backend.clone())); + + let gv = GossipValidator::new( Arc::new(Mutex::new(KnownPeers::new())), Arc::new(TestNetwork::new().0), + fisherman, ); gv.update_filter(GossipFilterCfg { start: 0, end: 10, validator_set: &validator_set }); - let sender = sc_network_types::PeerId::random(); + let sender = PeerId::random(); let topic = Default::default(); let vote = dummy_vote(1); diff --git a/substrate/client/consensus/beefy/src/communication/mod.rs b/substrate/client/consensus/beefy/src/communication/mod.rs index 3c93368be3635..70fca0cfa8ed1 100644 --- a/substrate/client/consensus/beefy/src/communication/mod.rs +++ b/substrate/client/consensus/beefy/src/communication/mod.rs @@ -103,6 +103,8 @@ mod cost { pub(super) const UNKNOWN_VOTER: Rep = Rep::new(-150, "BEEFY: Unknown voter"); // Message containing invalid proof. pub(super) const INVALID_PROOF: Rep = Rep::new(-5000, "BEEFY: Invalid commit"); + // Message containing equivocating payload. + pub(super) const EQUIVOCATION: Rep = Rep::new(-30000, "BEEFY: Equivocation detected"); // Reputation cost per signature checked for invalid proof. pub(super) const PER_SIGNATURE_CHECKED: i32 = -25; // Reputation cost per byte for un-decodable message. diff --git a/substrate/client/consensus/beefy/src/communication/request_response/outgoing_requests_engine.rs b/substrate/client/consensus/beefy/src/communication/request_response/outgoing_requests_engine.rs index 2ab0729609008..0e7596905ca54 100644 --- a/substrate/client/consensus/beefy/src/communication/request_response/outgoing_requests_engine.rs +++ b/substrate/client/consensus/beefy/src/communication/request_response/outgoing_requests_engine.rs @@ -22,12 +22,16 @@ use codec::Encode; use futures::channel::{oneshot, oneshot::Canceled}; use log::{debug, warn}; use parking_lot::Mutex; +use sc_client_api::Backend; use sc_network::{ request_responses::{IfDisconnected, RequestFailure}, NetworkRequest, ProtocolName, }; use sc_network_types::PeerId; -use sp_consensus_beefy::{ecdsa_crypto::AuthorityId, ValidatorSet}; +use sp_api::ProvideRuntimeApi; +use sp_consensus_beefy::{ + ecdsa_crypto::AuthorityId, BeefyApi, MmrRootHash, PayloadProvider, ValidatorSet, +}; use sp_runtime::traits::{Block, NumberFor}; use std::{collections::VecDeque, result::Result, sync::Arc}; @@ -37,11 +41,13 @@ use crate::{ peers::PeerReport, request_response::{Error, JustificationRequest, BEEFY_SYNC_LOG_TARGET}, }, + fisherman::Fisherman, justification::{decode_and_verify_finality_proof, BeefyVersionedFinalityProof}, metric_inc, metrics::{register_metrics, OnDemandOutgoingRequestsMetrics}, KnownPeers, }; +use sp_mmr_primitives::MmrApi; /// Response type received from network. type Response = Result<(Vec, ProtocolName), RequestFailure>; @@ -69,7 +75,7 @@ pub(crate) enum ResponseInfo { PeerReport(PeerReport), } -pub struct OnDemandJustificationsEngine { +pub struct OnDemandJustificationsEngine { network: Arc, protocol_name: ProtocolName, @@ -78,14 +84,24 @@ pub struct OnDemandJustificationsEngine { state: State, metrics: Option, + + fisherman: Arc>, } -impl OnDemandJustificationsEngine { +impl OnDemandJustificationsEngine +where + B: Block, + BE: Backend + Send + Sync, + P: PayloadProvider + Send + Sync, + R: ProvideRuntimeApi + Send + Sync, + R::Api: BeefyApi + MmrApi>, +{ pub fn new( network: Arc, protocol_name: ProtocolName, live_peers: Arc>>, prometheus_registry: Option, + fisherman: Arc>, ) -> Self { let metrics = register_metrics(prometheus_registry); Self { @@ -95,6 +111,7 @@ impl OnDemandJustificationsEngine { peers_cache: VecDeque::new(), state: State::Idle, metrics, + fisherman, } } @@ -225,6 +242,21 @@ impl OnDemandJustificationsEngine { Error::InvalidResponse(PeerReport { who: *peer, cost_benefit: cost }) }) }) + .and_then(|proof| match self.fisherman.check_proof(proof.clone()) { + Ok(()) => Ok(proof), + Err(err) => { + metric_inc!(self.metrics, beefy_on_demand_justification_equivocation); + warn!( + target: BEEFY_SYNC_LOG_TARGET, + "🥩 for on demand justification #{:?}, peer {:?} responded with equivocation: {:?}", + req_info.block, peer, err + ); + Err(Error::InvalidResponse(PeerReport { + who: *peer, + cost_benefit: cost::EQUIVOCATION, + })) + }, + }) } pub(crate) async fn next(&mut self) -> ResponseInfo { diff --git a/substrate/client/consensus/beefy/src/fisherman.rs b/substrate/client/consensus/beefy/src/fisherman.rs index a2b4c8f945d1c..c79d7594fa28d 100644 --- a/substrate/client/consensus/beefy/src/fisherman.rs +++ b/substrate/client/consensus/beefy/src/fisherman.rs @@ -16,19 +16,25 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use crate::{error::Error, keystore::BeefyKeystore, round::Rounds, LOG_TARGET}; +use crate::{ + error::Error, expect_validator_set_nonblocking, justification::BeefyVersionedFinalityProof, + keystore::BeefyKeystore, round::Rounds, LOG_TARGET, +}; use log::{debug, error, warn}; use sc_client_api::Backend; use sp_api::ProvideRuntimeApi; use sp_blockchain::HeaderBackend; use sp_consensus_beefy::{ - check_equivocation_proof, + check_double_voting_proof, ecdsa_crypto::{AuthorityId, Signature}, - BeefyApi, BeefySignatureHasher, DoubleVotingProof, OpaqueKeyOwnershipProof, ValidatorSetId, + BeefyApi, BeefyEquivocationProof, BeefySignatureHasher, DoubleVotingProof, + ForkEquivocationProof, KnownSignature, MmrHashing, MmrRootHash, OpaqueKeyOwnershipProof, + Payload, PayloadProvider, ValidatorSet, ValidatorSetId, VoteMessage, }; +use sp_mmr_primitives::{AncestryProof, MmrApi}; use sp_runtime::{ generic::BlockId, - traits::{Block, NumberFor}, + traits::{Block, Header, NumberFor}, }; use std::{marker::PhantomData, sync::Arc}; @@ -38,25 +44,43 @@ pub struct ProvedValidator<'a> { pub key_owner_proof: OpaqueKeyOwnershipProof, } -/// Helper used to check and report equivocations. -pub struct Fisherman { - backend: Arc, - runtime: Arc, - key_store: Arc>, +struct CanonicalHashHeaderPayload { + hash: B::Hash, + header: B::Header, + payload: Payload, +} - _phantom: PhantomData, +/// Helper wrapper used to check gossiped votes for (historical) equivocations, +/// and report any such protocol infringements. +pub(crate) struct Fisherman { + pub backend: Arc, + pub runtime: Arc, + pub key_store: Arc>, + pub payload_provider: P, + pub _phantom: PhantomData, } -impl, RuntimeApi: ProvideRuntimeApi> Fisherman +impl Fisherman where - RuntimeApi::Api: BeefyApi, + B: Block, + BE: Backend + Send + Sync, + P: PayloadProvider + Send + Sync, + RuntimeApi: ProvideRuntimeApi + Send + Sync, + RuntimeApi::Api: BeefyApi + MmrApi>, { pub fn new( backend: Arc, runtime: Arc, keystore: Arc>, + payload_provider: P, ) -> Self { - Self { backend, runtime, key_store: keystore, _phantom: Default::default() } + Self { + backend, + runtime, + key_store: keystore, + payload_provider, + _phantom: Default::default(), + } } fn prove_offenders<'a>( @@ -64,7 +88,7 @@ where at: BlockId, offender_ids: impl Iterator, validator_set_id: ValidatorSetId, - ) -> Result>, Error> { + ) -> Result>, Error> { let hash = match at { BlockId::Hash(hash) => hash, BlockId::Number(number) => self @@ -89,7 +113,10 @@ where offender_id.clone(), ) { Ok(Some(key_owner_proof)) => { - proved_offenders.push(ProvedValidator { id: offender_id, key_owner_proof }); + proved_offenders.push(crate::fisherman::ProvedValidator { + id: offender_id, + key_owner_proof, + }); }, Ok(None) => { debug!( @@ -126,7 +153,7 @@ where (active_rounds.validators(), active_rounds.validator_set_id()); let offender_id = proof.offender_id(); - if !check_equivocation_proof::<_, _, BeefySignatureHasher>(&proof) { + if !check_double_voting_proof::<_, _, BeefySignatureHasher>(&proof) { debug!(target: LOG_TARGET, "🥩 Skipping report for bad equivocation {:?}", proof); return Ok(()) } @@ -146,10 +173,10 @@ where // submit equivocation report at **best** block let best_block_hash = self.backend.blockchain().info().best_hash; - for ProvedValidator { key_owner_proof, .. } in key_owner_proofs { + for crate::fisherman::ProvedValidator { key_owner_proof, .. } in key_owner_proofs { self.runtime .runtime_api() - .submit_report_equivocation_unsigned_extrinsic( + .submit_report_vote_equivocation_unsigned_extrinsic( best_block_hash, proof.clone(), key_owner_proof, @@ -159,4 +186,297 @@ where Ok(()) } + + fn canonical_hash_header_payload( + &self, + number: NumberFor, + ) -> Result, Error> { + // This should be un-ambiguous since `number` should be finalized for + // incoming votes and proofs. + let hash = self + .backend + .blockchain() + .expect_block_hash_from_id(&BlockId::Number(number)) + .map_err(|e| Error::Backend(e.to_string()))?; + let header = self + .backend + .blockchain() + .expect_header(hash) + .map_err(|e| Error::Backend(e.to_string()))?; + self.payload_provider + .payload(&header) + .map(|payload| CanonicalHashHeaderPayload { hash, header, payload }) + .ok_or_else(|| Error::Backend("BEEFY Payload not found".into())) + } + + fn active_validator_set_at( + &self, + block_hash: <::Header as Header>::Hash, + ) -> Result, Error> { + let header = self + .backend + .blockchain() + .expect_header(block_hash) + .map_err(|e| Error::Backend(e.to_string()))?; + expect_validator_set_nonblocking(&*self.runtime, &*self.backend, &header) + .map_err(|e| Error::Backend(e.to_string())) + } + + pub(crate) fn report_fork_equivocation( + &self, + proof: ForkEquivocationProof, + ) -> Result { + let best_block_number = self.backend.blockchain().info().best_number; + let best_block_hash = self.backend.blockchain().info().best_hash; + + // if the commitment is for a block number exceeding our best block number, we assume the + // equivocators are part of the current validator set, hence we use the validator set at the + // best block + let canonical_commitment_block_hash = if best_block_number < proof.commitment.block_number { + best_block_hash + } else { + self.backend + .blockchain() + .expect_block_hash_from_id(&BlockId::Number(proof.commitment.block_number)) + .map_err(|e| Error::Backend(e.to_string()))? + }; + + let validator_set = self.active_validator_set_at(canonical_commitment_block_hash)?; + let set_id = validator_set.id(); + + let best_mmr_root = self + .runtime + .runtime_api() + .mmr_root(best_block_hash) + .map_err(|e| Error::RuntimeApi(e))? + .map_err(|e| Error::Backend(e.to_string()))?; + + // if this errors, mmr has not been instantiated yet, hence the pallet is not active yet and + // we should not report equivocations + let leaf_count = self + .runtime + .runtime_api() + .mmr_leaf_count(best_block_hash) + .map_err(|e| Error::RuntimeApi(e))? + .map_err(|e| Error::Backend(e.to_string()))?; + let first_mmr_block_num = sp_mmr_primitives::utils::first_mmr_block_num::( + best_block_number, + leaf_count, + ) + .map_err(|e| Error::Backend(e.to_string()))?; + + if proof.commitment.validator_set_id != set_id || + !proof.check::( + best_mmr_root, + leaf_count, + &canonical_commitment_block_hash, + first_mmr_block_num, + best_block_number, + ) { + debug!(target: LOG_TARGET, "🥩 Skip report for bad invalid fork proof {:?}", proof); + return Ok(false) + } + + let offender_ids = proof.offender_ids(); + if let Some(local_id) = self.key_store.authority_id(validator_set.validators()) { + if offender_ids.contains(&&local_id) { + warn!(target: LOG_TARGET, "🥩 Skip equivocation report for own equivocation"); + return Ok(false) + } + } + + let runtime_api = self.runtime.runtime_api(); + + let mut filtered_signatures = Vec::new(); + // generate key ownership proof at that block + let key_owner_proofs: Vec<_> = offender_ids + .iter() + .cloned() + .filter_map(|id| { + match runtime_api.generate_key_ownership_proof( + canonical_commitment_block_hash, + set_id, + id.clone(), + ) { + Ok(Some(proof)) => Some(Ok(proof)), + Ok(None) => { + warn!( + target: LOG_TARGET, + "🥩 Invalid fork vote offender not part of the authority set." + ); + // if signatory is not part of the authority set, we ignore the signatory + filtered_signatures.push(id); + None + }, + Err(e) => { + warn!(target: LOG_TARGET, + "🥩 Failed to generate key ownership proof for {:?}: {:?}", id, e); + // if a key ownership proof couldn't be generated for signatory, we ignore + // the signatory + filtered_signatures.push(id); + None + }, + } + }) + .collect::>()?; + + if key_owner_proofs.len() > 0 { + // filter out the signatories that a key ownership proof could not be generated for + let proof = ForkEquivocationProof { + signatures: proof + .signatures + .clone() + .into_iter() + .filter(|signature| !filtered_signatures.contains(&&signature.validator_id)) + .collect(), + ..proof + }; + // submit invalid fork vote report at **best** block + runtime_api + .submit_report_fork_equivocation_unsigned_extrinsic( + best_block_hash, + proof, + key_owner_proofs, + ) + .map_err(Error::RuntimeApi)?; + Ok(true) + } else { + Ok(false) + } + } + + /// Generates an ancestry proof for the given ancestoring block's mmr root. + fn try_generate_ancestry_proof( + &self, + best_block_hash: B::Hash, + prev_block_num: NumberFor, + ) -> Option> { + match self.runtime.runtime_api().generate_ancestry_proof( + best_block_hash, + prev_block_num, + None, + ) { + Ok(Ok(ancestry_proof)) => Some(ancestry_proof), + Ok(Err(e)) => { + debug!(target: LOG_TARGET, "🥩 Failed to generate ancestry proof: {:?}", e); + None + }, + Err(e) => { + debug!(target: LOG_TARGET, "🥩 Failed to generate ancestry proof: {:?}", e); + None + }, + } + } + + /// Check `vote` for contained block against canonical payload. If an equivocation is detected, + /// this also reports it. + pub(crate) fn check_vote( + &self, + vote: VoteMessage, AuthorityId, Signature>, + ) -> Result<(), Error> { + let number = vote.commitment.block_number; + // if the vote's commitment has not been signed by the purported signer, we ignore it + if !sp_consensus_beefy::check_commitment_signature::<_, _, BeefySignatureHasher>( + &vote.commitment, + &vote.id, + &vote.signature, + ) { + return Ok(()) + }; + // if the vote is for a block number exceeding our best block number, there shouldn't even + // be a payload to sign yet, hence we assume it is an equivocation and report it + if number > self.backend.blockchain().info().best_number { + let proof = ForkEquivocationProof { + commitment: vote.commitment, + signatures: vec![KnownSignature { + validator_id: vote.id, + signature: vote.signature, + }], + canonical_header: None, + ancestry_proof: None, + }; + self.report_fork_equivocation(proof)?; + } else { + let canonical_hhp = self.canonical_hash_header_payload(number)?; + if vote.commitment.payload != canonical_hhp.payload { + let ancestry_proof = self.try_generate_ancestry_proof( + self.backend.blockchain().info().finalized_hash, + number, + ); + let proof = ForkEquivocationProof { + commitment: vote.commitment, + signatures: vec![KnownSignature { + validator_id: vote.id, + signature: vote.signature, + }], + canonical_header: Some(canonical_hhp.header), + ancestry_proof, + }; + self.report_fork_equivocation(proof)?; + } + } + Ok(()) + } + + /// Check `proof` for contained block against canonical payload. If an equivocation is detected, + /// this also reports it. + pub fn check_proof(&self, proof: BeefyVersionedFinalityProof) -> Result<(), Error> { + let signed_commitment = match proof { + BeefyVersionedFinalityProof::::V1(signed_commitment) => signed_commitment, + }; + let commitment = &signed_commitment.commitment; + + // if the vote is for a block number exceeding our best block number, there shouldn't even + // be a payload to sign yet, hence we assume it is an equivocation and report it + let (validator_set, canonical_header, ancestry_proof) = + if commitment.block_number > self.backend.blockchain().info().best_number { + // if block number is in the future, we use the latest validator set + // as the assumed signatories (note: this assumption is fragile and can possibly be + // improved upon) + let best_hash = self.backend.blockchain().info().best_hash; + let validator_set = self.active_validator_set_at(best_hash)?; + + (validator_set, None, None) + } else { + let canonical_hhp = self.canonical_hash_header_payload(commitment.block_number)?; + if commitment.payload == canonical_hhp.payload { + // The commitment is valid + return Ok(()) + } + + let ancestry_proof = self.try_generate_ancestry_proof( + self.backend.blockchain().info().finalized_hash, + commitment.block_number, + ); + let validator_set = self.active_validator_set_at(canonical_hhp.hash)?; + (validator_set, Some(canonical_hhp.header), ancestry_proof) + }; + + // let finality_proof = BeefyVersionedFinalityProof::::V1(signed_commitment); + let signatures: Vec<_> = + match crate::justification::verify_signed_commitment_with_validator_set::( + commitment.block_number, + &validator_set, + &signed_commitment, + ) { + Ok(signatures_refs) => + signatures_refs.into_iter().map(|signature| signature.to_owned()).collect(), + Err(_) => { + // invalid proof + return Ok(()) + }, + }; + + if signatures.len() > 0 { + let proof = ForkEquivocationProof { + commitment: signed_commitment.commitment, + signatures, + canonical_header, + ancestry_proof, + }; + self.report_fork_equivocation(proof)?; + } + + Ok(()) + } } diff --git a/substrate/client/consensus/beefy/src/import.rs b/substrate/client/consensus/beefy/src/import.rs index ed8ed68c4e8d0..8f89a55a16bf0 100644 --- a/substrate/client/consensus/beefy/src/import.rs +++ b/substrate/client/consensus/beefy/src/import.rs @@ -22,7 +22,7 @@ use log::debug; use sp_api::ProvideRuntimeApi; use sp_consensus::Error as ConsensusError; -use sp_consensus_beefy::{ecdsa_crypto::AuthorityId, BeefyApi, BEEFY_ENGINE_ID}; +use sp_consensus_beefy::{ecdsa_crypto::AuthorityId, BeefyApi, MmrRootHash, BEEFY_ENGINE_ID}; use sp_runtime::{ traits::{Block as BlockT, Header as HeaderT, NumberFor}, EncodedJustification, @@ -83,7 +83,7 @@ where Block: BlockT, BE: Backend, Runtime: ProvideRuntimeApi, - Runtime::Api: BeefyApi + Send, + Runtime::Api: BeefyApi + Send, { fn decode_and_verify( &self, @@ -120,7 +120,7 @@ where BE: Backend, I: BlockImport + Send + Sync, Runtime: ProvideRuntimeApi + Send + Sync, - Runtime::Api: BeefyApi, + Runtime::Api: BeefyApi, { type Error = ConsensusError; diff --git a/substrate/client/consensus/beefy/src/justification.rs b/substrate/client/consensus/beefy/src/justification.rs index 886368c9d7cb0..e791ed48ebb43 100644 --- a/substrate/client/consensus/beefy/src/justification.rs +++ b/substrate/client/consensus/beefy/src/justification.rs @@ -20,7 +20,8 @@ use codec::DecodeAll; use sp_consensus::Error as ConsensusError; use sp_consensus_beefy::{ ecdsa_crypto::{AuthorityId, Signature}, - BeefySignatureHasher, KnownSignature, ValidatorSet, ValidatorSetId, VersionedFinalityProof, + BeefySignatureHasher, KnownSignature, SignedCommitment, ValidatorSet, ValidatorSetId, + VersionedFinalityProof, }; use sp_runtime::traits::{Block as BlockT, NumberFor}; @@ -36,7 +37,7 @@ pub(crate) fn proof_block_num_and_set_id( } } -/// Decode and verify a Beefy FinalityProof. +/// Decode and verify a BEEFY FinalityProof. pub(crate) fn decode_and_verify_finality_proof( encoded: &[u8], target_number: NumberFor, @@ -48,29 +49,35 @@ pub(crate) fn decode_and_verify_finality_proof( Ok(proof) } -/// Verify the Beefy finality proof against the validator set at the block it was generated. +/// Verify the BEEFY signed commitment against the validator set at the block it was generated. +pub(crate) fn verify_signed_commitment_with_validator_set<'a, Block: BlockT>( + target_number: NumberFor, + validator_set: &'a ValidatorSet, + signed_commitment: &'a SignedCommitment, Signature>, +) -> Result>, (ConsensusError, u32)> { + let signatures = signed_commitment + .verify_signatures::<_, BeefySignatureHasher>(target_number, validator_set) + .map_err(|checked_signatures| (ConsensusError::InvalidJustification, checked_signatures))?; + if signatures.len() >= crate::round::threshold(validator_set.len()) { + Ok(signatures) + } else { + Err((ConsensusError::InvalidJustification, signed_commitment.signature_count() as u32)) + } +} + +/// Verify the BEEFY finality proof against the validator set at the block it was generated. pub(crate) fn verify_with_validator_set<'a, Block: BlockT>( target_number: NumberFor, validator_set: &'a ValidatorSet, proof: &'a BeefyVersionedFinalityProof, ) -> Result>, (ConsensusError, u32)> { match proof { - VersionedFinalityProof::V1(signed_commitment) => { - let signatories = signed_commitment - .verify_signatures::<_, BeefySignatureHasher>(target_number, validator_set) - .map_err(|checked_signatures| { - (ConsensusError::InvalidJustification, checked_signatures) - })?; - - if signatories.len() >= crate::round::threshold(validator_set.len()) { - Ok(signatories) - } else { - Err(( - ConsensusError::InvalidJustification, - signed_commitment.signature_count() as u32, - )) - } - }, + VersionedFinalityProof::V1(signed_commitment) => + verify_signed_commitment_with_validator_set::( + target_number, + validator_set, + signed_commitment, + ), } } diff --git a/substrate/client/consensus/beefy/src/lib.rs b/substrate/client/consensus/beefy/src/lib.rs index 0e49839f0fd2d..a36521c9c13e8 100644 --- a/substrate/client/consensus/beefy/src/lib.rs +++ b/substrate/client/consensus/beefy/src/lib.rs @@ -29,6 +29,7 @@ use crate::{ }, error::Error, import::BeefyBlockImport, + keystore::BeefyKeystore, metrics::register_metrics, }; use futures::{stream::Fuse, FutureExt, StreamExt}; @@ -40,7 +41,9 @@ use sc_consensus::BlockImport; use sc_network::{NetworkRequest, NotificationService, ProtocolName}; use sc_network_gossip::{GossipEngine, Network as GossipNetwork, Syncing as GossipSyncing}; use sp_api::ProvideRuntimeApi; -use sp_blockchain::{Backend as BlockchainBackend, HeaderBackend}; +use sp_blockchain::{ + Backend as BlockchainBackend, Error as ClientError, HeaderBackend, Result as ClientResult, +}; use sp_consensus::{Error as ConsensusError, SyncOracle}; use sp_consensus_beefy::{ ecdsa_crypto::AuthorityId, BeefyApi, ConsensusLog, PayloadProvider, ValidatorSet, @@ -63,14 +66,13 @@ mod round; mod worker; pub mod communication; +mod fisherman; pub mod import; pub mod justification; use crate::{ communication::gossip::GossipValidator, - fisherman::Fisherman, justification::BeefyVersionedFinalityProof, - keystore::BeefyKeystore, metrics::VoterMetrics, round::Rounds, worker::{BeefyWorker, PersistedState}, @@ -78,9 +80,11 @@ use crate::{ pub use communication::beefy_protocol_name::{ gossip_protocol_name, justifications_protocol_name as justifs_protocol_name, }; +use fisherman::Fisherman; +use sp_consensus_beefy::MmrRootHash; +use sp_mmr_primitives::MmrApi; use sp_runtime::generic::OpaqueDigestItemId; -mod fisherman; #[cfg(test)] mod tests; @@ -151,7 +155,7 @@ where BE: Backend, I: BlockImport + Send + Sync, RuntimeApi: ProvideRuntimeApi + Send + Sync, - RuntimeApi::Api: BeefyApi, + RuntimeApi::Api: BeefyApi, { // Voter -> RPC links let (to_rpc_justif_sender, from_voter_justif_stream) = @@ -228,10 +232,10 @@ pub struct BeefyParams { /// Helper object holding BEEFY worker communication/gossip components. /// /// These are created once, but will be reused if worker is restarted/reinitialized. -pub(crate) struct BeefyComms { +pub(crate) struct BeefyComms { pub gossip_engine: GossipEngine, - pub gossip_validator: Arc>, - pub on_demand_justifications: OnDemandJustificationsEngine, + pub gossip_validator: Arc>, + pub on_demand_justifications: OnDemandJustificationsEngine, } /// Helper builder object for building [worker::BeefyWorker]. @@ -244,7 +248,7 @@ pub(crate) struct BeefyWorkerBuilder { // utilities backend: Arc, runtime: Arc, - key_store: BeefyKeystore, + key_store: Arc>, // voter metrics metrics: Option, persisted_state: PersistedState, @@ -255,7 +259,7 @@ where B: Block + codec::Codec, BE: Backend, R: ProvideRuntimeApi, - R::Api: BeefyApi, + R::Api: BeefyApi + MmrApi>, { /// This will wait for the chain to enable BEEFY (if not yet enabled) and also wait for the /// backend to sync all headers required by the voter to build a contiguous chain of mandatory @@ -263,13 +267,13 @@ where /// persisted state in AUX DB and latest chain information/progress. /// /// Returns a sane `BeefyWorkerBuilder` that can build the `BeefyWorker`. - pub async fn async_initialize( + pub async fn async_initialize + Send + Sync>( backend: Arc, runtime: Arc, - key_store: BeefyKeystore, + key_store: Arc>, metrics: Option, min_block_delta: u32, - gossip_validator: Arc>, + gossip_validator: Arc>, finality_notifications: &mut Fuse>, is_authority: bool, ) -> Result { @@ -301,19 +305,18 @@ where self, payload_provider: P, sync: Arc, - comms: BeefyComms, + comms: BeefyComms, links: BeefyVoterLinks, pending_justifications: BTreeMap, BeefyVersionedFinalityProof>, is_authority: bool, ) -> BeefyWorker { - let key_store = Arc::new(self.key_store); BeefyWorker { backend: self.backend.clone(), runtime: self.runtime.clone(), - key_store: key_store.clone(), + key_store: self.key_store.clone(), payload_provider, sync, - fisherman: Arc::new(Fisherman::new(self.backend, self.runtime, key_store)), + fisherman: comms.gossip_validator.fisherman.clone(), metrics: self.metrics, persisted_state: self.persisted_state, comms, @@ -486,11 +489,11 @@ pub async fn start_beefy_gadget( beefy_params: BeefyParams, ) where B: Block, - BE: Backend, + BE: Backend + 'static, C: Client + BlockBackend, P: PayloadProvider + Clone, - R: ProvideRuntimeApi, - R::Api: BeefyApi, + R: ProvideRuntimeApi + Send + Sync + 'static, + R::Api: BeefyApi + MmrApi>, N: GossipNetwork + NetworkRequest + Send + Sync + 'static, S: GossipSyncing + SyncOracle + 'static, { @@ -507,6 +510,7 @@ pub async fn start_beefy_gadget( mut on_demand_justifications_handler, is_authority, } = beefy_params; + let key_store: Arc> = Arc::new(key_store.into()); let BeefyNetworkParams { network, @@ -525,10 +529,16 @@ pub async fn start_beefy_gadget( let mut block_import_justif = links.from_block_import_justif_stream.subscribe(100_000).fuse(); let known_peers = Arc::new(Mutex::new(KnownPeers::new())); + let fisherman = Arc::new(Fisherman::new( + backend.clone(), + runtime.clone(), + key_store.clone(), + payload_provider.clone(), + )); // Default votes filter is to discard everything. // Validator is updated later with correct starting round and set id. let gossip_validator = - communication::gossip::GossipValidator::new(known_peers.clone(), network.clone()); + GossipValidator::new(known_peers.clone(), network.clone(), fisherman.clone()); let gossip_validator = Arc::new(gossip_validator); let gossip_engine = GossipEngine::new( network.clone(), @@ -546,6 +556,7 @@ pub async fn start_beefy_gadget( justifications_protocol_name.clone(), known_peers, prometheus_registry.clone(), + fisherman.clone(), ); let mut beefy_comms = BeefyComms { gossip_engine, gossip_validator, on_demand_justifications }; @@ -557,7 +568,7 @@ pub async fn start_beefy_gadget( builder_init_result = BeefyWorkerBuilder::async_initialize( backend.clone(), runtime.clone(), - key_store.clone().into(), + key_store.clone(), metrics.clone(), min_block_delta, beefy_comms.gossip_validator.clone(), @@ -658,7 +669,7 @@ async fn wait_for_runtime_pallet( where B: Block, R: ProvideRuntimeApi, - R::Api: BeefyApi, + R::Api: BeefyApi, { info!(target: LOG_TARGET, "🥩 BEEFY gadget waiting for BEEFY pallet to become available..."); loop { @@ -696,7 +707,7 @@ where B: Block, BE: Backend, R: ProvideRuntimeApi, - R::Api: BeefyApi, + R::Api: BeefyApi, { let blockchain = backend.blockchain(); // Walk up the chain looking for the validator set active at 'at_header'. Process both state and @@ -726,6 +737,49 @@ where } } +/// Provides validator set active `at_header`. It tries to get it from state, otherwise falls +/// back to walk up the chain looking the validator set enactment in header digests. +/// +/// Note: unlike `expect_validator_set` this function is non-blocking, but will therefore error if +/// a requested header is not available (yet). +fn expect_validator_set_nonblocking( + runtime: &R, + backend: &BE, + at_header: &B::Header, +) -> ClientResult> +where + B: Block, + BE: Backend, + R: ProvideRuntimeApi, + R::Api: BeefyApi, +{ + let blockchain = backend.blockchain(); + + // Walk up the chain looking for the validator set active at 'at_header'. Process both state and + // header digests. + debug!(target: LOG_TARGET, "🥩 Trying to find validator set active at header: {:?}", at_header); + let mut header = at_header.clone(); + loop { + debug!(target: LOG_TARGET, "🥩 Looking for auth set change at block number: {:?}", *header.number()); + if let Ok(Some(active)) = runtime.runtime_api().validator_set(header.hash()) { + return Ok(active) + } else { + match crate::find_authorities_change::(&header) { + Some(active) => return Ok(active), + // Move up the chain. Ultimately we'll get it from chain genesis state, or error out + // there. + None => match blockchain.header(*header.parent_hash())? { + Some(parent) => header = parent, + None => { + warn!(target: LOG_TARGET, "header {} not found", header.parent_hash()); + return Err(ClientError::MissingHeader(header.parent_hash().to_string())) + }, + }, + } + } + } +} + /// Scan the `header` digest log for a BEEFY validator set change. Return either the new /// validator set or `None` in case no validator set change has been signaled. pub(crate) fn find_authorities_change(header: &B::Header) -> Option> diff --git a/substrate/client/consensus/beefy/src/metrics.rs b/substrate/client/consensus/beefy/src/metrics.rs index ef3928d79faaa..8620a29c20278 100644 --- a/substrate/client/consensus/beefy/src/metrics.rs +++ b/substrate/client/consensus/beefy/src/metrics.rs @@ -236,6 +236,8 @@ pub struct OnDemandOutgoingRequestsMetrics { pub beefy_on_demand_justification_invalid_proof: Counter, /// Number of on-demand justification good proof pub beefy_on_demand_justification_good_proof: Counter, + /// Number of on-demand justification equivocation + pub beefy_on_demand_justification_equivocation: Counter, } impl PrometheusRegister for OnDemandOutgoingRequestsMetrics { @@ -277,6 +279,13 @@ impl PrometheusRegister for OnDemandOutgoingRequestsMetrics { )?, registry, )?, + beefy_on_demand_justification_equivocation: register( + Counter::new( + "substrate_beefy_on_demand_justification_equivocation", + "Number of on-demand justification equivocations", + )?, + registry, + )?, }) } } diff --git a/substrate/client/consensus/beefy/src/tests.rs b/substrate/client/consensus/beefy/src/tests.rs index 2bb145d660df0..b96b390d1346d 100644 --- a/substrate/client/consensus/beefy/src/tests.rs +++ b/substrate/client/consensus/beefy/src/tests.rs @@ -30,8 +30,10 @@ use crate::{ request_response::{on_demand_justifications_protocol_config, BeefyJustifsRequestHandler}, }, error::Error, + fisherman::Fisherman, gossip_protocol_name, justification::*, + keystore::BeefyKeystore, wait_for_runtime_pallet, worker::PersistedState, BeefyRPCLinks, BeefyVoterLinks, BeefyWorkerBuilder, KnownPeers, @@ -46,7 +48,7 @@ use sc_consensus::{ }; use sc_network::{config::RequestResponseConfig, ProtocolName}; use sc_network_test::{ - Block, BlockImportAdapter, FullPeerConfig, PassThroughVerifier, Peer, PeersClient, + Block, BlockImportAdapter, FullPeerConfig, Header, PassThroughVerifier, Peer, PeersClient, PeersFullClient, TestNetFactory, }; use sc_utils::notification::NotificationReceiver; @@ -59,13 +61,13 @@ use sp_consensus_beefy::{ known_payloads, mmr::{find_mmr_root_digest, MmrRootProvider}, test_utils::Keyring as BeefyKeyring, - BeefyApi, Commitment, ConsensusLog, DoubleVotingProof, MmrRootHash, OpaqueKeyOwnershipProof, - Payload, SignedCommitment, ValidatorSet, ValidatorSetId, VersionedFinalityProof, VoteMessage, - BEEFY_ENGINE_ID, + BeefyApi, Commitment, ConsensusLog, DoubleVotingProof, ForkEquivocationProof, MmrRootHash, + OpaqueKeyOwnershipProof, Payload, SignedCommitment, ValidatorSet, ValidatorSetId, + VersionedFinalityProof, VoteMessage, BEEFY_ENGINE_ID, }; use sp_core::H256; use sp_keystore::{testing::MemoryKeystore, Keystore, KeystorePtr}; -use sp_mmr_primitives::{Error as MmrError, MmrApi}; +use sp_mmr_primitives::{AncestryProof, Error as MmrError, LeafIndex, MmrApi}; use sp_runtime::{ codec::{Decode, Encode}, traits::{Header as HeaderT, NumberFor}, @@ -172,7 +174,7 @@ impl BeefyTestNet { // of hashes, otherwise indexing would be broken assert!(self.peer(0).client().as_backend().blockchain().hash(1).unwrap().is_none()); - // push genesis to make indexing human readable (index equals to block number) + // push genesis to make indexing human-readable (index equals to block number) all_hashes.push(self.peer(0).client().info().genesis_hash); let mut block_num: NumberFor = self.peer(0).client().info().best_number; @@ -258,8 +260,10 @@ pub(crate) struct TestApi { pub beefy_genesis: u64, pub validator_set: Option, pub mmr_root_hash: MmrRootHash, - pub reported_equivocations: + pub reported_vote_equivocations: Option, AuthorityId, Signature>>>>>, + pub reported_fork_equivocations: + Option>>>>, } impl TestApi { @@ -272,7 +276,8 @@ impl TestApi { beefy_genesis, validator_set: Some(validator_set.clone()), mmr_root_hash, - reported_equivocations: None, + reported_vote_equivocations: None, + reported_fork_equivocations: None, } } @@ -281,12 +286,14 @@ impl TestApi { beefy_genesis: 1, validator_set: Some(validator_set.clone()), mmr_root_hash: GOOD_MMR_ROOT, - reported_equivocations: None, + reported_vote_equivocations: None, + reported_fork_equivocations: None, } } pub fn allow_equivocations(&mut self) { - self.reported_equivocations = Some(Arc::new(Mutex::new(vec![]))); + self.reported_vote_equivocations = Some(Arc::new(Mutex::new(vec![]))); + self.reported_fork_equivocations = Some(Arc::new(Mutex::new(vec![]))); } } @@ -303,7 +310,7 @@ impl ProvideRuntimeApi for TestApi { } } sp_api::mock_impl_runtime_apis! { - impl BeefyApi for RuntimeApi { + impl BeefyApi for RuntimeApi { fn beefy_genesis() -> Option> { Some(self.inner.beefy_genesis) } @@ -312,16 +319,26 @@ sp_api::mock_impl_runtime_apis! { self.inner.validator_set.clone() } - fn submit_report_equivocation_unsigned_extrinsic( + fn submit_report_vote_equivocation_unsigned_extrinsic( proof: DoubleVotingProof, AuthorityId, Signature>, _dummy: OpaqueKeyOwnershipProof, ) -> Option<()> { - if let Some(equivocations_buf) = self.inner.reported_equivocations.as_ref() { + if let Some(equivocations_buf) = self.inner.reported_vote_equivocations.as_ref() { equivocations_buf.lock().push(proof); - None - } else { - panic!("Equivocations not expected, but following proof was reported: {:?}", proof); } + + None + } + + fn submit_report_fork_equivocation_unsigned_extrinsic( + proof: ForkEquivocationProof, + _dummy: Vec, + ) -> Option<()> { + if let Some(equivocations_buf) = self.inner.reported_fork_equivocations.as_ref() { + equivocations_buf.lock().push(proof); + } + + None } fn generate_key_ownership_proof( @@ -334,6 +351,22 @@ sp_api::mock_impl_runtime_apis! { fn mmr_root() -> Result { Ok(self.inner.mmr_root_hash) } + + fn mmr_leaf_count() -> Result { + Ok(0) + } + + fn generate_ancestry_proof( + _prev_best_block: NumberFor, + _best_known_number: Option>, + ) -> Result, MmrError> { + Ok(AncestryProof { + prev_peaks: vec![], + prev_leaf_count: 0, + leaf_count: 0, + items: vec![], + }) + } } } @@ -367,6 +400,25 @@ pub(crate) fn create_beefy_keystore(authority: &BeefyKeyring) -> Ke keystore.into() } +pub(crate) fn create_fisherman( + beefy_keyring: &BeefyKeyring, /* Assuming BeefyKeyring contains + * necessary keys for the Fisherman */ + api: Arc, + backend: Arc, +) -> Fisherman> { + let key_store: Arc> = + Arc::new(Some(create_beefy_keystore(beefy_keyring)).into()); + let payload_provider = MmrRootProvider::new(api.clone()); + + Fisherman { + backend, + key_store, + runtime: api.clone().into(), + payload_provider, + _phantom: PhantomData, + } +} + async fn voter_init_setup( net: &mut BeefyTestNet, finality: &mut futures::stream::Fuse>, @@ -396,8 +448,9 @@ fn initialize_beefy( min_block_delta: u32, ) -> impl Future where - API: ProvideRuntimeApi + Sync + Send, - API::Api: BeefyApi + MmrApi>, + API: ProvideRuntimeApi + Sync + Send + 'static, + API::Api: + BeefyApi + MmrApi>, { let tasks = FuturesUnordered::new(); @@ -1366,7 +1419,7 @@ async fn beefy_finalizing_after_pallet_genesis() { } #[tokio::test] -async fn beefy_reports_equivocations() { +async fn beefy_reports_vote_equivocations() { sp_tracing::try_init_simple(); let peers = [BeefyKeyring::Alice, BeefyKeyring::Bob, BeefyKeyring::Charlie]; @@ -1416,21 +1469,22 @@ async fn beefy_reports_equivocations() { // run for up to 5 seconds waiting for Alice's report of Bob/Bob_Prime equivocation. for wait_ms in [250, 500, 1250, 3000] { run_for(Duration::from_millis(wait_ms), &net).await; - if !api_alice.reported_equivocations.as_ref().unwrap().lock().is_empty() { + if !api_alice.reported_vote_equivocations.as_ref().unwrap().lock().is_empty() { break } } // Verify expected equivocation - let alice_reported_equivocations = api_alice.reported_equivocations.as_ref().unwrap().lock(); - assert_eq!(alice_reported_equivocations.len(), 1); - let equivocation_proof = alice_reported_equivocations.get(0).unwrap(); + let alice_reported_vote_equivocations = + api_alice.reported_vote_equivocations.as_ref().unwrap().lock(); + assert_eq!(alice_reported_vote_equivocations.len(), 1); + let equivocation_proof = alice_reported_vote_equivocations.get(0).unwrap(); assert_eq!(equivocation_proof.first.id, BeefyKeyring::Bob.public()); assert_eq!(equivocation_proof.first.commitment.block_number, 1); - // Verify neither Bob or Bob_Prime report themselves as equivocating. - assert!(api_bob.reported_equivocations.as_ref().unwrap().lock().is_empty()); - assert!(api_bob_prime.reported_equivocations.as_ref().unwrap().lock().is_empty()); + // Verify neither Bob nor Bob_Prime report themselves as equivocating. + assert!(api_bob.reported_vote_equivocations.as_ref().unwrap().lock().is_empty()); + assert!(api_bob_prime.reported_vote_equivocations.as_ref().unwrap().lock().is_empty()); // sanity verify no new blocks have been finalized by BEEFY streams_empty_after_timeout(best_blocks, &net, None).await; @@ -1438,7 +1492,7 @@ async fn beefy_reports_equivocations() { } #[tokio::test] -async fn gossipped_finality_proofs() { +async fn gossiped_finality_proofs() { sp_tracing::try_init_simple(); let validators = [BeefyKeyring::Alice, BeefyKeyring::Bob, BeefyKeyring::Charlie]; @@ -1449,13 +1503,16 @@ async fn gossipped_finality_proofs() { let min_block_delta = 1; let mut net = BeefyTestNet::new(3); + let backend = net.peer(0).client().as_backend(); let api = Arc::new(TestApi::with_validator_set(&validator_set)); let beefy_peers = peers.iter().enumerate().map(|(id, key)| (id, key, api.clone())).collect(); let charlie = &mut net.peers[2]; + let fisherman = Arc::new(create_fisherman(&BeefyKeyring::Alice, api, backend.clone())); let known_peers = Arc::new(Mutex::new(KnownPeers::::new())); // Charlie will run just the gossip engine and not the full voter. - let gossip_validator = GossipValidator::new(known_peers, Arc::new(TestNetwork::new().0)); + let gossip_validator = + GossipValidator::new(known_peers, Arc::new(TestNetwork::new().0), fisherman); let charlie_gossip_validator = Arc::new(gossip_validator); charlie_gossip_validator.update_filter(GossipFilterCfg:: { start: 1, @@ -1521,7 +1578,7 @@ async fn gossipped_finality_proofs() { // Simulate Charlie vote on #2 let header = net.lock().peer(2).client().as_client().expect_header(finalize).unwrap(); - let mmr_root = find_mmr_root_digest::(&header).unwrap(); + let mmr_root = find_mmr_root_digest::
(&header).unwrap(); let payload = Payload::from_single_entry(known_payloads::MMR_ROOT_ID, mmr_root.encode()); let commitment = Commitment { payload, block_number, validator_set_id: validator_set.id() }; let signature = sign_commitment(&BeefyKeyring::Charlie, &commitment); diff --git a/substrate/client/consensus/beefy/src/worker.rs b/substrate/client/consensus/beefy/src/worker.rs index cfbb3d63aea44..97f349c67ea8f 100644 --- a/substrate/client/consensus/beefy/src/worker.rs +++ b/substrate/client/consensus/beefy/src/worker.rs @@ -41,9 +41,10 @@ use sp_arithmetic::traits::{AtLeast32Bit, Saturating}; use sp_consensus::SyncOracle; use sp_consensus_beefy::{ ecdsa_crypto::{AuthorityId, Signature}, - BeefyApi, Commitment, DoubleVotingProof, PayloadProvider, ValidatorSet, VersionedFinalityProof, - VoteMessage, BEEFY_ENGINE_ID, + BeefyApi, Commitment, DoubleVotingProof, MmrRootHash, PayloadProvider, ValidatorSet, + VersionedFinalityProof, VoteMessage, BEEFY_ENGINE_ID, }; +use sp_mmr_primitives::MmrApi; use sp_runtime::{ generic::BlockId, traits::{Block, Header, NumberFor, Zero}, @@ -380,10 +381,10 @@ pub(crate) struct BeefyWorker { pub key_store: Arc>, pub payload_provider: P, pub sync: Arc, - pub fisherman: Arc>, + pub fisherman: Arc>, // communication (created once, but returned and reused if worker is restarted/reinitialized) - pub comms: BeefyComms, + pub comms: BeefyComms, // channels /// Links between the block importer, the background voter and the RPC layer. @@ -406,8 +407,8 @@ where BE: Backend, P: PayloadProvider, S: SyncOracle, - R: ProvideRuntimeApi, - R::Api: BeefyApi, + R: ProvideRuntimeApi + Send + Sync, + R::Api: BeefyApi + MmrApi>, { fn best_grandpa_block(&self) -> NumberFor { *self.persisted_state.voting_oracle.best_grandpa_block_header.number() @@ -572,7 +573,7 @@ where target: LOG_TARGET, "🥩 Round #{} concluded, finality_proof: {:?}.", block_number, finality_proof ); - // We created the `finality_proof` and know to be valid. + // We created the `finality_proof` and know it to be valid. // New state is persisted after finalization. self.finalize(finality_proof.clone())?; metric_inc!(self.metrics, beefy_good_votes_processed); @@ -827,7 +828,7 @@ where mut self, block_import_justif: &mut Fuse>>, finality_notifications: &mut Fuse>, - ) -> (Error, BeefyComms) { + ) -> (Error, BeefyComms) { info!( target: LOG_TARGET, "🥩 run BEEFY worker, best grandpa: #{:?}.", @@ -997,6 +998,7 @@ pub(crate) mod tests { notification::{BeefyBestBlockStream, BeefyVersionedFinalityProofStream}, request_response::outgoing_requests_engine::OnDemandJustificationsEngine, }, + fisherman::Fisherman, tests::{ create_beefy_keystore, get_beefy_streams, make_beefy_ids, BeefyPeer, BeefyTestNet, TestApi, @@ -1014,8 +1016,11 @@ pub(crate) mod tests { known_payloads, known_payloads::MMR_ROOT_ID, mmr::MmrRootProvider, - test_utils::{generate_equivocation_proof, Keyring}, - ConsensusLog, Payload, SignedCommitment, + test_utils::{ + generate_double_voting_proof, generate_fork_equivocation_proof_sc, + generate_fork_equivocation_proof_vote, Keyring, + }, + ConsensusLog, ForkEquivocationProof, KnownSignature, Payload, SignedCommitment, }; use sp_runtime::traits::{Header as HeaderT, One}; use substrate_test_runtime_client::{ @@ -1044,6 +1049,7 @@ pub(crate) mod tests { key: &Keyring, min_block_delta: u32, genesis_validator_set: ValidatorSet, + runtime_api: Option>, ) -> BeefyWorker< Block, Backend, @@ -1052,7 +1058,8 @@ pub(crate) mod tests { Arc>, TestNetwork, > { - let keystore = create_beefy_keystore(key); + let key_store: Arc> = + Arc::new(Some(create_beefy_keystore(key)).into()); let (to_rpc_justif_sender, from_voter_justif_stream) = BeefyVersionedFinalityProofStream::::channel(); @@ -1073,15 +1080,26 @@ pub(crate) mod tests { let backend = peer.client().as_backend(); let beefy_genesis = 1; - let api = Arc::new(TestApi::with_validator_set(&genesis_validator_set)); + let api = runtime_api + .unwrap_or_else(|| Arc::new(TestApi::with_validator_set(&genesis_validator_set))); let network = peer.network_service().clone(); let sync = peer.sync_service().clone(); + let payload_provider = MmrRootProvider::new(api.clone()); + let fisherman = Arc::new(Fisherman::new( + backend.clone(), + api.clone(), + key_store.clone(), + payload_provider.clone(), + )); let notification_service = peer .take_notification_service(&crate::tests::beefy_gossip_proto_name()) .unwrap(); let known_peers = Arc::new(Mutex::new(KnownPeers::new())); - let gossip_validator = - GossipValidator::new(known_peers.clone(), Arc::new(TestNetwork::new().0)); + let gossip_validator = GossipValidator::new( + known_peers.clone(), + Arc::new(TestNetwork::new().0), + fisherman.clone(), + ); let gossip_validator = Arc::new(gossip_validator); let gossip_engine = GossipEngine::new( network.clone(), @@ -1097,10 +1115,13 @@ pub(crate) mod tests { "/beefy/justifs/1".into(), known_peers, None, + fisherman.clone(), ); - // Push 1 block - will start first session. - let hashes = peer.push_blocks(1, false); - backend.finalize_block(hashes[0], None).unwrap(); + // If chain's still at genesis, push 1 block to start first session. + if backend.blockchain().info().best_hash == backend.blockchain().info().genesis_hash { + let hashes = peer.push_blocks(1, false); + backend.finalize_block(hashes[0], None).unwrap(); + } let first_header = backend .blockchain() .expect_header(backend.blockchain().info().best_hash) @@ -1113,9 +1134,7 @@ pub(crate) mod tests { beefy_genesis, ) .unwrap(); - let payload_provider = MmrRootProvider::new(api.clone()); let comms = BeefyComms { gossip_engine, gossip_validator, on_demand_justifications }; - let key_store: Arc> = Arc::new(Some(keystore).into()); BeefyWorker { backend: backend.clone(), runtime: api.clone(), @@ -1123,7 +1142,7 @@ pub(crate) mod tests { metrics, payload_provider, sync: Arc::new(sync), - fisherman: Arc::new(Fisherman::new(backend, api, key_store)), + fisherman: fisherman.clone(), links, comms, pending_justifications: BTreeMap::new(), @@ -1398,7 +1417,7 @@ pub(crate) mod tests { let validator_set = ValidatorSet::new(make_beefy_ids(&keys), 0).unwrap(); let mut net = BeefyTestNet::new(1); let backend = net.peer(0).client().as_backend(); - let mut worker = create_beefy_worker(net.peer(0), &keys[0], 1, validator_set.clone()); + let mut worker = create_beefy_worker(net.peer(0), &keys[0], 1, validator_set.clone(), None); // remove default session, will manually add custom one. worker.persisted_state.voting_oracle.sessions.clear(); @@ -1502,7 +1521,7 @@ pub(crate) mod tests { let keys = &[Keyring::Alice, Keyring::Bob]; let validator_set = ValidatorSet::new(make_beefy_ids(keys), 0).unwrap(); let mut net = BeefyTestNet::new(1); - let mut worker = create_beefy_worker(net.peer(0), &keys[0], 1, validator_set.clone()); + let mut worker = create_beefy_worker(net.peer(0), &keys[0], 1, validator_set.clone(), None); let worker_rounds = worker.active_rounds().unwrap(); assert_eq!(worker_rounds.session_start(), 1); @@ -1529,7 +1548,7 @@ pub(crate) mod tests { } #[tokio::test] - async fn should_not_report_bad_old_or_self_equivocations() { + async fn should_not_report_bad_old_or_self_vote_equivocations() { let block_num = 1; let set_id = 1; let keys = [Keyring::Alice]; @@ -1540,12 +1559,13 @@ pub(crate) mod tests { let api_alice = Arc::new(api_alice); let mut net = BeefyTestNet::new(1); - let mut worker = create_beefy_worker(net.peer(0), &keys[0], 1, validator_set.clone()); + let mut worker = create_beefy_worker(net.peer(0), &keys[0], 1, validator_set.clone(), None); worker.runtime = api_alice.clone(); worker.fisherman = Arc::new(Fisherman::new( worker.backend.clone(), worker.runtime.clone(), worker.key_store.clone(), + worker.payload_provider.clone(), )); // let there be a block with num = 1: @@ -1555,7 +1575,7 @@ pub(crate) mod tests { let payload2 = Payload::from_single_entry(MMR_ROOT_ID, vec![128]); // generate an equivocation proof, with Bob as perpetrator - let good_proof = generate_equivocation_proof( + let good_proof = generate_double_voting_proof( (block_num, payload1.clone(), set_id, &Keyring::Bob), (block_num, payload2.clone(), set_id, &Keyring::Bob), ); @@ -1563,11 +1583,11 @@ pub(crate) mod tests { // expect voter (Alice) to successfully report it assert_eq!(worker.report_double_voting(good_proof.clone()), Ok(())); // verify Alice reports Bob equivocation to runtime - let reported = api_alice.reported_equivocations.as_ref().unwrap().lock(); + let reported = api_alice.reported_vote_equivocations.as_ref().unwrap().lock(); assert_eq!(reported.len(), 1); assert_eq!(*reported.get(0).unwrap(), good_proof); } - api_alice.reported_equivocations.as_ref().unwrap().lock().clear(); + api_alice.reported_vote_equivocations.as_ref().unwrap().lock().clear(); // now let's try with a bad proof let mut bad_proof = good_proof.clone(); @@ -1575,7 +1595,7 @@ pub(crate) mod tests { // bad proofs are simply ignored assert_eq!(worker.report_double_voting(bad_proof), Ok(())); // verify nothing reported to runtime - assert!(api_alice.reported_equivocations.as_ref().unwrap().lock().is_empty()); + assert!(api_alice.reported_vote_equivocations.as_ref().unwrap().lock().is_empty()); // now let's try with old set it let mut old_proof = good_proof.clone(); @@ -1584,16 +1604,192 @@ pub(crate) mod tests { // old proofs are simply ignored assert_eq!(worker.report_double_voting(old_proof), Ok(())); // verify nothing reported to runtime - assert!(api_alice.reported_equivocations.as_ref().unwrap().lock().is_empty()); + assert!(api_alice.reported_vote_equivocations.as_ref().unwrap().lock().is_empty()); // now let's try reporting a self-equivocation - let self_proof = generate_equivocation_proof( + let self_proof = generate_double_voting_proof( (block_num, payload1.clone(), set_id, &Keyring::Alice), (block_num, payload2.clone(), set_id, &Keyring::Alice), ); // equivocations done by 'self' are simply ignored (not reported) assert_eq!(worker.report_double_voting(self_proof), Ok(())); // verify nothing reported to runtime - assert!(api_alice.reported_equivocations.as_ref().unwrap().lock().is_empty()); + assert!(api_alice.reported_vote_equivocations.as_ref().unwrap().lock().is_empty()); + } + + #[tokio::test] + async fn beefy_reports_fork_equivocations() { + let peers = [Keyring::Alice, Keyring::Bob, Keyring::Charlie]; + let validator_set = ValidatorSet::new(make_beefy_ids(&peers), 0).unwrap(); + let mut api_alice = TestApi::with_validator_set(&validator_set); + api_alice.allow_equivocations(); + let api_alice = Arc::new(api_alice); + + // instantiate network with Alice and Bob running full voters. + let mut net = BeefyTestNet::new(3); + + let session_len = 10; + let hashes = net.generate_blocks_and_sync(50, session_len, &validator_set, true).await; + let alice_worker = + create_beefy_worker(net.peer(0), &peers[0], 1, validator_set.clone(), Some(api_alice)); + + let block_number = 1; + let header = net + .peer(1) + .client() + .as_backend() + .blockchain() + .header(hashes[block_number as usize]) + .unwrap() + .unwrap(); + let payload = Payload::from_single_entry(MMR_ROOT_ID, "amievil".encode()); + let votes: Vec<_> = peers + .iter() + .map(|k| { + // signed_vote(block_number as u64, payload.clone(), validator_set.id(), k) + (block_number as u64, payload.clone(), validator_set.id(), k) + }) + .collect(); + + // verify: Alice reports Bob + let ancestry_proof = alice_worker + .runtime + .runtime_api() + .generate_ancestry_proof(*hashes.last().unwrap(), block_number, None) + .unwrap() + .unwrap(); + let proof = generate_fork_equivocation_proof_vote( + votes[1].clone(), + Some(header.clone()), + Some(ancestry_proof.clone()), + ); + + { + // expect fisher (Alice) to successfully process it + assert_eq!( + alice_worker + .comms + .gossip_validator + .fisherman + .report_fork_equivocation(proof.clone()), + Ok(true) + ); + // verify Alice reports Bob's equivocation to runtime + let reported = + alice_worker.runtime.reported_fork_equivocations.as_ref().unwrap().lock(); + assert_eq!(reported.len(), 1); + assert_eq!(*reported.get(0).unwrap(), proof); + } + + // verify: Alice does not self-report + let proof = generate_fork_equivocation_proof_vote( + votes[0].clone(), + Some(header.clone()), + Some(ancestry_proof.clone()), + ); + { + // expect fisher (Alice) to successfully process it + assert_eq!( + alice_worker + .comms + .gossip_validator + .fisherman + .report_fork_equivocation(proof.clone()), + Ok(false) + ); + // verify Alice does *not* report her own equivocation to runtime + let reported = + alice_worker.runtime.reported_fork_equivocations.as_ref().unwrap().lock(); + assert_eq!(reported.len(), 1); + assert!(*reported.get(0).unwrap() != proof); + } + + // verify: Alice reports VersionedFinalityProof equivocation + let commitment = Commitment { + payload: payload.clone(), + block_number: block_number as u64, + validator_set_id: validator_set.id(), + }; + + // only Bob and Charlie sign + let signatures = &[Keyring::Bob, Keyring::Charlie] + .into_iter() + .map(|k| KnownSignature { + validator_id: k.public(), + signature: k.sign(&commitment.encode()), + }) + .collect::>(); + + // test over all permutations of header and ancestry proof being submitted (proof should + // be valid as long as at least one is submitted) + for (canonical_header, ancestry_proof) in [ + (Some(header.clone()), Some(ancestry_proof.clone())), + (Some(header), None), + (None, Some(ancestry_proof)), + ] { + let proof = ForkEquivocationProof { + commitment: commitment.clone(), + signatures: signatures.clone(), + canonical_header, + ancestry_proof, + }; + // expect fisher (Alice) to successfully process it + assert_eq!( + alice_worker + .comms + .gossip_validator + .fisherman + .report_fork_equivocation(proof.clone()), + Ok(true) + ); + let mut reported = + alice_worker.runtime.reported_fork_equivocations.as_ref().unwrap().lock(); + // verify Alice report Bob's and Charlie's equivocation to runtime + assert_eq!(reported.len(), 2); + assert_eq!(reported.pop(), Some(proof)); + } + + // test that Alice does not submit invalid proof + let proofless_proof = ForkEquivocationProof { + commitment: commitment.clone(), + signatures: signatures.clone(), + canonical_header: None, + ancestry_proof: None, + }; + + assert_eq!( + alice_worker + .comms + .gossip_validator + .fisherman + .report_fork_equivocation(proofless_proof.clone()), + Ok(false) + ); + + let future_commitment = Commitment { + payload: commitment.payload, + block_number: net.peer(1).client().info().best_number + 10, + validator_set_id: commitment.validator_set_id, + }; + + let future_proof = generate_fork_equivocation_proof_sc( + future_commitment, + vec![Keyring::Bob, Keyring::Charlie], + None, + None, + ); + + assert_eq!( + alice_worker + .comms + .gossip_validator + .fisherman + .report_fork_equivocation(future_proof.clone()), + Ok(true) + ); + let mut reported = + alice_worker.runtime.reported_fork_equivocations.as_ref().unwrap().lock(); + assert_eq!(reported.len(), 2); + assert_eq!(reported.pop(), Some(future_proof)); } } diff --git a/substrate/frame/beefy-mmr/Cargo.toml b/substrate/frame/beefy-mmr/Cargo.toml index 51abc306265d6..3da9b93db0416 100644 --- a/substrate/frame/beefy-mmr/Cargo.toml +++ b/substrate/frame/beefy-mmr/Cargo.toml @@ -1,4 +1,5 @@ [package] + name = "pallet-beefy-mmr" version = "28.0.0" authors.workspace = true @@ -24,6 +25,8 @@ pallet-beefy = { path = "../beefy", default-features = false } pallet-mmr = { path = "../merkle-mountain-range", default-features = false } pallet-session = { path = "../session", default-features = false } sp-consensus-beefy = { path = "../../primitives/consensus/beefy", default-features = false } +sp-mmr-primitives = { path = "../../primitives/merkle-mountain-range", default-features = false } +sp-application-crypto = { path = "../../primitives/application-crypto", default-features = false, features = ["serde"] } sp-core = { path = "../../primitives/core", default-features = false } sp-io = { path = "../../primitives/io", default-features = false } sp-runtime = { path = "../../primitives/runtime", default-features = false } @@ -50,9 +53,11 @@ std = [ "scale-info/std", "serde", "sp-api/std", + "sp-application-crypto/std", "sp-consensus-beefy/std", "sp-core/std", "sp-io/std", + "sp-mmr-primitives/std", "sp-runtime/std", "sp-staking/std", "sp-state-machine/std", diff --git a/substrate/frame/beefy-mmr/src/lib.rs b/substrate/frame/beefy-mmr/src/lib.rs index e423f1b342f2f..b155ec5b4816e 100644 --- a/substrate/frame/beefy-mmr/src/lib.rs +++ b/substrate/frame/beefy-mmr/src/lib.rs @@ -33,18 +33,18 @@ //! //! and thanks to versioning can be easily updated in the future. -use sp_runtime::traits::{Convert, Member}; +use sp_runtime::traits::{Convert, Hash, Member}; use sp_std::prelude::*; use codec::Decode; use pallet_mmr::{LeafDataProvider, ParentNumberAndHash}; use sp_consensus_beefy::{ mmr::{BeefyAuthoritySet, BeefyDataProvider, BeefyNextAuthoritySet, MmrLeaf, MmrLeafVersion}, - ValidatorSet as BeefyValidatorSet, + CheckForkEquivocationProof, ForkEquivocationProof, ValidatorSet as BeefyValidatorSet, }; use frame_support::{crypto::ecdsa::ECDSAExt, traits::Get}; -use frame_system::pallet_prelude::BlockNumberFor; +use frame_system::pallet_prelude::{BlockNumberFor, HeaderFor}; pub use pallet::*; @@ -105,7 +105,7 @@ pub mod pallet { pub trait Config: pallet_mmr::Config + pallet_beefy::Config { /// Current leaf version. /// - /// Specifies the version number added to every leaf that get's appended to the MMR. + /// Specifies the version number added to every leaf that gets appended to the MMR. /// Read more in [`MmrLeafVersion`] docs about versioning leaves. type LeafVersion: Get; @@ -172,6 +172,46 @@ where } } +impl CheckForkEquivocationProof, HeaderFor> + for Pallet +{ + type Hash = ::Hashing; + fn check_fork_equivocation_proof( + proof: &ForkEquivocationProof, ::Output>, + ) -> Result<(), pallet_beefy::Error> + where + Id: sp_consensus_beefy::BeefyAuthorityId + PartialEq, + MsgHash: sp_runtime::traits::Hash, + { + let best_root = >::mmr_root(); + let mmr_size = + sp_mmr_primitives::utils::NodesUtils::new(>::mmr_leaves()).size(); + // if first_mmr_block_num is invalid, then presumably beefy is not active. + // TODO: should we slash in this case? + let block_number = proof.commitment.block_number; + let canonical_header_hash = >::block_hash(block_number); + let best_block_num = >::block_number(); + let first_mmr_block_num = { + let mmr_leaf_count = >::mmr_leaves(); + sp_mmr_primitives::utils::first_mmr_block_num::>( + best_block_num, + mmr_leaf_count, + ) + .map_err(|_| pallet_beefy::Error::::InvalidForkEquivocationProof)? + }; + if !proof.check::<_, Self::Hash>( + best_root, + mmr_size, + &canonical_header_hash, + first_mmr_block_num, + best_block_num, + ) { + return Err(pallet_beefy::Error::::InvalidForkEquivocationProof) + } + Ok(()) + } +} + impl Pallet { /// Return the currently active BEEFY authority set proof. pub fn authority_set_proof() -> BeefyAuthoritySet> { diff --git a/substrate/frame/beefy-mmr/src/mock.rs b/substrate/frame/beefy-mmr/src/mock.rs index d59c219d3e71e..5b197de9685f4 100644 --- a/substrate/frame/beefy-mmr/src/mock.rs +++ b/substrate/frame/beefy-mmr/src/mock.rs @@ -101,6 +101,7 @@ impl pallet_beefy::Config for Test { type MaxNominators = ConstU32<1000>; type MaxSetIdSessionEntries = ConstU64<100>; type OnNewValidatorSet = BeefyMmr; + type CheckForkEquivocationProof = BeefyMmr; type WeightInfo = (); type KeyOwnerProof = sp_core::Void; type EquivocationReportSystem = (); diff --git a/substrate/frame/beefy/Cargo.toml b/substrate/frame/beefy/Cargo.toml index 890ac1399b9df..4c6cd2400c836 100644 --- a/substrate/frame/beefy/Cargo.toml +++ b/substrate/frame/beefy/Cargo.toml @@ -16,10 +16,12 @@ codec = { package = "parity-scale-codec", version = "3.6.12", default-features = log = { workspace = true } scale-info = { version = "2.11.1", default-features = false, features = ["derive", "serde"] } serde = { optional = true, workspace = true, default-features = true } +pallet-mmr = { path = "../merkle-mountain-range", default-features = false } frame-support = { path = "../support", default-features = false } frame-system = { path = "../system", default-features = false } pallet-authorship = { path = "../authorship", default-features = false } pallet-session = { path = "../session", default-features = false } +sp-mmr-primitives = { path = "../../primitives/merkle-mountain-range", default-features = false, features = ["serde"] } sp-consensus-beefy = { path = "../../primitives/consensus/beefy", default-features = false, features = ["serde"] } sp-runtime = { path = "../../primitives/runtime", default-features = false, features = ["serde"] } sp-session = { path = "../../primitives/session", default-features = false } @@ -27,6 +29,7 @@ sp-staking = { path = "../../primitives/staking", default-features = false, feat sp-std = { path = "../../primitives/std", default-features = false } [dev-dependencies] +sp-mmr-primitives = { path = "../../primitives/merkle-mountain-range" } frame-election-provider-support = { path = "../election-provider-support" } pallet-balances = { path = "../balances" } pallet-offences = { path = "../offences" } @@ -48,6 +51,7 @@ std = [ "log/std", "pallet-authorship/std", "pallet-balances/std", + "pallet-mmr/std", "pallet-offences/std", "pallet-session/std", "pallet-staking/std", @@ -69,6 +73,7 @@ try-runtime = [ "frame-system/try-runtime", "pallet-authorship/try-runtime", "pallet-balances/try-runtime", + "pallet-mmr/try-runtime", "pallet-offences/try-runtime", "pallet-session/try-runtime", "pallet-staking/try-runtime", diff --git a/substrate/frame/beefy/src/default_weights.rs b/substrate/frame/beefy/src/default_weights.rs index 8042f0c932eb6..ad61c3ade5c8d 100644 --- a/substrate/frame/beefy/src/default_weights.rs +++ b/substrate/frame/beefy/src/default_weights.rs @@ -24,7 +24,7 @@ use frame_support::weights::{ }; impl crate::WeightInfo for () { - fn report_equivocation(validator_count: u32, max_nominators_per_validator: u32) -> Weight { + fn report_vote_equivocation(validator_count: u32, max_nominators_per_validator: u32) -> Weight { // we take the validator set count from the membership proof to // calculate the weight but we set a floor of 100 validators. let validator_count = validator_count.max(100) as u64; @@ -50,6 +50,38 @@ impl crate::WeightInfo for () { .saturating_add(DbWeight::get().reads(2)) } + // TODO: update weight calculation + fn report_fork_equivocation( + validator_count: u32, + max_nominators_per_validator: u32, + key_owner_proofs_len: usize, + ) -> Weight { + // we take the validator set count from the membership proof to + // calculate the weight but we set a floor of 100 validators. + let validator_count = validator_count.max(100) as u64; + + // checking membership proof + Weight::from_parts(35u64 * WEIGHT_REF_TIME_PER_MICROS, 0) + .saturating_add( + Weight::from_parts(175u64 * WEIGHT_REF_TIME_PER_NANOS, 0) + .saturating_mul(validator_count) + .saturating_mul(key_owner_proofs_len as u64), + ) + .saturating_add(DbWeight::get().reads(5)) + // check equivocation proof + .saturating_add(Weight::from_parts(95u64 * WEIGHT_REF_TIME_PER_MICROS, 0)) + // report offence + .saturating_add(Weight::from_parts(110u64 * WEIGHT_REF_TIME_PER_MICROS, 0)) + .saturating_add(Weight::from_parts( + 25u64 * WEIGHT_REF_TIME_PER_MICROS * max_nominators_per_validator as u64, + 0, + )) + .saturating_add(DbWeight::get().reads(14 + 3 * max_nominators_per_validator as u64)) + .saturating_add(DbWeight::get().writes(10 + 3 * max_nominators_per_validator as u64)) + // fetching set id -> session index mappings + .saturating_add(DbWeight::get().reads(2)) + } + fn set_new_genesis() -> Weight { DbWeight::get().writes(1) } diff --git a/substrate/frame/beefy/src/equivocation.rs b/substrate/frame/beefy/src/equivocation.rs index aecc9e721d5c4..0b57d45cca04a 100644 --- a/substrate/frame/beefy/src/equivocation.rs +++ b/substrate/frame/beefy/src/equivocation.rs @@ -36,10 +36,14 @@ use codec::{self as codec, Decode, Encode}; use frame_support::traits::{Get, KeyOwnerProofSystem}; -use frame_system::pallet_prelude::BlockNumberFor; +use frame_system::pallet_prelude::{BlockNumberFor, HeaderFor}; use log::{error, info}; -use sp_consensus_beefy::{DoubleVotingProof, ValidatorSetId, KEY_TYPE as BEEFY_KEY_TYPE}; +use sp_consensus_beefy::{ + BeefyEquivocationProof, CheckForkEquivocationProof, DoubleVotingProof, ForkEquivocationProof, + ValidatorSetId, KEY_TYPE as BEEFY_KEY_TYPE, +}; use sp_runtime::{ + traits::Hash as HashT, transaction_validity::{ InvalidTransaction, TransactionPriority, TransactionSource, TransactionValidity, TransactionValidityError, ValidTransaction, @@ -76,8 +80,8 @@ where 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: Offender, + /// The authorities which produced this equivocation. + pub offenders: Vec, } impl Offence for EquivocationOffence @@ -88,7 +92,7 @@ where type TimeSlot = TimeSlot; fn offenders(&self) -> Vec { - vec![self.offender.clone()] + self.offenders.clone() } fn session_index(&self) -> SessionIndex { @@ -122,14 +126,91 @@ where pub struct EquivocationReportSystem(sp_std::marker::PhantomData<(T, R, P, L)>); /// Equivocation evidence convenience alias. -pub type EquivocationEvidenceFor = ( - DoubleVotingProof< - BlockNumberFor, - ::BeefyId, - <::BeefyId as RuntimeAppPublic>::Signature, - >, - ::KeyOwnerProof, -); +pub enum EquivocationEvidenceFor { + VoteEquivocationProof( + DoubleVotingProof< + BlockNumberFor, + ::BeefyId, + <::BeefyId as RuntimeAppPublic>::Signature, + >, + ::KeyOwnerProof, + ), + ForkEquivocationProof( + ForkEquivocationProof< + ::BeefyId, + HeaderFor, + <<::CheckForkEquivocationProof as CheckForkEquivocationProof< + Error, + HeaderFor, + >>::Hash as HashT>::Output, + >, + Vec<::KeyOwnerProof>, + ), +} + +impl EquivocationEvidenceFor { + fn proof(&self) -> Box<&dyn BeefyEquivocationProof<::BeefyId, BlockNumberFor>> { + match self { + EquivocationEvidenceFor::VoteEquivocationProof(proof, _) => Box::new(proof), + EquivocationEvidenceFor::ForkEquivocationProof(proof, _) => Box::new(proof), + } + } + + fn key_owner_proofs(&self) -> Vec<&::KeyOwnerProof> { + match self { + EquivocationEvidenceFor::VoteEquivocationProof(_, key_owner_proof) => + vec![key_owner_proof], + EquivocationEvidenceFor::ForkEquivocationProof(_, key_owner_proofs) => + key_owner_proofs.iter().collect(), + } + } + + // Validate the key ownership proofs extracting the ids of the offenders. + fn checked_offenders

(&self) -> Option> + where + P: KeyOwnerProofSystem<(KeyTypeId, T::BeefyId), Proof = T::KeyOwnerProof>, + { + let offenders_unique: sp_std::collections::btree_set::BTreeSet<_> = + self.proof().offender_ids().iter().cloned().collect(); + if offenders_unique.len() != self.proof().offender_ids().len() { + log::warn!( + target: LOG_TARGET, + "ignoring equivocation evidence since it contains duplicate offenders." + ); + return None + } + + self.proof() + .offender_ids() + .into_iter() + .zip(self.key_owner_proofs().iter()) + .map(|(key, &key_owner_proof)| { + P::check_proof((BEEFY_KEY_TYPE, key.clone()), key_owner_proof.clone()) + }) + .collect::>>() + } + + fn check_equivocation_proof(&self) -> Result<(), Error> { + match self { + EquivocationEvidenceFor::VoteEquivocationProof(equivocation_proof, _) => { + // Validate equivocation proof (check votes are different and signatures are valid). + if !sp_consensus_beefy::check_double_voting_proof(&equivocation_proof) { + return Err(Error::::InvalidVoteEquivocationProof.into()); + } + + return Ok(()) + }, + EquivocationEvidenceFor::ForkEquivocationProof(equivocation_proof, _) => { + // Validate equivocation proof (check commitment is to unexpected payload and + // signatures are valid). + , + HeaderFor, + >>::check_fork_equivocation_proof(equivocation_proof) + }, + } + } +} impl OffenceReportSystem, EquivocationEvidenceFor> for EquivocationReportSystem @@ -148,13 +229,8 @@ where fn publish_evidence(evidence: EquivocationEvidenceFor) -> Result<(), ()> { use frame_system::offchain::SubmitTransaction; - let (equivocation_proof, key_owner_proof) = evidence; - - let call = Call::report_equivocation_unsigned { - equivocation_proof: Box::new(equivocation_proof), - key_owner_proof, - }; + let call: Call = evidence.into(); let res = SubmitTransaction::>::submit_unsigned_transaction(call.into()); match res { Ok(_) => info!(target: LOG_TARGET, "Submitted equivocation report."), @@ -166,19 +242,11 @@ where fn check_evidence( evidence: EquivocationEvidenceFor, ) -> Result<(), TransactionValidityError> { - let (equivocation_proof, key_owner_proof) = evidence; - - // Check the membership proof to extract the offender's id - let key = (BEEFY_KEY_TYPE, equivocation_proof.offender_id().clone()); - let offender = P::check_proof(key, key_owner_proof).ok_or(InvalidTransaction::BadProof)?; + let offenders = evidence.checked_offenders::

().ok_or(InvalidTransaction::BadProof)?; - // Check if the offence has already been reported, and if so then we can discard the report. - let time_slot = TimeSlot { - set_id: equivocation_proof.set_id(), - round: *equivocation_proof.round_number(), - }; - - if R::is_known_offence(&[offender], &time_slot) { + let time_slot = + TimeSlot { set_id: evidence.proof().set_id(), round: *evidence.proof().round_number() }; + if R::is_known_offence(&offenders, &time_slot) { Err(InvalidTransaction::Stale.into()) } else { Ok(()) @@ -189,99 +257,77 @@ where reporter: Option, evidence: EquivocationEvidenceFor, ) -> Result<(), DispatchError> { - let (equivocation_proof, key_owner_proof) = evidence; - let reporter = reporter.or_else(|| pallet_authorship::Pallet::::author()); - let offender = equivocation_proof.offender_id().clone(); - - // We check the equivocation within the context of its set id (and - // associated session) and round. We also need to know the validator - // set count at the time of the offence since it is required to calculate - // the slash amount. - let set_id = equivocation_proof.set_id(); - let round = *equivocation_proof.round_number(); - let session_index = key_owner_proof.session(); - let validator_set_count = key_owner_proof.validator_count(); - - // Validate the key ownership proof extracting the id of the offender. - let offender = P::check_proof((BEEFY_KEY_TYPE, offender), key_owner_proof) - .ok_or(Error::::InvalidKeyOwnershipProof)?; - - // Validate equivocation proof (check votes are different and signatures are valid). - if !sp_consensus_beefy::check_equivocation_proof(&equivocation_proof) { - return Err(Error::::InvalidEquivocationProof.into()) - } + let reporter = reporter.or_else(|| >::author()); + + // We check the equivocation within the context of its set id (and associated session). + let set_id = evidence.proof().set_id(); + let set_id_session_index = crate::SetIdSession::::get(set_id) + .ok_or(Error::::InvalidEquivocationProofSession)?; - // Check that the session id for the membership proof is within the - // bounds of the set id reported in the equivocation. - let set_id_session_index = - crate::SetIdSession::::get(set_id).ok_or(Error::::InvalidEquivocationProof)?; + // Check that the session id for the membership proof is within the bounds + // of the set id reported in the equivocation. + let key_owner_proofs = evidence.key_owner_proofs(); + let session_index = key_owner_proofs[0].session(); if session_index != set_id_session_index { - return Err(Error::::InvalidEquivocationProof.into()) + return Err(Error::::InvalidEquivocationProofSession.into()) } + let offenders = + evidence.checked_offenders::

().ok_or(Error::::InvalidKeyOwnershipProof)?; + evidence.check_equivocation_proof()?; + let offence = EquivocationOffence { - time_slot: TimeSlot { set_id, round }, + time_slot: TimeSlot { set_id, round: *evidence.proof().round_number() }, session_index, - validator_set_count, - offender, + validator_set_count: key_owner_proofs[0].validator_count(), + offenders, }; - R::report_offence(reporter.into_iter().collect(), offence) - .map_err(|_| Error::::DuplicateOffenceReport)?; - - Ok(()) + .map_err(|_| Error::::DuplicateOffenceReport.into()) } } /// Methods for the `ValidateUnsigned` implementation: -/// It restricts calls to `report_equivocation_unsigned` to local calls (i.e. extrinsics generated -/// on this node) or that already in a block. This guarantees that only block authors can include -/// unsigned equivocation reports. +/// It restricts calls to `report_vote_equivocation_unsigned` to local calls (i.e. extrinsics +/// generated on this node) or that already in a block. This guarantees that only block authors can +/// include unsigned equivocation reports. impl Pallet { pub fn validate_unsigned(source: TransactionSource, call: &Call) -> TransactionValidity { - if let Call::report_equivocation_unsigned { equivocation_proof, key_owner_proof } = call { - // discard equivocation report not coming from the local node - match source { - TransactionSource::Local | TransactionSource::InBlock => { /* allowed */ }, - _ => { - log::warn!( - target: LOG_TARGET, - "rejecting unsigned report equivocation transaction because it is not local/in-block." - ); - return InvalidTransaction::Call.into() - }, - } - - let evidence = (*equivocation_proof.clone(), key_owner_proof.clone()); - T::EquivocationReportSystem::check_evidence(evidence)?; - - let longevity = - >::Longevity::get(); - - ValidTransaction::with_tag_prefix("BeefyEquivocation") - // We assign the maximum priority for any equivocation report. - .priority(TransactionPriority::MAX) - // Only one equivocation report for the same offender at the same slot. - .and_provides(( - equivocation_proof.offender_id().clone(), - equivocation_proof.set_id(), - *equivocation_proof.round_number(), - )) - .longevity(longevity) - // We don't propagate this. This can never be included on a remote node. - .propagate(false) - .build() - } else { - InvalidTransaction::Call.into() + // discard equivocation report not coming from the local node + match source { + TransactionSource::Local | TransactionSource::InBlock => { /* allowed */ }, + _ => { + log::warn!( + target: LOG_TARGET, + "rejecting unsigned report equivocation transaction because it is not local/in-block." + ); + return InvalidTransaction::Call.into() + }, } + + let (evidence, equivocation_proof) = + call.to_equivocation_evidence_for().ok_or(InvalidTransaction::Call)?; + T::EquivocationReportSystem::check_evidence(evidence)?; + + let longevity = + >::Longevity::get(); + ValidTransaction::with_tag_prefix("BeefyEquivocation") + // We assign the maximum priority for any equivocation report. + .priority(TransactionPriority::MAX) + // Only one equivocation report for the same offender at the same slot. + .and_provides(( + equivocation_proof.offender_ids(), + equivocation_proof.set_id(), + *equivocation_proof.round_number(), + )) + .longevity(longevity) + // We don't propagate this. This can never be included on a remote node. + .propagate(false) + .build() } pub fn pre_dispatch(call: &Call) -> Result<(), TransactionValidityError> { - if let Call::report_equivocation_unsigned { equivocation_proof, key_owner_proof } = call { - let evidence = (*equivocation_proof.clone(), key_owner_proof.clone()); - T::EquivocationReportSystem::check_evidence(evidence) - } else { - Err(InvalidTransaction::Call.into()) - } + let (evidence, _) = call.to_equivocation_evidence_for().ok_or(InvalidTransaction::Call)?; + T::EquivocationReportSystem::check_evidence(evidence) } } diff --git a/substrate/frame/beefy/src/lib.rs b/substrate/frame/beefy/src/lib.rs index 63f3e9bb309c6..ccd9ac53e7d38 100644 --- a/substrate/frame/beefy/src/lib.rs +++ b/substrate/frame/beefy/src/lib.rs @@ -28,12 +28,12 @@ use frame_support::{ }; use frame_system::{ ensure_none, ensure_signed, - pallet_prelude::{BlockNumberFor, OriginFor}, + pallet_prelude::{BlockNumberFor, HeaderFor, OriginFor}, }; use log; use sp_runtime::{ generic::DigestItem, - traits::{IsMember, Member, One}, + traits::{Hash as HashT, IsMember, Member, One}, RuntimeAppPublic, }; use sp_session::{GetSessionNumber, GetValidatorCount}; @@ -41,8 +41,8 @@ use sp_staking::{offence::OffenceReportSystem, SessionIndex}; use sp_std::prelude::*; use sp_consensus_beefy::{ - AuthorityIndex, BeefyAuthorityId, ConsensusLog, DoubleVotingProof, OnNewValidatorSet, - ValidatorSet, BEEFY_ENGINE_ID, GENESIS_AUTHORITY_SET_ID, + AuthorityIndex, BeefyAuthorityId, CheckForkEquivocationProof, ConsensusLog, DoubleVotingProof, + OnNewValidatorSet, ValidatorSet, BEEFY_ENGINE_ID, GENESIS_AUTHORITY_SET_ID, }; mod default_weights; @@ -63,6 +63,7 @@ const LOG_TARGET: &str = "runtime::beefy"; pub mod pallet { use super::*; use frame_system::{ensure_root, pallet_prelude::BlockNumberFor}; + use sp_consensus_beefy::{BeefyEquivocationProof, ForkEquivocationProof}; #[pallet::config] pub trait Config: frame_system::Config { @@ -72,7 +73,8 @@ pub mod pallet { // todo: use custom signature hashing type instead of hardcoded `Keccak256` + BeefyAuthorityId + MaybeSerializeDeserialize - + MaxEncodedLen; + + MaxEncodedLen + + Ord; /// The maximum number of authorities that can be added. #[pallet::constant] @@ -98,6 +100,12 @@ pub mod pallet { /// weight MMR root over validators and make it available for Light Clients. type OnNewValidatorSet: OnNewValidatorSet<::BeefyId>; + /// Hook for checking fork equivocation proofs + type CheckForkEquivocationProof: sp_consensus_beefy::CheckForkEquivocationProof< + pallet::Error, + HeaderFor, + >; + /// Weights for this pallet. type WeightInfo: WeightInfo; @@ -188,8 +196,12 @@ pub mod pallet { pub enum Error { /// A key ownership proof provided as part of an equivocation report is invalid. InvalidKeyOwnershipProof, - /// An equivocation proof provided as part of an equivocation report is invalid. - InvalidEquivocationProof, + /// An equivocation proof provided as part of a voter equivocation report is invalid. + InvalidVoteEquivocationProof, + /// An equivocation proof provided as part of a fork equivocation report is invalid. + InvalidForkEquivocationProof, + /// The session of the equivocation proof is invalid + InvalidEquivocationProofSession, /// A given equivocation report is valid but already previously reported. DuplicateOffenceReport, /// Submitted configuration is invalid. @@ -203,11 +215,11 @@ pub mod pallet { /// against the extracted offender. If both are valid, the offence /// will be reported. #[pallet::call_index(0)] - #[pallet::weight(T::WeightInfo::report_equivocation( + #[pallet::weight(::WeightInfo::report_vote_equivocation( key_owner_proof.validator_count(), T::MaxNominators::get(), ))] - pub fn report_equivocation( + pub fn report_vote_equivocation( origin: OriginFor, equivocation_proof: Box< DoubleVotingProof< @@ -222,7 +234,10 @@ pub mod pallet { T::EquivocationReportSystem::process_evidence( Some(reporter), - (*equivocation_proof, key_owner_proof), + EquivocationEvidenceFor::VoteEquivocationProof( + *equivocation_proof, + key_owner_proof, + ), )?; // Waive the fee since the report is valid and beneficial Ok(Pays::No.into()) @@ -238,11 +253,11 @@ pub mod pallet { /// if the block author is defined it will be defined as the equivocation /// reporter. #[pallet::call_index(1)] - #[pallet::weight(T::WeightInfo::report_equivocation( + #[pallet::weight(::WeightInfo::report_vote_equivocation( key_owner_proof.validator_count(), T::MaxNominators::get(), ))] - pub fn report_equivocation_unsigned( + pub fn report_vote_equivocation_unsigned( origin: OriginFor, equivocation_proof: Box< DoubleVotingProof< @@ -257,7 +272,10 @@ pub mod pallet { T::EquivocationReportSystem::process_evidence( None, - (*equivocation_proof, key_owner_proof), + EquivocationEvidenceFor::::VoteEquivocationProof( + *equivocation_proof, + key_owner_proof, + ), )?; Ok(Pays::No.into()) } @@ -278,6 +296,78 @@ pub mod pallet { GenesisBlock::::put(Some(genesis_block)); Ok(()) } + + /// Report voter voting on invalid fork. This method will verify the + /// invalid fork proof and validate the given key ownership proof + /// against the extracted offender. If both are valid, the offence + /// will be reported. + #[pallet::call_index(3)] + #[pallet::weight(::WeightInfo::report_fork_equivocation( + key_owner_proofs[0].validator_count(), + T::MaxNominators::get(), + key_owner_proofs.len(), + ))] + pub fn report_fork_equivocation( + origin: OriginFor, + equivocation_proof: Box< + ForkEquivocationProof< + T::BeefyId, + HeaderFor, + <<::CheckForkEquivocationProof as CheckForkEquivocationProof< + Error, + HeaderFor, + >>::Hash as HashT>::Output, + >, + >, + key_owner_proofs: Vec, + ) -> DispatchResultWithPostInfo { + let reporter = ensure_signed(origin)?; + + T::EquivocationReportSystem::process_evidence( + Some(reporter), + EquivocationEvidenceFor::ForkEquivocationProof( + *equivocation_proof, + key_owner_proofs, + ), + )?; + // Waive the fee since the report is valid and beneficial + Ok(Pays::No.into()) + } + + /// Report commitment on invalid fork. This method will verify the + /// invalid fork proof and validate the given key ownership proof + /// against the extracted offenders. If both are valid, the offence + /// will be reported. + /// + /// This extrinsic must be called unsigned and it is expected that only + /// block authors will call it (validated in `ValidateUnsigned`), as such + /// if the block author is defined it will be defined as the equivocation + /// reporter. + #[pallet::call_index(4)] + #[pallet::weight(::WeightInfo::report_fork_equivocation(key_owner_proofs[0].validator_count(), T::MaxNominators::get(), key_owner_proofs.len()))] + pub fn report_fork_equivocation_unsigned( + origin: OriginFor, + equivocation_proof: Box< + ForkEquivocationProof< + T::BeefyId, + HeaderFor, + <<::CheckForkEquivocationProof as sp_consensus_beefy::CheckForkEquivocationProof, HeaderFor>>::Hash as HashT>::Output, + >, + >, + key_owner_proofs: Vec, + ) -> DispatchResultWithPostInfo { + ensure_none(origin)?; + + T::EquivocationReportSystem::process_evidence( + None, + EquivocationEvidenceFor::ForkEquivocationProof( + *equivocation_proof, + key_owner_proofs, + ), + )?; + // Waive the fee since the report is valid and beneficial + Ok(Pays::No.into()) + } } #[pallet::hooks] @@ -300,6 +390,58 @@ pub mod pallet { Self::validate_unsigned(source, call) } } + + impl Call { + pub fn to_equivocation_evidence_for( + &self, + ) -> Option<( + EquivocationEvidenceFor, + Box<&dyn BeefyEquivocationProof<::BeefyId, BlockNumberFor>>, + )> { + match self { + Call::report_vote_equivocation_unsigned { equivocation_proof, key_owner_proof } => + Some(( + EquivocationEvidenceFor::::VoteEquivocationProof( + *equivocation_proof.clone(), + key_owner_proof.clone(), + ), + Box::new(equivocation_proof.as_ref()), + )), + Call::report_fork_equivocation_unsigned { + equivocation_proof, + key_owner_proofs, + } => Some(( + EquivocationEvidenceFor::::ForkEquivocationProof( + *equivocation_proof.clone(), + key_owner_proofs.clone(), + ), + Box::new(equivocation_proof.as_ref()), + )), + _ => None, + } + } + } + + impl From> for Call { + fn from(evidence: EquivocationEvidenceFor) -> Self { + match evidence { + EquivocationEvidenceFor::VoteEquivocationProof( + equivocation_proof, + key_owner_proof, + ) => Call::report_vote_equivocation_unsigned { + equivocation_proof: Box::new(equivocation_proof), + key_owner_proof, + }, + EquivocationEvidenceFor::ForkEquivocationProof( + equivocation_proof, + key_owner_proofs, + ) => Call::report_fork_equivocation_unsigned { + equivocation_proof: Box::new(equivocation_proof), + key_owner_proofs, + }, + } + } + } } #[cfg(any(feature = "try-runtime", test))] @@ -367,7 +509,7 @@ impl Pallet { /// Submits an extrinsic to report an equivocation. This method will create /// an unsigned extrinsic with a call to `report_equivocation_unsigned` and /// will push the transaction to the pool. Only useful in an offchain context. - pub fn submit_unsigned_equivocation_report( + pub fn submit_unsigned_vote_equivocation_report( equivocation_proof: DoubleVotingProof< BlockNumberFor, T::BeefyId, @@ -375,7 +517,40 @@ impl Pallet { >, key_owner_proof: T::KeyOwnerProof, ) -> Option<()> { - T::EquivocationReportSystem::publish_evidence((equivocation_proof, key_owner_proof)).ok() + T::EquivocationReportSystem::publish_evidence( + EquivocationEvidenceFor::::VoteEquivocationProof( + equivocation_proof, + key_owner_proof, + ), + ) + .ok() + } + + /// Submits an extrinsic to report an invalid fork signed by potentially + /// multiple signatories. This method will create an unsigned extrinsic with + /// a call to `report_fork_equivocation_unsigned` and will push the transaction + /// to the pool. Only useful in an offchain context. + pub fn submit_unsigned_fork_equivocation_report( + fork_equivocation_proof: sp_consensus_beefy::ForkEquivocationProof< + T::BeefyId, + HeaderFor, + <<::CheckForkEquivocationProof as CheckForkEquivocationProof< + pallet::Error, + HeaderFor, + >>::Hash as HashT>::Output, + >, + key_owner_proofs: Vec, + ) -> Option<()> { + let key_owner_proofs = + key_owner_proofs.into_iter().map(|p| p.decode()).collect::>>()?; + + T::EquivocationReportSystem::publish_evidence( + EquivocationEvidenceFor::::ForkEquivocationProof( + fork_equivocation_proof, + key_owner_proofs, + ), + ) + .ok() } fn change_authorities( @@ -526,6 +701,11 @@ impl IsMember for Pallet { } pub trait WeightInfo { - fn report_equivocation(validator_count: u32, max_nominators_per_validator: u32) -> Weight; + fn report_vote_equivocation(validator_count: u32, max_nominators_per_validator: u32) -> Weight; + fn report_fork_equivocation( + validator_count: u32, + max_nominators_per_validator: u32, + key_owner_proofs_len: usize, + ) -> Weight; fn set_new_genesis() -> Weight; } diff --git a/substrate/frame/beefy/src/mock.rs b/substrate/frame/beefy/src/mock.rs index 0b87de6bf5d79..10dc834971658 100644 --- a/substrate/frame/beefy/src/mock.rs +++ b/substrate/frame/beefy/src/mock.rs @@ -25,11 +25,17 @@ use frame_support::{ construct_runtime, derive_impl, parameter_types, traits::{ConstU32, ConstU64, KeyOwnerProofSystem, OnFinalize, OnInitialize}, }; +use pallet_mmr::DefaultBlockHashProvider; use pallet_session::historical as pallet_session_historical; +use sp_consensus_beefy::{BeefyAuthorityId, CheckForkEquivocationProof, ForkEquivocationProof}; use sp_core::{crypto::KeyTypeId, ConstU128}; use sp_runtime::{ - app_crypto::ecdsa::Public, curve::PiecewiseLinear, impl_opaque_keys, testing::TestXt, - traits::OpaqueKeys, BuildStorage, Perbill, + app_crypto::ecdsa::Public, + curve::PiecewiseLinear, + impl_opaque_keys, + testing::TestXt, + traits::{Hash as HashT, Header as HeaderT, Keccak256, OpaqueKeys}, + BuildStorage, Perbill, }; use sp_staking::{EraIndex, SessionIndex}; use sp_state_machine::BasicExternalities; @@ -54,6 +60,7 @@ construct_runtime!( Timestamp: pallet_timestamp, Balances: pallet_balances, Beefy: pallet_beefy, + Mmr: pallet_mmr, Staking: pallet_staking, Session: pallet_session, Offences: pallet_offences, @@ -80,6 +87,28 @@ parameter_types! { pub const ReportLongevity: u64 = BondingDuration::get() as u64 * SessionsPerEra::get() as u64 * Period::get(); pub const MaxSetIdSessionEntries: u32 = BondingDuration::get() * SessionsPerEra::get(); + + pub storage IsValidForkEquivocationProof: bool = true; +} + +pub struct MockForkEquivocationProofChecker; + +impl CheckForkEquivocationProof, Header> + for MockForkEquivocationProofChecker +{ + type Hash = Keccak256; + fn check_fork_equivocation_proof( + _proof: &ForkEquivocationProof::Output>, + ) -> Result<(), crate::pallet::Error> + where + Id: BeefyAuthorityId + PartialEq, + MsgHash: HashT, + { + match IsValidForkEquivocationProof::get() { + true => Ok(()), + false => Err(crate::pallet::Error::InvalidForkEquivocationProof), + } + } } impl pallet_beefy::Config for Test { @@ -88,12 +117,26 @@ impl pallet_beefy::Config for Test { type MaxNominators = ConstU32<1000>; type MaxSetIdSessionEntries = MaxSetIdSessionEntries; type OnNewValidatorSet = (); + type CheckForkEquivocationProof = MockForkEquivocationProofChecker; type WeightInfo = (); type KeyOwnerProof = >::Proof; type EquivocationReportSystem = super::EquivocationReportSystem; } +impl pallet_mmr::Config for Test { + const INDEXING_PREFIX: &'static [u8] = b"mmr"; + + type Hashing = Keccak256; + + type LeafData = pallet_mmr::ParentNumberAndHash; + + type OnNewRoot = (); + type BlockHashProvider = DefaultBlockHashProvider; + + type WeightInfo = (); +} + parameter_types! { pub const DisabledValidatorsThreshold: Perbill = Perbill::from_percent(33); } @@ -255,7 +298,7 @@ impl ExtBuilder { let staking_config = pallet_staking::GenesisConfig:: { stakers, - validator_count: 2, + validator_count: self.authorities.len() as u32 - 1, force_era: pallet_staking::Forcing::ForceNew, minimum_validator_count: 0, invulnerables: vec![], @@ -314,6 +357,7 @@ pub fn start_session(session_index: SessionIndex) { Session::on_initialize(System::block_number()); Staking::on_initialize(System::block_number()); Beefy::on_initialize(System::block_number()); + Mmr::on_initialize(System::block_number()); } assert_eq!(Session::current_index(), session_index); diff --git a/substrate/frame/beefy/src/tests.rs b/substrate/frame/beefy/src/tests.rs index 6a6aa245ce1f9..340f589d09236 100644 --- a/substrate/frame/beefy/src/tests.rs +++ b/substrate/frame/beefy/src/tests.rs @@ -24,11 +24,15 @@ use frame_support::{ traits::{Currency, KeyOwnerProofSystem, OnInitialize}, }; use sp_consensus_beefy::{ - check_equivocation_proof, + check_double_voting_proof, known_payloads::MMR_ROOT_ID, - test_utils::{generate_equivocation_proof, Keyring as BeefyKeyring}, - Payload, ValidatorSet, KEY_TYPE as BEEFY_KEY_TYPE, + test_utils::{ + generate_double_voting_proof, generate_fork_equivocation_proof_sc, + generate_fork_equivocation_proof_vote, Keyring as BeefyKeyring, + }, + Commitment, Payload, ValidatorSet, KEY_TYPE as BEEFY_KEY_TYPE, }; +use sp_core::offchain::{testing::TestOffchainExt, OffchainDbExt, OffchainWorkerExt}; use sp_runtime::DigestItem; use crate::{self as beefy, mock::*, Call, Config, Error, Weight, WeightInfo}; @@ -90,7 +94,8 @@ fn session_change_updates_authorities() { assert!(2 == beefy::ValidatorSetId::::get()); let want = beefy_log(ConsensusLog::AuthoritiesChange( - ValidatorSet::new(vec![mock_beefy_id(2), mock_beefy_id(4)], 2).unwrap(), + ValidatorSet::new(vec![mock_beefy_id(2), mock_beefy_id(3), mock_beefy_id(4)], 2) + .unwrap(), )); let log = System::digest().logs[1].clone(); @@ -117,9 +122,9 @@ fn session_change_updates_next_authorities() { let next_authorities = beefy::NextAuthorities::::get(); - assert_eq!(next_authorities.len(), 2); + assert_eq!(next_authorities.len(), 3); assert_eq!(want[1], next_authorities[0]); - assert_eq!(want[3], next_authorities[1]); + assert_eq!(want[3], next_authorities[2]); }); } @@ -166,7 +171,7 @@ fn validator_set_updates_work() { assert_eq!(vs.id(), 2u64); assert_eq!(want[1], vs.validators()[0]); - assert_eq!(want[3], vs.validators()[1]); + assert_eq!(want[3], vs.validators()[2]); }); } @@ -208,7 +213,8 @@ fn cleans_up_old_set_id_session_mappings() { /// Returns a list with 3 authorities with known keys: /// Alice, Bob and Charlie. pub fn test_authorities() -> Vec { - let authorities = vec![BeefyKeyring::Alice, BeefyKeyring::Bob, BeefyKeyring::Charlie]; + let authorities = + vec![BeefyKeyring::Alice, BeefyKeyring::Bob, BeefyKeyring::Charlie, BeefyKeyring::Dave]; authorities.into_iter().map(|id| id.public()).collect() } @@ -222,51 +228,53 @@ fn should_sign_and_verify() { // generate an equivocation proof, with two votes in the same round for // same payload signed by the same key - let equivocation_proof = generate_equivocation_proof( + let equivocation_proof = generate_double_voting_proof( (1, payload1.clone(), set_id, &BeefyKeyring::Bob), (1, payload1.clone(), set_id, &BeefyKeyring::Bob), ); // expect invalid equivocation proof - assert!(!check_equivocation_proof::<_, _, Keccak256>(&equivocation_proof)); + assert!(!check_double_voting_proof::<_, _, Keccak256>(&equivocation_proof)); // generate an equivocation proof, with two votes in different rounds for // different payloads signed by the same key - let equivocation_proof = generate_equivocation_proof( + let equivocation_proof = generate_double_voting_proof( (1, payload1.clone(), set_id, &BeefyKeyring::Bob), (2, payload2.clone(), set_id, &BeefyKeyring::Bob), ); // expect invalid equivocation proof - assert!(!check_equivocation_proof::<_, _, Keccak256>(&equivocation_proof)); + assert!(!check_double_voting_proof::<_, _, Keccak256>(&equivocation_proof)); // generate an equivocation proof, with two votes by different authorities - let equivocation_proof = generate_equivocation_proof( + let equivocation_proof = generate_double_voting_proof( (1, payload1.clone(), set_id, &BeefyKeyring::Alice), (1, payload2.clone(), set_id, &BeefyKeyring::Bob), ); // expect invalid equivocation proof - assert!(!check_equivocation_proof::<_, _, Keccak256>(&equivocation_proof)); + assert!(!check_double_voting_proof::<_, _, Keccak256>(&equivocation_proof)); // generate an equivocation proof, with two votes in different set ids - let equivocation_proof = generate_equivocation_proof( + let equivocation_proof = generate_double_voting_proof( (1, payload1.clone(), set_id, &BeefyKeyring::Bob), (1, payload2.clone(), set_id + 1, &BeefyKeyring::Bob), ); // expect invalid equivocation proof - assert!(!check_equivocation_proof::<_, _, Keccak256>(&equivocation_proof)); + assert!(!check_double_voting_proof::<_, _, Keccak256>(&equivocation_proof)); // generate an equivocation proof, with two votes in the same round for // different payloads signed by the same key let payload2 = Payload::from_single_entry(MMR_ROOT_ID, vec![128]); - let equivocation_proof = generate_equivocation_proof( + let equivocation_proof = generate_double_voting_proof( (1, payload1, set_id, &BeefyKeyring::Bob), (1, payload2, set_id, &BeefyKeyring::Bob), ); // expect valid equivocation proof - assert!(check_equivocation_proof::<_, _, Keccak256>(&equivocation_proof)); + assert!(check_double_voting_proof::<_, _, Keccak256>(&equivocation_proof)); } +// vote equivocation report tests +// TODO: deduplicate by extracting common test structure of equivocation classes #[test] -fn report_equivocation_current_set_works() { +fn report_vote_equivocation_current_set_works() { let authorities = test_authorities(); ExtBuilder::default().add_authorities(authorities).build_and_execute(|| { @@ -292,7 +300,7 @@ fn report_equivocation_current_set_works() { ); } - assert_eq!(authorities.len(), 2); + assert_eq!(authorities.len(), 3); let equivocation_authority_index = 1; let equivocation_key = &authorities[equivocation_authority_index]; let equivocation_keyring = BeefyKeyring::from_public(equivocation_key).unwrap(); @@ -301,7 +309,7 @@ fn report_equivocation_current_set_works() { let payload2 = Payload::from_single_entry(MMR_ROOT_ID, vec![128]); // generate an equivocation proof, with two votes in the same round for // different payloads signed by the same key - let equivocation_proof = generate_equivocation_proof( + let equivocation_proof = generate_double_voting_proof( (block_num, payload1, set_id, &equivocation_keyring), (block_num, payload2, set_id, &equivocation_keyring), ); @@ -310,7 +318,7 @@ fn report_equivocation_current_set_works() { let key_owner_proof = Historical::prove((BEEFY_KEY_TYPE, &equivocation_key)).unwrap(); // report the equivocation and the tx should be dispatched successfully - assert_ok!(Beefy::report_equivocation_unsigned( + assert_ok!(Beefy::report_vote_equivocation_unsigned( RuntimeOrigin::none(), Box::new(equivocation_proof), key_owner_proof, @@ -346,7 +354,7 @@ fn report_equivocation_current_set_works() { } #[test] -fn report_equivocation_old_set_works() { +fn report_vote_equivocation_old_set_works() { let authorities = test_authorities(); ExtBuilder::default().add_authorities(authorities).build_and_execute(|| { @@ -358,7 +366,7 @@ fn report_equivocation_old_set_works() { let validators = Session::validators(); let old_set_id = validator_set.id(); - assert_eq!(authorities.len(), 2); + assert_eq!(authorities.len(), 3); let equivocation_authority_index = 0; let equivocation_key = &authorities[equivocation_authority_index]; @@ -387,13 +395,13 @@ fn report_equivocation_old_set_works() { let payload1 = Payload::from_single_entry(MMR_ROOT_ID, vec![42]); let payload2 = Payload::from_single_entry(MMR_ROOT_ID, vec![128]); // generate an equivocation proof for the old set, - let equivocation_proof = generate_equivocation_proof( + let equivocation_proof = generate_double_voting_proof( (block_num, payload1, old_set_id, &equivocation_keyring), (block_num, payload2, old_set_id, &equivocation_keyring), ); // report the equivocation and the tx should be dispatched successfully - assert_ok!(Beefy::report_equivocation_unsigned( + assert_ok!(Beefy::report_vote_equivocation_unsigned( RuntimeOrigin::none(), Box::new(equivocation_proof), key_owner_proof, @@ -429,7 +437,7 @@ fn report_equivocation_old_set_works() { } #[test] -fn report_equivocation_invalid_set_id() { +fn report_vote_equivocation_invalid_set_id() { let authorities = test_authorities(); ExtBuilder::default().add_authorities(authorities).build_and_execute(|| { @@ -449,25 +457,25 @@ fn report_equivocation_invalid_set_id() { let payload1 = Payload::from_single_entry(MMR_ROOT_ID, vec![42]); let payload2 = Payload::from_single_entry(MMR_ROOT_ID, vec![128]); // generate an equivocation for a future set - let equivocation_proof = generate_equivocation_proof( + let equivocation_proof = generate_double_voting_proof( (block_num, payload1, set_id + 1, &equivocation_keyring), (block_num, payload2, set_id + 1, &equivocation_keyring), ); // the call for reporting the equivocation should error assert_err!( - Beefy::report_equivocation_unsigned( + Beefy::report_vote_equivocation_unsigned( RuntimeOrigin::none(), Box::new(equivocation_proof), key_owner_proof, ), - Error::::InvalidEquivocationProof, + Error::::InvalidEquivocationProofSession, ); }); } #[test] -fn report_equivocation_invalid_session() { +fn report_vote_equivocation_invalid_session() { let authorities = test_authorities(); ExtBuilder::default().add_authorities(authorities).build_and_execute(|| { @@ -491,7 +499,7 @@ fn report_equivocation_invalid_session() { let payload1 = Payload::from_single_entry(MMR_ROOT_ID, vec![42]); let payload2 = Payload::from_single_entry(MMR_ROOT_ID, vec![128]); // generate an equivocation proof at following era set id = 2 - let equivocation_proof = generate_equivocation_proof( + let equivocation_proof = generate_double_voting_proof( (block_num, payload1, set_id, &equivocation_keyring), (block_num, payload2, set_id, &equivocation_keyring), ); @@ -499,18 +507,18 @@ fn report_equivocation_invalid_session() { // report an equivocation for the current set using an key ownership // proof from the previous set, the session should be invalid. assert_err!( - Beefy::report_equivocation_unsigned( + Beefy::report_vote_equivocation_unsigned( RuntimeOrigin::none(), Box::new(equivocation_proof), key_owner_proof, ), - Error::::InvalidEquivocationProof, + Error::::InvalidEquivocationProofSession, ); }); } #[test] -fn report_equivocation_invalid_key_owner_proof() { +fn report_vote_equivocation_invalid_key_owner_proof() { let authorities = test_authorities(); ExtBuilder::default().add_authorities(authorities).build_and_execute(|| { @@ -535,9 +543,9 @@ fn report_equivocation_invalid_key_owner_proof() { let payload1 = Payload::from_single_entry(MMR_ROOT_ID, vec![42]); let payload2 = Payload::from_single_entry(MMR_ROOT_ID, vec![128]); // generate an equivocation proof for the authority at index 0 - let equivocation_proof = generate_equivocation_proof( - (block_num, payload1, set_id + 1, &equivocation_keyring), - (block_num, payload2, set_id + 1, &equivocation_keyring), + let equivocation_proof = generate_double_voting_proof( + (block_num, payload1, set_id, &equivocation_keyring), + (block_num, payload2, set_id, &equivocation_keyring), ); // we need to start a new era otherwise the key ownership proof won't be @@ -547,7 +555,7 @@ fn report_equivocation_invalid_key_owner_proof() { // 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!( - Beefy::report_equivocation_unsigned( + Beefy::report_vote_equivocation_unsigned( RuntimeOrigin::none(), Box::new(equivocation_proof), invalid_key_owner_proof, @@ -558,7 +566,7 @@ fn report_equivocation_invalid_key_owner_proof() { } #[test] -fn report_equivocation_invalid_equivocation_proof() { +fn report_vote_equivocation_invalid_equivocation_proof() { let authorities = test_authorities(); ExtBuilder::default().add_authorities(authorities).build_and_execute(|| { @@ -578,12 +586,12 @@ fn report_equivocation_invalid_equivocation_proof() { let assert_invalid_equivocation_proof = |equivocation_proof| { assert_err!( - Beefy::report_equivocation_unsigned( + Beefy::report_vote_equivocation_unsigned( RuntimeOrigin::none(), Box::new(equivocation_proof), key_owner_proof.clone(), ), - Error::::InvalidEquivocationProof, + Error::::InvalidVoteEquivocationProof, ); }; @@ -594,31 +602,31 @@ fn report_equivocation_invalid_equivocation_proof() { // both votes target the same block number and payload, // there is no equivocation. - assert_invalid_equivocation_proof(generate_equivocation_proof( + assert_invalid_equivocation_proof(generate_double_voting_proof( (block_num, payload1.clone(), set_id, &equivocation_keyring), (block_num, payload1.clone(), set_id, &equivocation_keyring), )); // votes targeting different rounds, there is no equivocation. - assert_invalid_equivocation_proof(generate_equivocation_proof( + assert_invalid_equivocation_proof(generate_double_voting_proof( (block_num, payload1.clone(), set_id, &equivocation_keyring), (block_num + 1, payload2.clone(), set_id, &equivocation_keyring), )); // votes signed with different authority keys - assert_invalid_equivocation_proof(generate_equivocation_proof( + assert_invalid_equivocation_proof(generate_double_voting_proof( (block_num, payload1.clone(), set_id, &equivocation_keyring), (block_num, payload1.clone(), set_id, &BeefyKeyring::Charlie), )); // votes signed with a key that isn't part of the authority set - assert_invalid_equivocation_proof(generate_equivocation_proof( + assert_invalid_equivocation_proof(generate_double_voting_proof( (block_num, payload1.clone(), set_id, &equivocation_keyring), (block_num, payload1.clone(), set_id, &BeefyKeyring::Dave), )); // votes targeting different set ids - assert_invalid_equivocation_proof(generate_equivocation_proof( + assert_invalid_equivocation_proof(generate_double_voting_proof( (block_num, payload1, set_id, &equivocation_keyring), (block_num, payload2, set_id + 1, &equivocation_keyring), )); @@ -626,7 +634,7 @@ fn report_equivocation_invalid_equivocation_proof() { } #[test] -fn report_equivocation_validate_unsigned_prevents_duplicates() { +fn report_vote_equivocation_validate_unsigned_prevents_duplicates() { use sp_runtime::transaction_validity::{ InvalidTransaction, TransactionPriority, TransactionSource, TransactionValidity, ValidTransaction, @@ -649,14 +657,14 @@ fn report_equivocation_validate_unsigned_prevents_duplicates() { let payload1 = Payload::from_single_entry(MMR_ROOT_ID, vec![42]); let payload2 = Payload::from_single_entry(MMR_ROOT_ID, vec![128]); - let equivocation_proof = generate_equivocation_proof( + let equivocation_proof = generate_double_voting_proof( (block_num, payload1, set_id, &equivocation_keyring), (block_num, payload2, set_id, &equivocation_keyring), ); let key_owner_proof = Historical::prove((BEEFY_KEY_TYPE, &equivocation_key)).unwrap(); - let call = Call::report_equivocation_unsigned { + let call = Call::report_vote_equivocation_unsigned { equivocation_proof: Box::new(equivocation_proof.clone()), key_owner_proof: key_owner_proof.clone(), }; @@ -671,7 +679,7 @@ fn report_equivocation_validate_unsigned_prevents_duplicates() { ); // the transaction is valid when passed as local - let tx_tag = (equivocation_key, set_id, 3u64); + let tx_tag = (vec![equivocation_key], set_id, 3u64); assert_eq!( ::validate_unsigned( @@ -691,7 +699,7 @@ fn report_equivocation_validate_unsigned_prevents_duplicates() { assert_ok!(::pre_dispatch(&call)); // we submit the report - Beefy::report_equivocation_unsigned( + Beefy::report_vote_equivocation_unsigned( RuntimeOrigin::none(), Box::new(equivocation_proof), key_owner_proof, @@ -716,11 +724,11 @@ fn report_equivocation_validate_unsigned_prevents_duplicates() { } #[test] -fn report_equivocation_has_valid_weight() { +fn report_vote_equivocation_has_valid_weight() { // the weight depends on the size of the validator set, // but there's a lower bound of 100 validators. assert!((1..=100) - .map(|validators| ::WeightInfo::report_equivocation(validators, 1000)) + .map(|validators| ::WeightInfo::report_vote_equivocation(validators, 1000)) .collect::>() .windows(2) .all(|w| w[0] == w[1])); @@ -728,14 +736,14 @@ fn report_equivocation_has_valid_weight() { // after 100 validators the weight should keep increasing // with every extra validator. assert!((100..=1000) - .map(|validators| ::WeightInfo::report_equivocation(validators, 1000)) + .map(|validators| ::WeightInfo::report_vote_equivocation(validators, 1000)) .collect::>() .windows(2) .all(|w| w[0].ref_time() < w[1].ref_time())); } #[test] -fn valid_equivocation_reports_dont_pay_fees() { +fn valid_vote_equivocation_reports_dont_pay_fees() { let authorities = test_authorities(); ExtBuilder::default().add_authorities(authorities).build_and_execute(|| { @@ -753,7 +761,7 @@ fn valid_equivocation_reports_dont_pay_fees() { // generate equivocation proof let payload1 = Payload::from_single_entry(MMR_ROOT_ID, vec![42]); let payload2 = Payload::from_single_entry(MMR_ROOT_ID, vec![128]); - let equivocation_proof = generate_equivocation_proof( + let equivocation_proof = generate_double_voting_proof( (block_num, payload1, set_id, &equivocation_keyring), (block_num, payload2, set_id, &equivocation_keyring), ); @@ -762,7 +770,7 @@ fn valid_equivocation_reports_dont_pay_fees() { let key_owner_proof = Historical::prove((BEEFY_KEY_TYPE, &equivocation_key)).unwrap(); // check the dispatch info for the call. - let info = Call::::report_equivocation_unsigned { + let info = Call::::report_vote_equivocation_unsigned { equivocation_proof: Box::new(equivocation_proof.clone()), key_owner_proof: key_owner_proof.clone(), } @@ -773,7 +781,7 @@ fn valid_equivocation_reports_dont_pay_fees() { assert_eq!(info.pays_fee, Pays::Yes); // report the equivocation. - let post_info = Beefy::report_equivocation_unsigned( + let post_info = Beefy::report_vote_equivocation_unsigned( RuntimeOrigin::none(), Box::new(equivocation_proof.clone()), key_owner_proof.clone(), @@ -787,7 +795,7 @@ fn valid_equivocation_reports_dont_pay_fees() { // report the equivocation again which is invalid now since it is // duplicate. - let post_info = Beefy::report_equivocation_unsigned( + let post_info = Beefy::report_vote_equivocation_unsigned( RuntimeOrigin::none(), Box::new(equivocation_proof), key_owner_proof, @@ -802,6 +810,1771 @@ fn valid_equivocation_reports_dont_pay_fees() { }) } +// fork equivocation (via vote) report tests +// TODO: deduplicate by extracting common test structure of equivocation classes +#[test] +fn report_fork_equivocation_vote_current_set_works() { + let authorities = test_authorities(); + + let mut ext = ExtBuilder::default().add_authorities(authorities).build(); + let (offchain, _offchain_state) = TestOffchainExt::with_offchain_db(ext.offchain_db()); + ext.register_extension(OffchainDbExt::new(offchain.clone())); + ext.register_extension(OffchainWorkerExt::new(offchain)); + + let mut era = 1; + let block_num = ext.execute_with(|| { + assert_eq!(Staking::current_era(), Some(0)); + assert_eq!(Session::current_index(), 0); + start_era(era); + + let block_num = System::block_number(); + era += 1; + start_era(era); + block_num + }); + ext.persist_offchain_overlay(); + + ext.execute_with(|| { + let validator_set = Beefy::validator_set().unwrap(); + let authorities = validator_set.validators(); + let set_id = validator_set.id(); + let validators = Session::validators(); + + // make sure that all validators have the same balance + for validator in &validators { + assert_eq!(Balances::total_balance(validator), 10_000_000); + assert_eq!(Staking::slashable_balance_of(validator), 10_000); + + assert_eq!( + Staking::eras_stakers(era, validator), + pallet_staking::Exposure { total: 10_000, own: 10_000, others: vec![] }, + ); + } + + assert_eq!(authorities.len(), 3); + let equivocation_authority_index = 1; + let equivocation_key = &authorities[equivocation_authority_index]; + let equivocation_keyring = BeefyKeyring::from_public(equivocation_key).unwrap(); + + let payload = Payload::from_single_entry(MMR_ROOT_ID, vec![42]); + let ancestry_proof = Mmr::generate_ancestry_proof(block_num, None).unwrap(); + + // generate an fork equivocation proof, with a vote in the same round for a + // different payload than finalized + let equivocation_proof = generate_fork_equivocation_proof_vote( + (block_num, payload, set_id, &equivocation_keyring), + None, + Some(ancestry_proof), + ); + + // create the key ownership proof + let key_owner_proof = Historical::prove((BEEFY_KEY_TYPE, &equivocation_key)).unwrap(); + + // report the equivocation and the tx should be dispatched successfully + assert_ok!(Beefy::report_fork_equivocation_unsigned( + RuntimeOrigin::none(), + Box::new(equivocation_proof), + vec![key_owner_proof], + ),); + + era += 1; + start_era(era); + + // check that the balance of 0-th validator is slashed 100%. + let equivocation_validator_id = validators[equivocation_authority_index]; + + assert_eq!(Balances::total_balance(&equivocation_validator_id), 10_000_000 - 10_000); + assert_eq!(Staking::slashable_balance_of(&equivocation_validator_id), 0); + assert_eq!( + Staking::eras_stakers(era, &equivocation_validator_id), + pallet_staking::Exposure { total: 0, own: 0, others: vec![] }, + ); + + // check that the balances of all other validators are left intact. + for validator in &validators { + if *validator == equivocation_validator_id { + continue + } + + assert_eq!(Balances::total_balance(validator), 10_000_000); + assert_eq!(Staking::slashable_balance_of(validator), 10_000); + + assert_eq!( + Staking::eras_stakers(era, validator), + pallet_staking::Exposure { total: 10_000, own: 10_000, others: vec![] }, + ); + } + }); +} + +#[test] +fn report_fork_equivocation_vote_old_set_works() { + let authorities = test_authorities(); + + let mut ext = ExtBuilder::default().add_authorities(authorities).build(); + let (offchain, _offchain_state) = TestOffchainExt::with_offchain_db(ext.offchain_db()); + ext.register_extension(OffchainDbExt::new(offchain.clone())); + ext.register_extension(OffchainWorkerExt::new(offchain)); + + let mut era = 1; + let ( + block_num, + validators, + old_set_id, + equivocation_authority_index, + equivocation_key, + key_owner_proof, + ) = ext.execute_with(|| { + start_era(era); + let block_num = System::block_number(); + era += 1; + start_era(era); + + let validator_set = Beefy::validator_set().unwrap(); + let authorities = validator_set.validators(); + let validators = Session::validators(); + let old_set_id = validator_set.id(); + + assert_eq!(authorities.len(), 3); + let equivocation_authority_index = 0; + let equivocation_key = authorities[equivocation_authority_index].clone(); + + // create the key ownership proof in the "old" set + let key_owner_proof = Historical::prove((BEEFY_KEY_TYPE, &&equivocation_key)).unwrap(); + + era += 1; + start_era(era); + ( + block_num, + validators, + old_set_id, + equivocation_authority_index, + equivocation_key, + key_owner_proof, + ) + }); + ext.persist_offchain_overlay(); + + ext.execute_with(|| { + // make sure that all authorities have the same balance + for validator in &validators { + assert_eq!(Balances::total_balance(validator), 10_000_000); + assert_eq!(Staking::slashable_balance_of(validator), 10_000); + + assert_eq!( + Staking::eras_stakers(2, validator), + pallet_staking::Exposure { total: 10_000, own: 10_000, others: vec![] }, + ); + } + + let validator_set = Beefy::validator_set().unwrap(); + let new_set_id = validator_set.id(); + assert_eq!(old_set_id + 3, new_set_id); + + let equivocation_keyring = BeefyKeyring::from_public(&equivocation_key).unwrap(); + + let payload = Payload::from_single_entry(MMR_ROOT_ID, vec![42]); + let ancestry_proof = Mmr::generate_ancestry_proof(block_num, None).unwrap(); + + // generate an fork equivocation proof, with a vote in the same round for a + // different payload than finalized + let equivocation_proof = generate_fork_equivocation_proof_vote( + (block_num, payload, old_set_id, &equivocation_keyring), + None, + Some(ancestry_proof), + ); + + // report the equivocation and the tx should be dispatched successfully + assert_ok!(Beefy::report_fork_equivocation_unsigned( + RuntimeOrigin::none(), + Box::new(equivocation_proof), + vec![key_owner_proof], + ),); + + era += 1; + start_era(era); + + // check that the balance of 0-th validator is slashed 100%. + let equivocation_validator_id = validators[equivocation_authority_index]; + + assert_eq!(Balances::total_balance(&equivocation_validator_id), 10_000_000 - 10_000); + assert_eq!(Staking::slashable_balance_of(&equivocation_validator_id), 0); + assert_eq!( + Staking::eras_stakers(era, &equivocation_validator_id), + pallet_staking::Exposure { total: 0, own: 0, others: vec![] }, + ); + + // check that the balances of all other validators are left intact. + for validator in &validators { + if *validator == equivocation_validator_id { + continue + } + + assert_eq!(Balances::total_balance(validator), 10_000_000); + assert_eq!(Staking::slashable_balance_of(validator), 10_000); + + assert_eq!( + Staking::eras_stakers(3, validator), + pallet_staking::Exposure { total: 10_000, own: 10_000, others: vec![] }, + ); + } + }); +} + +#[test] +fn report_fork_equivocation_vote_future_block_works() { + let authorities = test_authorities(); + + let mut ext = ExtBuilder::default().add_authorities(authorities).build(); + let (offchain, _offchain_state) = TestOffchainExt::with_offchain_db(ext.offchain_db()); + ext.register_extension(OffchainDbExt::new(offchain.clone())); + ext.register_extension(OffchainWorkerExt::new(offchain)); + + let mut era = 2; + ext.execute_with(|| { + assert_eq!(Staking::current_era(), Some(0)); + assert_eq!(Session::current_index(), 0); + start_era(era); + }); + ext.persist_offchain_overlay(); + + ext.execute_with(|| { + let validator_set = Beefy::validator_set().unwrap(); + let authorities = validator_set.validators(); + let set_id = validator_set.id(); + let validators = Session::validators(); + + // make sure that all validators have the same balance + for validator in &validators { + assert_eq!(Balances::total_balance(validator), 10_000_000); + assert_eq!(Staking::slashable_balance_of(validator), 10_000); + + assert_eq!( + Staking::eras_stakers(era, validator), + pallet_staking::Exposure { total: 10_000, own: 10_000, others: vec![] }, + ); + } + + assert_eq!(authorities.len(), 3); + let equivocation_authority_index = 1; + let equivocation_key = &authorities[equivocation_authority_index]; + let equivocation_keyring = BeefyKeyring::from_public(equivocation_key).unwrap(); + + let payload = Payload::from_single_entry(MMR_ROOT_ID, vec![42]); + let block_num = System::block_number() + 20; + + // generate an fork equivocation proof, with a vote in the same round for a future block + let equivocation_proof = generate_fork_equivocation_proof_vote( + (block_num, payload, set_id, &equivocation_keyring), + None, + None, + ); + + // create the key ownership proof + let key_owner_proof = Historical::prove((BEEFY_KEY_TYPE, &equivocation_key)).unwrap(); + + // report the equivocation and the tx should be dispatched successfully + assert_ok!(Beefy::report_fork_equivocation_unsigned( + RuntimeOrigin::none(), + Box::new(equivocation_proof), + vec![key_owner_proof], + ),); + + era += 1; + start_era(era); + + // check that the balance of 0-th validator is slashed 100%. + let equivocation_validator_id = validators[equivocation_authority_index]; + + assert_eq!(Balances::total_balance(&equivocation_validator_id), 10_000_000 - 10_000); + assert_eq!(Staking::slashable_balance_of(&equivocation_validator_id), 0); + assert_eq!( + Staking::eras_stakers(era, &equivocation_validator_id), + pallet_staking::Exposure { total: 0, own: 0, others: vec![] }, + ); + + // check that the balances of all other validators are left intact. + for validator in &validators { + if *validator == equivocation_validator_id { + continue + } + + assert_eq!(Balances::total_balance(validator), 10_000_000); + assert_eq!(Staking::slashable_balance_of(validator), 10_000); + + assert_eq!( + Staking::eras_stakers(era, validator), + pallet_staking::Exposure { total: 10_000, own: 10_000, others: vec![] }, + ); + } + }); +} + +#[test] +fn report_fork_equivocation_vote_invalid_set_id() { + let authorities = test_authorities(); + + let mut ext = ExtBuilder::default().add_authorities(authorities).build(); + let (offchain, _offchain_state) = TestOffchainExt::with_offchain_db(ext.offchain_db()); + ext.register_extension(OffchainDbExt::new(offchain.clone())); + ext.register_extension(OffchainWorkerExt::new(offchain)); + + let block_num = ext.execute_with(|| { + let mut era = 1; + start_era(era); + let block_num = System::block_number(); + + era += 1; + start_era(era); + block_num + }); + ext.persist_offchain_overlay(); + + ext.execute_with(|| { + let validator_set = Beefy::validator_set().unwrap(); + let authorities = validator_set.validators(); + let set_id = validator_set.id(); + + let equivocation_authority_index = 0; + let equivocation_key = &authorities[equivocation_authority_index]; + let equivocation_keyring = BeefyKeyring::from_public(equivocation_key).unwrap(); + + let key_owner_proof = Historical::prove((BEEFY_KEY_TYPE, &equivocation_key)).unwrap(); + + let payload = Payload::from_single_entry(MMR_ROOT_ID, vec![42]); + let ancestry_proof = Mmr::generate_ancestry_proof(block_num, None).unwrap(); + // generate an equivocation for a future set + let equivocation_proof = generate_fork_equivocation_proof_vote( + (block_num, payload, set_id + 1, &equivocation_keyring), + None, + Some(ancestry_proof), + ); + + // the call for reporting the equivocation should error + assert_err!( + Beefy::report_fork_equivocation_unsigned( + RuntimeOrigin::none(), + Box::new(equivocation_proof), + vec![key_owner_proof], + ), + Error::::InvalidEquivocationProofSession, + ); + }); +} + +#[test] +fn report_fork_equivocation_vote_invalid_session() { + let authorities = test_authorities(); + + let mut ext = ExtBuilder::default().add_authorities(authorities).build(); + let (offchain, _offchain_state) = TestOffchainExt::with_offchain_db(ext.offchain_db()); + ext.register_extension(OffchainDbExt::new(offchain.clone())); + ext.register_extension(OffchainWorkerExt::new(offchain)); + + let mut era = 1; + let (block_num, equivocation_keyring, key_owner_proof) = ext.execute_with(|| { + start_era(era); + let block_num = System::block_number(); + + era += 1; + start_era(era); + + let validator_set = Beefy::validator_set().unwrap(); + let authorities = validator_set.validators(); + + let equivocation_authority_index = 0; + let equivocation_key = &authorities[equivocation_authority_index]; + let equivocation_keyring = BeefyKeyring::from_public(equivocation_key).unwrap(); + + // generate a key ownership proof at current era set id + let key_owner_proof = Historical::prove((BEEFY_KEY_TYPE, &equivocation_key)).unwrap(); + + era += 1; + start_era(era); + (block_num, equivocation_keyring, key_owner_proof) + }); + ext.persist_offchain_overlay(); + + ext.execute_with(|| { + let set_id = Beefy::validator_set().unwrap().id(); + + let payload = Payload::from_single_entry(MMR_ROOT_ID, vec![42]); + let ancestry_proof = Mmr::generate_ancestry_proof(block_num, None).unwrap(); + // generate an equivocation proof at following era set id = 3 + let equivocation_proof = generate_fork_equivocation_proof_vote( + (block_num, payload, set_id, &equivocation_keyring), + None, + Some(ancestry_proof), + ); + + // report an equivocation for the current set using an key ownership + // proof from the previous set, the session should be invalid. + assert_err!( + Beefy::report_fork_equivocation_unsigned( + RuntimeOrigin::none(), + Box::new(equivocation_proof), + vec![key_owner_proof], + ), + Error::::InvalidEquivocationProofSession, + ); + }); +} + +#[test] +fn report_fork_equivocation_vote_invalid_key_owner_proof() { + let authorities = test_authorities(); + + let mut ext = ExtBuilder::default().add_authorities(authorities).build(); + let (offchain, _offchain_state) = TestOffchainExt::with_offchain_db(ext.offchain_db()); + ext.register_extension(OffchainDbExt::new(offchain.clone())); + ext.register_extension(OffchainWorkerExt::new(offchain)); + + let mut era = 1; + let block_num = ext.execute_with(|| { + start_era(era); + let block_num = System::block_number(); + + era += 1; + start_era(era); + block_num + }); + ext.persist_offchain_overlay(); + + ext.execute_with(|| { + let validator_set = Beefy::validator_set().unwrap(); + let authorities = validator_set.validators(); + let set_id = validator_set.id(); + + let invalid_owner_authority_index = 1; + let invalid_owner_key = &authorities[invalid_owner_authority_index]; + + // generate a key ownership proof for the authority at index 1 + let invalid_key_owner_proof = + Historical::prove((BEEFY_KEY_TYPE, &invalid_owner_key)).unwrap(); + + let equivocation_authority_index = 0; + let equivocation_key = &authorities[equivocation_authority_index]; + let equivocation_keyring = BeefyKeyring::from_public(equivocation_key).unwrap(); + + let payload = Payload::from_single_entry(MMR_ROOT_ID, vec![42]); + let ancestry_proof = Mmr::generate_ancestry_proof(block_num, None).unwrap(); + // generate an equivocation for a future set + let equivocation_proof = generate_fork_equivocation_proof_vote( + (block_num, payload, set_id, &equivocation_keyring), + None, + Some(ancestry_proof), + ); + + // 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 + era += 1; + start_era(era); + + // 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!( + Beefy::report_fork_equivocation_unsigned( + RuntimeOrigin::none(), + Box::new(equivocation_proof), + vec![invalid_key_owner_proof], + ), + Error::::InvalidKeyOwnershipProof, + ); + }); +} + +#[test] +fn report_fork_equivocation_vote_invalid_equivocation_proof() { + let authorities = test_authorities(); + + let mut ext = ExtBuilder::default().add_authorities(authorities).build(); + let (offchain, _offchain_state) = TestOffchainExt::with_offchain_db(ext.offchain_db()); + ext.register_extension(OffchainDbExt::new(offchain.clone())); + ext.register_extension(OffchainWorkerExt::new(offchain)); + + let mut era = 1; + let (block_num, set_id, equivocation_keyring, key_owner_proof) = ext.execute_with(|| { + start_era(era); + let block_num = System::block_number(); + + let validator_set = Beefy::validator_set().unwrap(); + let authorities = validator_set.validators(); + let set_id = validator_set.id(); + + let equivocation_authority_index = 0; + let equivocation_key = &authorities[equivocation_authority_index]; + let equivocation_keyring = BeefyKeyring::from_public(equivocation_key).unwrap(); + + // generate a key ownership proof at set id in era 1 + let key_owner_proof = Historical::prove((BEEFY_KEY_TYPE, &equivocation_key)).unwrap(); + + era += 1; + start_era(era); + (block_num, set_id, equivocation_keyring, key_owner_proof) + }); + ext.persist_offchain_overlay(); + + ext.execute_with(|| { + let payload = Payload::from_single_entry(MMR_ROOT_ID, vec![42]); + let ancestry_proof = Mmr::generate_ancestry_proof(block_num, None).unwrap(); + + // vote signed with a key that isn't part of the authority set + let equivocation_proof = generate_fork_equivocation_proof_vote( + (block_num, payload.clone(), set_id, &BeefyKeyring::Dave), + None, + Some(ancestry_proof.clone()), + ); + assert_err!( + Beefy::report_fork_equivocation_unsigned( + RuntimeOrigin::none(), + Box::new(equivocation_proof), + vec![key_owner_proof.clone()], + ), + Error::::InvalidKeyOwnershipProof, + ); + + // vote targets future set id + let equivocation_proof = generate_fork_equivocation_proof_vote( + (block_num, payload.clone(), set_id + 1, &equivocation_keyring), + None, + Some(ancestry_proof.clone()), + ); + assert_err!( + Beefy::report_fork_equivocation_unsigned( + RuntimeOrigin::none(), + Box::new(equivocation_proof), + vec![key_owner_proof.clone()], + ), + Error::::InvalidEquivocationProofSession, + ); + + // Simulate InvalidForkEquivocationProof error. + IsValidForkEquivocationProof::set(&false); + let equivocation_proof = generate_fork_equivocation_proof_vote( + (block_num + 1, payload.clone(), set_id, &equivocation_keyring), + None, + Some(ancestry_proof), + ); + assert_err!( + Beefy::report_fork_equivocation_unsigned( + RuntimeOrigin::none(), + Box::new(equivocation_proof), + vec![key_owner_proof.clone()], + ), + Error::::InvalidForkEquivocationProof, + ); + }); +} + +#[test] +fn report_fork_equivocation_vote_validate_unsigned_prevents_duplicates() { + use sp_runtime::transaction_validity::{ + InvalidTransaction, TransactionPriority, TransactionSource, TransactionValidity, + ValidTransaction, + }; + + let authorities = test_authorities(); + + let mut ext = ExtBuilder::default().add_authorities(authorities).build(); + let (offchain, _offchain_state) = TestOffchainExt::with_offchain_db(ext.offchain_db()); + ext.register_extension(OffchainDbExt::new(offchain.clone())); + ext.register_extension(OffchainWorkerExt::new(offchain)); + + let mut era = 1; + let block_num = ext.execute_with(|| { + start_era(era); + let block_num = System::block_number(); + + era += 1; + start_era(era); + block_num + }); + ext.persist_offchain_overlay(); + + ext.execute_with(|| { + let validator_set = Beefy::validator_set().unwrap(); + let authorities = validator_set.validators(); + let set_id = validator_set.id(); + + // generate and report an equivocation for the validator at index 0 + let equivocation_authority_index = 0; + let equivocation_key = &authorities[equivocation_authority_index]; + let equivocation_keyring = BeefyKeyring::from_public(equivocation_key).unwrap(); + + let payload = Payload::from_single_entry(MMR_ROOT_ID, vec![42]); + let ancestry_proof = Mmr::generate_ancestry_proof(block_num, None).unwrap(); + let equivocation_proof = generate_fork_equivocation_proof_vote( + (block_num, payload, set_id, &equivocation_keyring), + None, + Some(ancestry_proof), + ); + + let key_owner_proof = Historical::prove((BEEFY_KEY_TYPE, &equivocation_key)).unwrap(); + + let call = Call::report_fork_equivocation_unsigned { + equivocation_proof: Box::new(equivocation_proof.clone()), + key_owner_proofs: vec![key_owner_proof.clone()], + }; + + // only local/inblock reports are allowed + assert_eq!( + ::validate_unsigned( + TransactionSource::External, + &call, + ), + InvalidTransaction::Call.into(), + ); + + // the transaction is valid when passed as local + let tx_tag = (vec![equivocation_key], set_id, 3u64); + + let call_result = ::validate_unsigned( + TransactionSource::Local, + &call, + ); + + assert_eq!( + call_result, + TransactionValidity::Ok(ValidTransaction { + priority: TransactionPriority::max_value(), + requires: vec![], + provides: vec![("BeefyEquivocation", tx_tag.clone()).encode()], + longevity: ReportLongevity::get(), + propagate: false, + }) + ); + + assert_ok!(::pre_dispatch(&call)); + + // we submit the report + Beefy::report_fork_equivocation_unsigned( + RuntimeOrigin::none(), + Box::new(equivocation_proof), + vec![key_owner_proof], + ) + .unwrap(); + + // the report should now be considered stale and the transaction is invalid + // the check for staleness should be done on both `validate_unsigned` and on `pre_dispatch` + assert_err!( + ::validate_unsigned( + TransactionSource::Local, + &call, + ), + InvalidTransaction::Stale, + ); + + assert_err!( + ::pre_dispatch(&call), + InvalidTransaction::Stale, + ); + }); +} + +#[test] +fn valid_fork_equivocation_vote_reports_dont_pay_fees() { + let authorities = test_authorities(); + + let mut ext = ExtBuilder::default().add_authorities(authorities).build(); + let (offchain, _offchain_state) = TestOffchainExt::with_offchain_db(ext.offchain_db()); + ext.register_extension(OffchainDbExt::new(offchain.clone())); + ext.register_extension(OffchainWorkerExt::new(offchain)); + + let mut era = 1; + let block_num = ext.execute_with(|| { + start_era(era); + let block_num = System::block_number(); + + era += 1; + start_era(era); + block_num + }); + ext.persist_offchain_overlay(); + + ext.execute_with(|| { + let validator_set = Beefy::validator_set().unwrap(); + let authorities = validator_set.validators(); + let set_id = validator_set.id(); + + let equivocation_authority_index = 0; + let equivocation_key = &authorities[equivocation_authority_index]; + let equivocation_keyring = BeefyKeyring::from_public(equivocation_key).unwrap(); + + // generate equivocation proof + let payload = Payload::from_single_entry(MMR_ROOT_ID, vec![42]); + let ancestry_proof = Mmr::generate_ancestry_proof(block_num, None).unwrap(); + let equivocation_proof = generate_fork_equivocation_proof_vote( + (block_num, payload, set_id, &equivocation_keyring), + None, + Some(ancestry_proof), + ); + + // create the key ownership proof. + let key_owner_proof = Historical::prove((BEEFY_KEY_TYPE, &equivocation_key)).unwrap(); + + // check the dispatch info for the call. + let info = Call::::report_fork_equivocation_unsigned { + equivocation_proof: Box::new(equivocation_proof.clone()), + key_owner_proofs: vec![key_owner_proof.clone()], + } + .get_dispatch_info(); + + // it should have non-zero weight and the fee has to be paid. + assert!(info.weight.any_gt(Weight::zero())); + assert_eq!(info.pays_fee, Pays::Yes); + + // report the equivocation. + let post_info = Beefy::report_fork_equivocation_unsigned( + RuntimeOrigin::none(), + Box::new(equivocation_proof.clone()), + vec![key_owner_proof.clone()], + ) + .unwrap(); + + // the original weight should be kept, but given that the report + // is valid the fee is waived. + assert!(post_info.actual_weight.is_none()); + assert_eq!(post_info.pays_fee, Pays::No); + + // report the equivocation again which is invalid now since it is + // duplicate. + let post_info = Beefy::report_fork_equivocation_unsigned( + RuntimeOrigin::none(), + Box::new(equivocation_proof.clone()), + vec![key_owner_proof.clone()], + ) + .err() + .unwrap() + .post_info; + + // the fee is not waived and the original weight is kept. + assert!(post_info.actual_weight.is_none()); + assert_eq!(post_info.pays_fee, Pays::Yes); + }) +} + +// fork equivocation (via signed commitment) report tests +// TODO: deduplicate by extracting common test structure of equivocation classes +#[test] +fn report_fork_equivocation_sc_current_set_works() { + let authorities = test_authorities(); + + let mut ext = ExtBuilder::default().add_authorities(authorities).build(); + let (offchain, _offchain_state) = TestOffchainExt::with_offchain_db(ext.offchain_db()); + ext.register_extension(OffchainDbExt::new(offchain.clone())); + ext.register_extension(OffchainWorkerExt::new(offchain)); + + let mut era = 1; + let block_num = ext.execute_with(|| { + start_era(era); + let block_num = System::block_number(); + + era += 1; + start_era(era); + block_num + }); + ext.persist_offchain_overlay(); + + ext.execute_with(|| { + let validator_set = Beefy::validator_set().unwrap(); + let authorities = validator_set.validators(); + let set_id = validator_set.id(); + let validators = Session::validators(); + + // make sure that all validators have the same balance + for validator in &validators { + assert_eq!(Balances::total_balance(validator), 10_000_000); + assert_eq!(Staking::slashable_balance_of(validator), 10_000); + + assert_eq!( + Staking::eras_stakers(era, validator), + pallet_staking::Exposure { total: 10_000, own: 10_000, others: vec![] }, + ); + } + + assert_eq!(authorities.len(), 3); + let equivocation_authority_indices = [0, 2]; + let equivocation_keys = equivocation_authority_indices + .iter() + .map(|i| &authorities[*i]) + .collect::>(); + let equivocation_keyrings = equivocation_keys + .iter() + .map(|k| BeefyKeyring::from_public(k.clone()).unwrap()) + .collect(); + + let payload = Payload::from_single_entry(MMR_ROOT_ID, vec![42]); + let ancestry_proof = Mmr::generate_ancestry_proof(block_num, None).unwrap(); + let commitment = Commitment { validator_set_id: set_id, block_number: block_num, payload }; + // generate an fork equivocation proof, with a vote in the same round for a + // different payload than finalized + let equivocation_proof = generate_fork_equivocation_proof_sc( + commitment, + equivocation_keyrings, + None, + Some(ancestry_proof), + ); + + // create the key ownership proof + let key_owner_proofs = equivocation_keys + .iter() + .map(|k| Historical::prove((BEEFY_KEY_TYPE, &k)).unwrap()) + .collect(); + + // report the equivocation and the tx should be dispatched successfully + assert_ok!(Beefy::report_fork_equivocation_unsigned( + RuntimeOrigin::none(), + Box::new(equivocation_proof), + key_owner_proofs, + ),); + + era += 1; + start_era(era); + + // check that the balance of equivocating validators is slashed 100%. + let equivocation_validator_ids = equivocation_authority_indices + .iter() + .map(|i| validators[*i]) + .collect::>(); + + for equivocation_validator_id in &equivocation_validator_ids { + assert_eq!(Balances::total_balance(&equivocation_validator_id), 10_000_000 - 10_000); + assert_eq!(Staking::slashable_balance_of(&equivocation_validator_id), 0); + assert_eq!( + Staking::eras_stakers(era, equivocation_validator_id), + pallet_staking::Exposure { total: 0, own: 0, others: vec![] }, + ); + } + + // check that the balances of all other validators are left intact. + for validator in &validators { + if equivocation_validator_ids.contains(&validator) { + continue + } + + assert_eq!(Balances::total_balance(validator), 10_000_000); + assert_eq!(Staking::slashable_balance_of(validator), 10_000); + + assert_eq!( + Staking::eras_stakers(era, validator), + pallet_staking::Exposure { total: 10_000, own: 10_000, others: vec![] }, + ); + } + }); +} + +#[test] +fn report_fork_equivocation_sc_old_set_works() { + let authorities = test_authorities(); + + let mut ext = ExtBuilder::default().add_authorities(authorities).build(); + let (offchain, _offchain_state) = TestOffchainExt::with_offchain_db(ext.offchain_db()); + ext.register_extension(OffchainDbExt::new(offchain.clone())); + ext.register_extension(OffchainWorkerExt::new(offchain)); + + let mut era = 1; + let ( + block_num, + validators, + old_set_id, + equivocation_authority_indices, + equivocation_keys, + key_owner_proofs, + ) = ext.execute_with(|| { + start_era(era); + let block_num = System::block_number(); + + era += 1; + start_era(era); + + let validator_set = Beefy::validator_set().unwrap(); + let authorities = validator_set.validators(); + let validators = Session::validators(); + let old_set_id = validator_set.id(); + + assert_eq!(authorities.len(), 3); + let equivocation_authority_indices = [0, 2]; + let equivocation_keys = equivocation_authority_indices + .iter() + .map(|i| authorities[*i].clone()) + .collect::>(); + + // create the key ownership proofs in the "old" set + let key_owner_proofs = equivocation_keys + .iter() + .map(|k| Historical::prove((BEEFY_KEY_TYPE, &k)).unwrap()) + .collect(); + + era += 1; + start_era(era); + + ( + block_num, + validators, + old_set_id, + equivocation_authority_indices, + equivocation_keys, + key_owner_proofs, + ) + }); + ext.persist_offchain_overlay(); + + ext.execute_with(|| { + // make sure that all authorities have the same balance + for validator in &validators { + assert_eq!(Balances::total_balance(validator), 10_000_000); + assert_eq!(Staking::slashable_balance_of(validator), 10_000); + + assert_eq!( + Staking::eras_stakers(2, validator), + pallet_staking::Exposure { total: 10_000, own: 10_000, others: vec![] }, + ); + } + + let validator_set = Beefy::validator_set().unwrap(); + let new_set_id = validator_set.id(); + assert_eq!(old_set_id + 3, new_set_id); + + let equivocation_keyrings = equivocation_keys + .iter() + .map(|k| BeefyKeyring::from_public(k).unwrap()) + .collect(); + + let payload = Payload::from_single_entry(MMR_ROOT_ID, vec![42]); + let ancestry_proof = Mmr::generate_ancestry_proof(block_num, None).unwrap(); + // generate an fork equivocation proof, with a vote in the same round for a + // different payload than finalized + let commitment = + Commitment { validator_set_id: old_set_id, block_number: block_num, payload }; + // generate an fork equivocation proof, with a vote in the same round for a + // different payload than finalized + let equivocation_proof = generate_fork_equivocation_proof_sc( + commitment, + equivocation_keyrings, + None, + Some(ancestry_proof), + ); + + // report the equivocation and the tx should be dispatched successfully + assert_ok!(Beefy::report_fork_equivocation_unsigned( + RuntimeOrigin::none(), + Box::new(equivocation_proof), + key_owner_proofs, + ),); + + era += 1; + start_era(era); + + // check that the balance of equivocating validators is slashed 100%. + let equivocation_validator_ids = equivocation_authority_indices + .iter() + .map(|i| validators[*i]) + .collect::>(); + + for equivocation_validator_id in &equivocation_validator_ids { + assert_eq!(Balances::total_balance(&equivocation_validator_id), 10_000_000 - 10_000); + assert_eq!(Staking::slashable_balance_of(&equivocation_validator_id), 0); + assert_eq!( + Staking::eras_stakers(era, &equivocation_validator_id), + pallet_staking::Exposure { total: 0, own: 0, others: vec![] }, + ); + } + + // check that the balances of all other validators are left intact. + for validator in &validators { + if equivocation_validator_ids.contains(&validator) { + continue + } + + assert_eq!(Balances::total_balance(validator), 10_000_000); + assert_eq!(Staking::slashable_balance_of(validator), 10_000); + + assert_eq!( + Staking::eras_stakers(3, validator), + pallet_staking::Exposure { total: 10_000, own: 10_000, others: vec![] }, + ); + } + }); +} + +#[test] +fn report_fork_equivocation_sc_future_block_works() { + let authorities = test_authorities(); + + let mut ext = ExtBuilder::default().add_authorities(authorities).build(); + let (offchain, _offchain_state) = TestOffchainExt::with_offchain_db(ext.offchain_db()); + ext.register_extension(OffchainDbExt::new(offchain.clone())); + ext.register_extension(OffchainWorkerExt::new(offchain)); + + let mut era = 2; + ext.execute_with(|| { + start_era(era); + }); + ext.persist_offchain_overlay(); + + ext.execute_with(|| { + let validator_set = Beefy::validator_set().unwrap(); + let authorities = validator_set.validators(); + let set_id = validator_set.id(); + let validators = Session::validators(); + + // make sure that all validators have the same balance + for validator in &validators { + assert_eq!(Balances::total_balance(validator), 10_000_000); + assert_eq!(Staking::slashable_balance_of(validator), 10_000); + + assert_eq!( + Staking::eras_stakers(era, validator), + pallet_staking::Exposure { total: 10_000, own: 10_000, others: vec![] }, + ); + } + + assert_eq!(authorities.len(), 3); + let equivocation_authority_indices = [0, 2]; + let equivocation_keys = equivocation_authority_indices + .iter() + .map(|i| &authorities[*i]) + .collect::>(); + let equivocation_keyrings = equivocation_keys + .iter() + .map(|k| BeefyKeyring::from_public(k.clone()).unwrap()) + .collect(); + + let payload = Payload::from_single_entry(MMR_ROOT_ID, vec![42]); + let block_num = System::block_number() + 20; + // create commitment to a future block + let commitment = Commitment { validator_set_id: set_id, block_number: block_num, payload }; + // generate an fork equivocation proof, with a vote in the same round but for a + // future block + let equivocation_proof = + generate_fork_equivocation_proof_sc(commitment, equivocation_keyrings, None, None); + + // create the key ownership proof + let key_owner_proofs = equivocation_keys + .iter() + .map(|k| Historical::prove((BEEFY_KEY_TYPE, &k)).unwrap()) + .collect(); + + // report the equivocation and the tx should be dispatched successfully + assert_ok!(Beefy::report_fork_equivocation_unsigned( + RuntimeOrigin::none(), + Box::new(equivocation_proof), + key_owner_proofs, + ),); + + era += 1; + start_era(era); + + // check that the balance of equivocating validators is slashed 100%. + let equivocation_validator_ids = equivocation_authority_indices + .iter() + .map(|i| validators[*i]) + .collect::>(); + + for equivocation_validator_id in &equivocation_validator_ids { + assert_eq!(Balances::total_balance(&equivocation_validator_id), 10_000_000 - 10_000); + assert_eq!(Staking::slashable_balance_of(&equivocation_validator_id), 0); + assert_eq!( + Staking::eras_stakers(era, equivocation_validator_id), + pallet_staking::Exposure { total: 0, own: 0, others: vec![] }, + ); + } + + // check that the balances of all other validators are left intact. + for validator in &validators { + if equivocation_validator_ids.contains(&validator) { + continue + } + + assert_eq!(Balances::total_balance(validator), 10_000_000); + assert_eq!(Staking::slashable_balance_of(validator), 10_000); + + assert_eq!( + Staking::eras_stakers(era, validator), + pallet_staking::Exposure { total: 10_000, own: 10_000, others: vec![] }, + ); + } + }); +} + +#[test] +fn report_fork_equivocation_sc_invalid_set_id() { + let authorities = test_authorities(); + + let mut ext = ExtBuilder::default().add_authorities(authorities).build(); + let (offchain, _offchain_state) = TestOffchainExt::with_offchain_db(ext.offchain_db()); + ext.register_extension(OffchainDbExt::new(offchain.clone())); + ext.register_extension(OffchainWorkerExt::new(offchain)); + + let mut era = 1; + let block_num = ext.execute_with(|| { + start_era(era); + let block_num = System::block_number(); + + era += 1; + start_era(era); + block_num + }); + ext.persist_offchain_overlay(); + + ext.execute_with(|| { + let validator_set = Beefy::validator_set().unwrap(); + let authorities = validator_set.validators(); + let set_id = validator_set.id(); + + let equivocation_authority_indices = [0, 2]; + let equivocation_keys = equivocation_authority_indices + .iter() + .map(|i| &authorities[*i]) + .collect::>(); + let equivocation_keyrings = equivocation_keys + .iter() + .map(|k| BeefyKeyring::from_public(k.clone()).unwrap()) + .collect(); + + let key_owner_proofs = equivocation_keys + .iter() + .map(|k| Historical::prove((BEEFY_KEY_TYPE, &k)).unwrap()) + .collect(); + + let payload = Payload::from_single_entry(MMR_ROOT_ID, vec![42]); + let ancestry_proof = Mmr::generate_ancestry_proof(block_num, None).unwrap(); + // generate an equivocation for a future set + let commitment = + Commitment { validator_set_id: set_id + 1, block_number: block_num, payload }; + let equivocation_proof = generate_fork_equivocation_proof_sc( + commitment, + equivocation_keyrings, + None, + Some(ancestry_proof), + ); + + // the call for reporting the equivocation should error + assert_err!( + Beefy::report_fork_equivocation_unsigned( + RuntimeOrigin::none(), + Box::new(equivocation_proof), + key_owner_proofs, + ), + Error::::InvalidEquivocationProofSession, + ); + }); +} + +#[test] +fn report_fork_equivocation_sc_invalid_session() { + let authorities = test_authorities(); + + let mut ext = ExtBuilder::default().add_authorities(authorities).build(); + let (offchain, _offchain_state) = TestOffchainExt::with_offchain_db(ext.offchain_db()); + ext.register_extension(OffchainDbExt::new(offchain.clone())); + ext.register_extension(OffchainWorkerExt::new(offchain)); + + let mut era = 1; + let (block_num, equivocation_keyrings, key_owner_proofs) = ext.execute_with(|| { + start_era(era); + let block_num = System::block_number(); + + era += 1; + start_era(era); + + let validator_set = Beefy::validator_set().unwrap(); + let authorities = validator_set.validators(); + + let equivocation_authority_indices = [0, 2]; + let equivocation_keys = equivocation_authority_indices + .iter() + .map(|i| authorities[*i].clone()) + .collect::>(); + let equivocation_keyrings = equivocation_keys + .iter() + .map(|k| BeefyKeyring::from_public(k).unwrap()) + .collect(); + + // generate key ownership proofs at current era set id + let key_owner_proofs = equivocation_keys + .iter() + .map(|k| Historical::prove((BEEFY_KEY_TYPE, &k)).unwrap()) + .collect(); + + era += 1; + start_era(era); + (block_num, equivocation_keyrings, key_owner_proofs) + }); + ext.persist_offchain_overlay(); + + ext.execute_with(|| { + let set_id = Beefy::validator_set().unwrap().id(); + + let payload = Payload::from_single_entry(MMR_ROOT_ID, vec![42]); + let ancestry_proof = Mmr::generate_ancestry_proof(block_num, None).unwrap(); + // generate an equivocation proof at following era set id = 3 + let commitment = Commitment { validator_set_id: set_id, block_number: block_num, payload }; + let equivocation_proof = generate_fork_equivocation_proof_sc( + commitment, + equivocation_keyrings, + None, + Some(ancestry_proof), + ); + + // report an equivocation for the current set using an key ownership + // proof from the previous set, the session should be invalid. + assert_err!( + Beefy::report_fork_equivocation_unsigned( + RuntimeOrigin::none(), + Box::new(equivocation_proof), + key_owner_proofs, + ), + Error::::InvalidEquivocationProofSession, + ); + }); +} + +#[test] +fn report_fork_equivocation_sc_invalid_key_owner_proof() { + let authorities = test_authorities(); + + let mut ext = ExtBuilder::default().add_authorities(authorities).build(); + let (offchain, _offchain_state) = TestOffchainExt::with_offchain_db(ext.offchain_db()); + ext.register_extension(OffchainDbExt::new(offchain.clone())); + ext.register_extension(OffchainWorkerExt::new(offchain)); + + let mut era = 1; + let block_num = ext.execute_with(|| { + start_era(era); + let block_num = System::block_number(); + + era += 1; + start_era(era); + block_num + }); + ext.persist_offchain_overlay(); + + ext.execute_with(|| { + let validator_set = Beefy::validator_set().unwrap(); + let authorities = validator_set.validators(); + let set_id = validator_set.id(); + + let invalid_owner_authority_index = 1; + let invalid_owner_key = &authorities[invalid_owner_authority_index]; + let valid_owner_authority_index = 0; + let valid_owner_key = &authorities[valid_owner_authority_index]; + + // generate a key ownership proof for the authority at index 1 + let invalid_key_owner_proof = + Historical::prove((BEEFY_KEY_TYPE, &invalid_owner_key)).unwrap(); + // generate a key ownership proof for the authority at index 1 + let valid_key_owner_proof = Historical::prove((BEEFY_KEY_TYPE, &valid_owner_key)).unwrap(); + + let equivocation_authority_indices = [0, 2]; + let equivocation_keys = equivocation_authority_indices + .iter() + .map(|i| &authorities[*i]) + .collect::>(); + let equivocation_keyrings = equivocation_keys + .iter() + .map(|k| BeefyKeyring::from_public(k.clone()).unwrap()) + .collect(); + + let payload = Payload::from_single_entry(MMR_ROOT_ID, vec![42]); + let ancestry_proof = Mmr::generate_ancestry_proof(block_num, None).unwrap(); + // generate an equivocation proof for the authorities at indices [0, 2] + let commitment = Commitment { validator_set_id: set_id, block_number: block_num, payload }; + let equivocation_proof = generate_fork_equivocation_proof_sc( + commitment, + equivocation_keyrings, + None, + Some(ancestry_proof), + ); + + // 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 + era += 1; + start_era(era); + + // report an equivocation for the current set using a key ownership + // proof for a different key than the ones in the equivocation proof. + assert_err!( + Beefy::report_fork_equivocation_unsigned( + RuntimeOrigin::none(), + Box::new(equivocation_proof), + vec![valid_key_owner_proof, invalid_key_owner_proof], + ), + Error::::InvalidKeyOwnershipProof, + ); + }); +} + +#[test] +fn report_fork_equivocation_sc_invalid_equivocation_proof() { + let authorities = test_authorities(); + + let mut ext = ExtBuilder::default().add_authorities(authorities).build(); + let (offchain, _offchain_state) = TestOffchainExt::with_offchain_db(ext.offchain_db()); + ext.register_extension(OffchainDbExt::new(offchain.clone())); + ext.register_extension(OffchainWorkerExt::new(offchain)); + + let mut era = 1; + let block_num = ext.execute_with(|| { + start_era(era); + let block_num = System::block_number(); + + era += 1; + start_era(era); + block_num + }); + ext.persist_offchain_overlay(); + + ext.execute_with(|| { + let validator_set = Beefy::validator_set().unwrap(); + let authorities = validator_set.validators(); + let set_id = validator_set.id(); + + let equivocation_authority_indices = [0, 2]; + let equivocation_keys = equivocation_authority_indices + .iter() + .map(|i| &authorities[*i]) + .collect::>(); + let equivocation_keyrings: Vec<_> = equivocation_keys + .iter() + .map(|k| BeefyKeyring::from_public(k.clone()).unwrap()) + .collect(); + + // generate a key ownership proof at set id in era 1 + let key_owner_proofs: Vec<_> = equivocation_keys + .iter() + .map(|k| Historical::prove((BEEFY_KEY_TYPE, &k)).unwrap()) + .collect(); + + start_era(2); + + let payload = Payload::from_single_entry(MMR_ROOT_ID, vec![42]); + let ancestry_proof = Mmr::generate_ancestry_proof(block_num, None).unwrap(); + + // commitment signed with a key that isn't part of the authority set + let equivocation_proof = generate_fork_equivocation_proof_sc( + Commitment { + validator_set_id: set_id, + block_number: block_num, + payload: payload.clone(), + }, + vec![BeefyKeyring::Eve], + None, + Some(ancestry_proof.clone()), + ); + assert_err!( + Beefy::report_fork_equivocation_unsigned( + RuntimeOrigin::none(), + Box::new(equivocation_proof), + key_owner_proofs.clone(), + ), + Error::::InvalidKeyOwnershipProof, + ); + + // commitment targets future set id + let equivocation_proof = generate_fork_equivocation_proof_sc( + Commitment { + validator_set_id: set_id + 1, + block_number: block_num, + payload: payload.clone(), + }, + equivocation_keyrings.clone(), + None, + Some(ancestry_proof.clone()), + ); + assert_err!( + Beefy::report_fork_equivocation_unsigned( + RuntimeOrigin::none(), + Box::new(equivocation_proof), + key_owner_proofs.clone(), + ), + Error::::InvalidEquivocationProofSession, + ); + + // Simulate InvalidForkEquivocationProof error. + IsValidForkEquivocationProof::set(&false); + let equivocation_proof = generate_fork_equivocation_proof_sc( + Commitment { + validator_set_id: set_id, + block_number: block_num + 1, + payload: payload.clone(), + }, + equivocation_keyrings, + None, + Some(ancestry_proof), + ); + assert_err!( + Beefy::report_fork_equivocation_unsigned( + RuntimeOrigin::none(), + Box::new(equivocation_proof), + key_owner_proofs, + ), + Error::::InvalidForkEquivocationProof, + ); + }); +} + +#[test] +fn report_fork_equivocation_sc_validate_unsigned_prevents_duplicates() { + use sp_runtime::transaction_validity::{ + InvalidTransaction, TransactionPriority, TransactionSource, TransactionValidity, + ValidTransaction, + }; + + let authorities = test_authorities(); + + let mut ext = ExtBuilder::default().add_authorities(authorities).build(); + let (offchain, _offchain_state) = TestOffchainExt::with_offchain_db(ext.offchain_db()); + ext.register_extension(OffchainDbExt::new(offchain.clone())); + ext.register_extension(OffchainWorkerExt::new(offchain)); + + let mut era = 1; + let block_num = ext.execute_with(|| { + start_era(era); + let block_num = System::block_number(); + + era += 1; + start_era(era); + block_num + }); + ext.persist_offchain_overlay(); + + ext.execute_with(|| { + let validator_set = Beefy::validator_set().unwrap(); + let authorities = validator_set.validators(); + let set_id = validator_set.id(); + + // generate and report an equivocation for the validator at index 0 + let equivocation_authority_index = 0; + let equivocation_key = &authorities[equivocation_authority_index]; + let equivocation_keyring = BeefyKeyring::from_public(equivocation_key).unwrap(); + + let payload = Payload::from_single_entry(MMR_ROOT_ID, vec![42]); + let ancestry_proof = Mmr::generate_ancestry_proof(block_num, None).unwrap(); + let equivocation_proof = generate_fork_equivocation_proof_vote( + (block_num, payload, set_id, &equivocation_keyring), + None, + Some(ancestry_proof), + ); + + let key_owner_proof = Historical::prove((BEEFY_KEY_TYPE, &equivocation_key)).unwrap(); + + let call = Call::report_fork_equivocation_unsigned { + equivocation_proof: Box::new(equivocation_proof.clone()), + key_owner_proofs: vec![key_owner_proof.clone()], + }; + + // only local/inblock reports are allowed + assert_eq!( + ::validate_unsigned( + TransactionSource::External, + &call, + ), + InvalidTransaction::Call.into(), + ); + + // the transaction is valid when passed as local + let tx_tag = (vec![equivocation_key], set_id, 3u64); + + let call_result = ::validate_unsigned( + TransactionSource::Local, + &call, + ); + + assert_eq!( + call_result, + TransactionValidity::Ok(ValidTransaction { + priority: TransactionPriority::max_value(), + requires: vec![], + provides: vec![("BeefyEquivocation", tx_tag.clone()).encode()], + longevity: ReportLongevity::get(), + propagate: false, + }) + ); + + assert_ok!(::pre_dispatch(&call)); + + // we submit the report + Beefy::report_fork_equivocation_unsigned( + RuntimeOrigin::none(), + Box::new(equivocation_proof), + vec![key_owner_proof], + ) + .unwrap(); + + // the report should now be considered stale and the transaction is invalid + // the check for staleness should be done on both `validate_unsigned` and on `pre_dispatch` + assert_err!( + ::validate_unsigned( + TransactionSource::Local, + &call, + ), + InvalidTransaction::Stale, + ); + + assert_err!( + ::pre_dispatch(&call), + InvalidTransaction::Stale, + ); + }); +} + +#[test] +fn valid_fork_equivocation_sc_reports_dont_pay_fees() { + let authorities = test_authorities(); + + let mut ext = ExtBuilder::default().add_authorities(authorities).build(); + let (offchain, _offchain_state) = TestOffchainExt::with_offchain_db(ext.offchain_db()); + ext.register_extension(OffchainDbExt::new(offchain.clone())); + ext.register_extension(OffchainWorkerExt::new(offchain)); + + let mut era = 1; + let block_num = ext.execute_with(|| { + start_era(era); + let block_num = System::block_number(); + + era += 1; + start_era(era); + block_num + }); + ext.persist_offchain_overlay(); + + ext.execute_with(|| { + let validator_set = Beefy::validator_set().unwrap(); + let authorities = validator_set.validators(); + let set_id = validator_set.id(); + + let equivocation_authority_index = 0; + let equivocation_key = &authorities[equivocation_authority_index]; + let equivocation_keyring = BeefyKeyring::from_public(equivocation_key).unwrap(); + + // generate equivocation proof + let payload = Payload::from_single_entry(MMR_ROOT_ID, vec![42]); + let ancestry_proof = Mmr::generate_ancestry_proof(block_num, None).unwrap(); + let equivocation_proof = generate_fork_equivocation_proof_vote( + (block_num, payload, set_id, &equivocation_keyring), + None, + Some(ancestry_proof), + ); + + // create the key ownership proof. + let key_owner_proof = Historical::prove((BEEFY_KEY_TYPE, &equivocation_key)).unwrap(); + + // check the dispatch info for the call. + let info = Call::::report_fork_equivocation_unsigned { + equivocation_proof: Box::new(equivocation_proof.clone()), + key_owner_proofs: vec![key_owner_proof.clone()], + } + .get_dispatch_info(); + + // it should have non-zero weight and the fee has to be paid. + assert!(info.weight.any_gt(Weight::zero())); + assert_eq!(info.pays_fee, Pays::Yes); + + // report the equivocation. + let post_info = Beefy::report_fork_equivocation_unsigned( + RuntimeOrigin::none(), + Box::new(equivocation_proof.clone()), + vec![key_owner_proof.clone()], + ) + .unwrap(); + + // the original weight should be kept, but given that the report + // is valid the fee is waived. + assert!(post_info.actual_weight.is_none()); + assert_eq!(post_info.pays_fee, Pays::No); + + // report the equivocation again which is invalid now since it is + // duplicate. + let post_info = Beefy::report_fork_equivocation_unsigned( + RuntimeOrigin::none(), + Box::new(equivocation_proof.clone()), + vec![key_owner_proof.clone()], + ) + .err() + .unwrap() + .post_info; + + // the fee is not waived and the original weight is kept. + assert!(post_info.actual_weight.is_none()); + assert_eq!(post_info.pays_fee, Pays::Yes); + }) +} + +#[test] +fn report_fork_equivocation_sc_stacked_reports_stack_correctly() { + let authorities = test_authorities(); + + let mut ext = ExtBuilder::default().add_authorities(authorities).build(); + let (offchain, _offchain_state) = TestOffchainExt::with_offchain_db(ext.offchain_db()); + ext.register_extension(OffchainDbExt::new(offchain.clone())); + ext.register_extension(OffchainWorkerExt::new(offchain)); + + let mut era = 1; + let block_num = ext.execute_with(|| { + assert_eq!(Staking::current_era(), Some(0)); + assert_eq!(Session::current_index(), 0); + start_era(era); + let block_num = System::block_number(); + + era += 1; + start_era(era); + block_num + }); + ext.persist_offchain_overlay(); + + let ( + commitment, + validators, + equivocation_keyrings, + equivocation_authority_indices, + key_owner_proofs, + ) = ext.execute_with(|| { + let validator_set = Beefy::validator_set().unwrap(); + let authorities = validator_set.validators(); + let set_id = validator_set.id(); + let validators = Session::validators(); + + // make sure that all validators have the same balance + for validator in &validators { + assert_eq!(Balances::total_balance(validator), 10_000_000); + assert_eq!(Staking::slashable_balance_of(validator), 10_000); + + assert_eq!( + Staking::eras_stakers(era, validator), + pallet_staking::Exposure { total: 10_000, own: 10_000, others: vec![] }, + ); + } + + assert_eq!(authorities.len(), 3); + let equivocation_authority_indices = [0, 2]; + let equivocation_keys = equivocation_authority_indices + .iter() + .map(|i| &authorities[*i]) + .collect::>(); + let equivocation_keyrings: Vec<_> = equivocation_keys + .iter() + .map(|k| BeefyKeyring::from_public(k.clone()).unwrap()) + .collect(); + + let payload = Payload::from_single_entry(MMR_ROOT_ID, vec![42]); + let ancestry_proof = Mmr::generate_ancestry_proof(block_num, None).unwrap(); + let commitment = Commitment { validator_set_id: set_id, block_number: block_num, payload }; + // generate two fork equivocation proofs with a signed commitment in the same round for a + // different payload than finalized + // 1. the first equivocation proof is only for Alice + // 2. the second equivocation proof is for all equivocators + let equivocation_proof_singleton = generate_fork_equivocation_proof_sc( + commitment.clone(), + vec![equivocation_keyrings[0].clone()], + None, + Some(ancestry_proof.clone()), + ); + + // create the key ownership proof + let key_owner_proofs: Vec<_> = equivocation_keys + .iter() + .map(|k| Historical::prove((BEEFY_KEY_TYPE, &k)).unwrap()) + .collect(); + + // only report a single equivocator and the tx should be dispatched successfully + assert_ok!(Beefy::report_fork_equivocation_unsigned( + RuntimeOrigin::none(), + Box::new(equivocation_proof_singleton), + vec![key_owner_proofs[0].clone()], + ),); + era += 1; + start_era(era); + ( + commitment, + validators, + equivocation_keyrings, + equivocation_authority_indices, + key_owner_proofs, + ) + }); + ext.persist_offchain_overlay(); + + ext.execute_with(|| { + let ancestry_proof = Mmr::generate_ancestry_proof(block_num, None).unwrap(); + let equivocation_proof_full = generate_fork_equivocation_proof_sc( + commitment, + equivocation_keyrings, + None, + Some(ancestry_proof), + ); + + // check that the balance of the reported equivocating validator is slashed 100%. + let equivocation_validator_ids = equivocation_authority_indices + .iter() + .map(|i| validators[*i]) + .collect::>(); + + assert_eq!(Balances::total_balance(&equivocation_validator_ids[0]), 10_000_000 - 10_000); + assert_eq!(Staking::slashable_balance_of(&equivocation_validator_ids[0]), 0); + assert_eq!( + Staking::eras_stakers(era, &equivocation_validator_ids[0]), + pallet_staking::Exposure { total: 0, own: 0, others: vec![] }, + ); + + // check that the balances of all other validators are left intact. + for validator in &validators { + if equivocation_validator_ids[0] == *validator { + continue + } + + assert_eq!(Balances::total_balance(validator), 10_000_000); + assert_eq!(Staking::slashable_balance_of(validator), 10_000); + + assert_eq!( + Staking::eras_stakers(era, validator), + pallet_staking::Exposure { total: 10_000, own: 10_000, others: vec![] }, + ); + } + + // report the full equivocation and the tx should be dispatched successfully + assert_ok!(Beefy::report_fork_equivocation_unsigned( + RuntimeOrigin::none(), + Box::new(equivocation_proof_full), + key_owner_proofs, + ),); + + era += 1; + start_era(era); + + let equivocation_validator_ids = equivocation_authority_indices + .iter() + .map(|i| validators[*i]) + .collect::>(); + + // check that the balance of equivocating validators is slashed 100%, and the validator + // already reported isn't slashed again + for equivocation_validator_id in &equivocation_validator_ids { + assert_eq!(Balances::total_balance(&equivocation_validator_id), 10_000_000 - 10_000); + assert_eq!(Staking::slashable_balance_of(&equivocation_validator_id), 0); + assert_eq!( + Staking::eras_stakers(era, &equivocation_validator_id), + pallet_staking::Exposure { total: 0, own: 0, others: vec![] }, + ); + } + + // check that the balances of all other validators are left intact. + for validator in &validators { + if equivocation_validator_ids.contains(&validator) { + continue + } + + assert_eq!(Balances::total_balance(validator), 10_000_000); + assert_eq!(Staking::slashable_balance_of(validator), 10_000); + + assert_eq!( + Staking::eras_stakers(era, validator), + pallet_staking::Exposure { total: 10_000, own: 10_000, others: vec![] }, + ); + } + }); +} + #[test] fn set_new_genesis_works() { let authorities = test_authorities(); diff --git a/substrate/frame/merkle-mountain-range/src/lib.rs b/substrate/frame/merkle-mountain-range/src/lib.rs index a86443f2e0114..fa854d144f73e 100644 --- a/substrate/frame/merkle-mountain-range/src/lib.rs +++ b/substrate/frame/merkle-mountain-range/src/lib.rs @@ -27,7 +27,7 @@ //! - on-chain storage - hashes only; not full leaf content; //! - off-chain storage - via Indexing API we push full leaf content (and all internal nodes as //! well) to the Off-chain DB, so that the data is available for Off-chain workers. -//! Hashing used for MMR is configurable independently from the rest of the runtime (i.e. not using +//! Hashing used for MMR is configurable independently of the rest of the runtime (i.e. not using //! `frame_system::Hashing`) so something compatible with external chains can be used (like //! Keccak256 for Ethereum compatibility). //! diff --git a/substrate/frame/staking/src/migrations.rs b/substrate/frame/staking/src/migrations.rs index b2ddf77004f95..3cd9d1e37a310 100644 --- a/substrate/frame/staking/src/migrations.rs +++ b/substrate/frame/staking/src/migrations.rs @@ -347,9 +347,9 @@ pub mod v10 { #[storage_alias] type EarliestUnappliedSlash = StorageValue, EraIndex>; - /// Apply any pending slashes that where queued. + /// Apply any pending slashes that were queued. /// - /// That means we might slash someone a bit too early, but we will definitely + /// That means we might slash someone a bit too early, but we definitely /// won't forget to slash them. The cap of 512 is somewhat randomly taken to /// prevent us from iterating over an arbitrary large number of keys `on_runtime_upgrade`. pub struct MigrateToV10(core::marker::PhantomData); diff --git a/substrate/frame/staking/src/slashing.rs b/substrate/frame/staking/src/slashing.rs index f831f625957d4..68fc4df098da9 100644 --- a/substrate/frame/staking/src/slashing.rs +++ b/substrate/frame/staking/src/slashing.rs @@ -47,7 +47,7 @@ //! has multiple misbehaviors. However, accounting for such cases is necessary //! to deter a class of "rage-quit" attacks. //! -//! Based on research at +//! Based on research at use crate::{ BalanceOf, Config, DisabledValidators, DisablingStrategy, Error, Exposure, NegativeImbalanceOf, diff --git a/substrate/primitives/consensus/beefy/src/commitment.rs b/substrate/primitives/consensus/beefy/src/commitment.rs index 8d3a6c6aa90f9..1cd06be633bba 100644 --- a/substrate/primitives/consensus/beefy/src/commitment.rs +++ b/substrate/primitives/consensus/beefy/src/commitment.rs @@ -25,7 +25,7 @@ use sp_runtime::traits::Hash; use crate::{BeefyAuthorityId, Payload, ValidatorSet, ValidatorSetId}; /// A commitment signature, accompanied by the id of the validator that it belongs to. -#[derive(Debug)] +#[derive(Clone, Debug, Decode, Encode, PartialEq, TypeInfo)] pub struct KnownSignature { /// The signing validator. pub validator_id: TAuthorityId, @@ -104,7 +104,7 @@ where } } -/// A commitment with matching GRANDPA validators' signatures. +/// A commitment with matching BEEFY validators' signatures. /// /// Note that SCALE-encoding of the structure is optimized for size efficiency over the wire, /// please take a look at custom [`Encode`] and [`Decode`] implementations and @@ -113,7 +113,7 @@ where pub struct SignedCommitment { /// The commitment signatures are collected for. pub commitment: Commitment, - /// GRANDPA validators' signatures for the commitment. + /// BEEFY validators' signatures for the commitment. /// /// The length of this `Vec` must match number of validators in the current set (see /// [Commitment::validator_set_id]). @@ -163,7 +163,7 @@ impl SignedCommitment { // Arrangement of signatures in the commitment should be in the same order // as validators for that set. let encoded_commitment = self.commitment.encode(); - let signatories: Vec<_> = validator_set + let signatures: Vec<_> = validator_set .validators() .into_iter() .zip(self.signatures.iter()) @@ -176,7 +176,7 @@ impl SignedCommitment { }) .collect(); - Ok(signatories) + Ok(signatures) } } @@ -193,7 +193,7 @@ struct CompactSignedCommitment { /// A bitfield representing presence of a signature coming from a validator at some index. /// /// The bit at index `0` is set to `1` in case we have a signature coming from a validator at - /// index `0` in in the original validator set. In case the [`SignedCommitment`] does not + /// index `0` in the original validator set. In case the [`SignedCommitment`] does not /// contain that signature the `bit` will be set to `0`. Bits are packed into `Vec` signatures_from: BitField, /// Number of validators in the Validator Set and hence number of significant bits in the @@ -331,6 +331,21 @@ impl From> for VersionedFinalityProof { } } +impl VersionedFinalityProof { + /// Provide reference to inner `Payload`. + pub fn payload(&self) -> &Payload { + match self { + VersionedFinalityProof::V1(inner) => &inner.commitment.payload, + } + } + /// Block number this proof is for. + pub fn number(&self) -> &N { + match self { + VersionedFinalityProof::V1(inner) => &inner.commitment.block_number, + } + } +} + #[cfg(test)] mod tests { diff --git a/substrate/primitives/consensus/beefy/src/lib.rs b/substrate/primitives/consensus/beefy/src/lib.rs index 390c0ff71273a..bc0e2048aef8b 100644 --- a/substrate/primitives/consensus/beefy/src/lib.rs +++ b/substrate/primitives/consensus/beefy/src/lib.rs @@ -45,14 +45,19 @@ pub mod test_utils; pub use commitment::{Commitment, KnownSignature, SignedCommitment, VersionedFinalityProof}; pub use payload::{known_payloads, BeefyPayloadId, Payload, PayloadProvider}; +use sp_mmr_primitives::{ + mmr_lib, + utils::{self, AncestryHasher}, + AncestryProof, +}; -use alloc::vec::Vec; +use alloc::{vec, vec::Vec}; use codec::{Codec, Decode, Encode}; use core::fmt::{Debug, Display}; use scale_info::TypeInfo; use sp_application_crypto::{AppCrypto, AppPublic, ByteArray, RuntimeAppPublic}; use sp_core::H256; -use sp_runtime::traits::{Hash, Keccak256, NumberFor}; +use sp_runtime::traits::{Hash as HashT, HashOutput, Header as HeaderT, Keccak256, NumberFor}; /// Key type for BEEFY module. pub const KEY_TYPE: sp_core::crypto::KeyTypeId = sp_application_crypto::key_types::BEEFY; @@ -60,7 +65,7 @@ pub const KEY_TYPE: sp_core::crypto::KeyTypeId = sp_application_crypto::key_type /// Trait representing BEEFY authority id, including custom signature verification. /// /// Accepts custom hashing fn for the message and custom convertor fn for the signer. -pub trait BeefyAuthorityId: RuntimeAppPublic { +pub trait BeefyAuthorityId: RuntimeAppPublic { /// Verify a signature. /// /// Return `true` if signature over `msg` is valid for this id. @@ -97,7 +102,7 @@ pub trait AuthorityIdBound: /// Your code should use the above types as concrete types for all crypto related /// functionality. pub mod ecdsa_crypto { - use super::{AuthorityIdBound, BeefyAuthorityId, Hash, RuntimeAppPublic, KEY_TYPE}; + use super::{AuthorityIdBound, BeefyAuthorityId, HashT, RuntimeAppPublic, KEY_TYPE}; use sp_application_crypto::{app_crypto, ecdsa}; use sp_core::crypto::Wraps; @@ -109,12 +114,12 @@ pub mod ecdsa_crypto { /// Signature for a BEEFY authority using ECDSA as its crypto. pub type AuthoritySignature = Signature; - impl BeefyAuthorityId for AuthorityId + impl BeefyAuthorityId for AuthorityId where - ::Output: Into<[u8; 32]>, + ::Output: Into<[u8; 32]>, { fn verify(&self, signature: &::Signature, msg: &[u8]) -> bool { - let msg_hash = ::hash(msg).into(); + let msg_hash = ::hash(msg).into(); match sp_io::crypto::secp256k1_ecdsa_recover_compressed( signature.as_inner_ref().as_ref(), &msg_hash, @@ -140,7 +145,7 @@ pub mod ecdsa_crypto { #[cfg(feature = "bls-experimental")] pub mod bls_crypto { - use super::{AuthorityIdBound, BeefyAuthorityId, Hash, RuntimeAppPublic, KEY_TYPE}; + use super::{AuthorityIdBound, BeefyAuthorityId, HashT, RuntimeAppPublic, KEY_TYPE}; use sp_application_crypto::{app_crypto, bls377}; use sp_core::{bls377::Pair as BlsPair, crypto::Wraps, Pair as _}; @@ -152,9 +157,9 @@ pub mod bls_crypto { /// Signature for a BEEFY authority using BLS as its crypto. pub type AuthoritySignature = Signature; - impl BeefyAuthorityId for AuthorityId + impl BeefyAuthorityId for AuthorityId where - ::Output: Into<[u8; 32]>, + ::Output: Into<[u8; 32]>, { fn verify(&self, signature: &::Signature, msg: &[u8]) -> bool { // `w3f-bls` library uses IETF hashing standard and as such does not expose @@ -180,7 +185,7 @@ pub mod bls_crypto { /// functionality. #[cfg(feature = "bls-experimental")] pub mod ecdsa_bls_crypto { - use super::{AuthorityIdBound, BeefyAuthorityId, Hash, RuntimeAppPublic, KEY_TYPE}; + use super::{AuthorityIdBound, BeefyAuthorityId, HashT, RuntimeAppPublic, KEY_TYPE}; use sp_application_crypto::{app_crypto, ecdsa_bls377}; use sp_core::{crypto::Wraps, ecdsa_bls377::Pair as EcdsaBlsPair}; @@ -194,7 +199,7 @@ pub mod ecdsa_bls_crypto { impl BeefyAuthorityId for AuthorityId where - H: Hash, + H: HashT, H::Output: Into<[u8; 32]>, { fn verify(&self, signature: &::Signature, msg: &[u8]) -> bool { @@ -302,6 +307,18 @@ pub struct VoteMessage { pub signature: Signature, } +/// Trait containing generic methods for BEEFY equivocation proofs. +pub trait BeefyEquivocationProof { + /// Returns the authority ids of the misbehaving voters. + fn offender_ids(&self) -> Vec<&Id>; + + /// Returns the round number at which the infringement occurred. + fn round_number(&self) -> &Number; + + /// Returns the set id at which the infringement occurred. + fn set_id(&self) -> ValidatorSetId; +} + /// Proof of voter misbehavior on a given set id. Misbehavior/equivocation in /// BEEFY happens when a voter votes on the same round/block for different payloads. /// Proving is achieved by collecting the signed commitments of conflicting votes. @@ -318,16 +335,146 @@ impl DoubleVotingProof { pub fn offender_id(&self) -> &Id { &self.first.id } - /// Returns the round number at which the equivocation occurred. - pub fn round_number(&self) -> &Number { +} + +impl BeefyEquivocationProof + for DoubleVotingProof +{ + fn offender_ids(&self) -> Vec<&Id> { + vec![self.offender_id()] + } + + fn round_number(&self) -> &Number { &self.first.commitment.block_number } - /// Returns the set id at which the equivocation occurred. - pub fn set_id(&self) -> ValidatorSetId { + + fn set_id(&self) -> ValidatorSetId { self.first.commitment.validator_set_id } } +/// Proof of authority misbehavior on a given set id. +/// This proof shows commitment signed on a different fork. +/// See [`CheckForkEquivocationProof`] for proof validity conditions. +#[derive(Clone, Debug, Decode, Encode, PartialEq, TypeInfo)] +pub struct ForkEquivocationProof { + /// Commitment for a block on a different fork than one at the same height in + /// the chain where this proof is submitted. + pub commitment: Commitment, + /// Signatures on this block + pub signatures: Vec>, + /// Canonical header at the same height as `commitment.block_number`. + pub canonical_header: Option

, + /// Ancestry proof showing that the current best mmr root descends from another mmr root at + /// `commitment.block_number` than commitment.payload + pub ancestry_proof: Option>, +} + +impl + ForkEquivocationProof +{ + fn check_fork>( + &self, + best_root: Hash, + mmr_size: u64, + canonical_header_hash: &Header::Hash, + first_mmr_block_num: Header::Number, + best_block_num: Header::Number, + ) -> bool { + if self.commitment.block_number <= best_block_num { + if let Some(canonical_header) = &self.canonical_header { + if check_header_proof(&self.commitment, canonical_header, canonical_header_hash) { + // avoid verifying the ancestry proof if a valid header proof has been provided + return true; + } + } + + if let Some(ancestry_proof) = &self.ancestry_proof { + return check_ancestry_proof::( + &self.commitment, + ancestry_proof, + first_mmr_block_num, + best_root, + mmr_size, + ); + } + + return false; + } + + true + } + + /// Validates [ForkEquivocationProof] with the following checks: + /// - if the commitment is to a block in our history, then at least a header or an ancestry + /// proof is provided: + /// - the proof is correct if `self.canonical_header` hashes to `canonical_header_hash`, is at + /// height `commitment.block_number`, and `commitment.payload` != + /// `canonical_payload(canonical_header)` + /// - the proof is correct if the provided `ancestry_proof` proves + /// `mmr_root(commitment.block_number) != mmr_root(commitment.payload)` + /// - `commitment` is signed by all claimed signatories + /// + /// NOTE: GRANDPA finalization proof is not checked, which leads to slashing on forks. This is + /// fine since honest validators will not be slashed on the chain finalized by GRANDPA, which is + /// the only chain that ultimately matters. The only material difference not checking GRANDPA + /// proofs makes is that validators are not slashed for signing BEEFY commitments prior to the + /// blocks committed to being finalized by GRANDPA. This is fine too, since the slashing risk of + /// committing to an incorrect block implies validators will only sign blocks they *know* will + /// be finalized by GRANDPA. + pub fn check>( + &self, + // The MMR root of the best block of the chain where this proof is submitted. + best_root: Hash, + // The size of the MMR at the best block. + mmr_size: u64, + // The hash of the canonical header at the height of `commitment.block_number`. + canonical_header_hash: &Header::Hash, + // The block number at which the mmr pallet was added to the runtime. + first_mmr_block_num: Header::Number, + // The best block number of the chain where this proof is submitted. + best_block_num: Header::Number, + ) -> bool + where + Id: BeefyAuthorityId + PartialEq, + { + if !self.check_fork::( + best_root, + mmr_size, + canonical_header_hash, + first_mmr_block_num, + best_block_num, + ) { + return false; + } + + return self.signatures.iter().all(|signature| { + // TODO: refactor check_commitment_signature to take a slice of signatories + check_commitment_signature( + &self.commitment, + &signature.validator_id, + &signature.signature, + ) + }) + } +} + +impl + BeefyEquivocationProof for ForkEquivocationProof +{ + fn offender_ids(&self) -> Vec<&Id> { + self.signatures.iter().map(|signature| &signature.validator_id).collect() + } + + fn round_number(&self) -> &Header::Number { + &self.commitment.block_number + } + + fn set_id(&self) -> ValidatorSetId { + self.commitment.validator_set_id + } +} + /// Check a commitment signature by encoding the commitment and /// verifying the provided signature using the expected authority id. pub fn check_commitment_signature( @@ -338,21 +485,21 @@ pub fn check_commitment_signature( where Id: BeefyAuthorityId, Number: Clone + Encode + PartialEq, - MsgHash: Hash, + MsgHash: HashT, { let encoded_commitment = commitment.encode(); BeefyAuthorityId::::verify(authority_id, signature, &encoded_commitment) } -/// Verifies the equivocation proof by making sure that both votes target +/// Verifies the vote equivocation proof by making sure that both votes target /// different blocks and that its signatures are valid. -pub fn check_equivocation_proof( +pub fn check_double_voting_proof( report: &DoubleVotingProof::Signature>, ) -> bool where Id: BeefyAuthorityId + PartialEq, Number: Clone + Encode + PartialEq, - MsgHash: Hash, + MsgHash: HashT, { let first = &report.first; let second = &report.second; @@ -379,6 +526,90 @@ where return valid_first && valid_second } +/// Checks whether the provided header's payload differs from the commitment's payload. +fn check_header_proof
( + commitment: &Commitment, + canonical_header: &Header, + canonical_header_hash: &Header::Hash, +) -> bool +where + Header: HeaderT, +{ + let canonical_mmr_root_digest = mmr::find_mmr_root_digest::
(canonical_header); + let canonical_payload = canonical_mmr_root_digest + .map(|mmr_root| Payload::from_single_entry(known_payloads::MMR_ROOT_ID, mmr_root.encode())); + // Check header's hash and that the `payload` of the `commitment` differs from the + // `canonical_payload`. Note that if the signatories signed a payload when there should be + // none (for instance for a block prior to BEEFY activation), then canonical_payload = None, + // and they will likewise be slashed. + // Note that we can only check this if a valid header has been provided - we cannot + // slash for this with an ancestry proof - by necessity) + return canonical_header.hash() == *canonical_header_hash && + Some(&commitment.payload) != canonical_payload.as_ref() +} + +/// Checks whether an ancestry proof has the correct size and its calculated root differs from the +/// commitment's payload's. +fn check_ancestry_proof( + commitment: &Commitment, + ancestry_proof: &AncestryProof, + first_mmr_block_num: Header::Number, + best_root: NodeHash::Output, + mmr_size: u64, +) -> bool +where + Header: HeaderT, + NodeHash: HashT, +{ + let expected_leaf_count = sp_mmr_primitives::utils::block_num_to_leaf_index::
( + commitment.block_number, + first_mmr_block_num, + ) + .and_then(|leaf_index| { + leaf_index.checked_add(1).ok_or_else(|| { + sp_mmr_primitives::Error::InvalidNumericOp.log_debug("leaf_index + 1 overflowed") + }) + }); + + if let Ok(expected_leaf_count) = expected_leaf_count { + let expected_mmr_size = + sp_mmr_primitives::utils::NodesUtils::new(expected_leaf_count).size(); + // verify that the prev_root is at the correct block number + // this can be inferred from the leaf_count / mmr_size of the prev_root: + // we've converted the commitment.block_number to an mmr size and now + // compare with the value in the ancestry proof + if expected_mmr_size != ancestry_proof.prev_leaf_count { + return false + } + if sp_mmr_primitives::utils::verify_ancestry_proof::< + NodeHash::Output, + utils::AncestryHasher, + >(best_root, mmr_size, ancestry_proof.clone()) != + Ok(true) + { + return false + } + } else { + // if the block number either under- or overflowed, the + // commitment.block_number was not valid and the commitment should not have + // been signed, hence we can skip the ancestry proof and slash the + // signatories + return true + } + + // once the ancestry proof is verified, calculate the prev_root to compare it + // with the commitment's prev_root + let ancestry_prev_root = mmr_lib::ancestry_proof::bagging_peaks_hashes::< + NodeHash::Output, + AncestryHasher, + >(ancestry_proof.prev_peaks.clone()); + // if the commitment payload does not commit to an MMR root, then this + // commitment may have another purpose and should not be slashed + let commitment_prev_root = + commitment.payload.get_decoded::(&known_payloads::MMR_ROOT_ID); + return commitment_prev_root != ancestry_prev_root.ok() +} + /// New BEEFY validator set notification hook. pub trait OnNewValidatorSet { /// Function called by the pallet when BEEFY validator set changes. @@ -393,13 +624,33 @@ impl OnNewValidatorSet for () { fn on_new_validator_set(_: &ValidatorSet, _: &ValidatorSet) {} } +/// Hook for checking fork equivocation proof for validity. +pub trait CheckForkEquivocationProof { + /// Associated hash type for hashing ancestry proof. + type Hash: HashT; + /// Validate equivocation proof (check commitment is to unexpected payload and + /// signatures are valid). + /// NOTE: Fork equivocation proof currently only prevents attacks + /// assuming 2/3rds of validators honestly participate in BEEFY + /// finalization and at least one honest relayer can update the + /// beefy light client at least once every 4096 blocks. See + /// for + /// replacement solution. + fn check_fork_equivocation_proof( + proof: &ForkEquivocationProof::Output>, + ) -> Result<(), Err> + where + Id: BeefyAuthorityId + PartialEq, + MsgHash: HashT; +} + /// 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, TypeInfo)] +#[derive(Decode, Encode, PartialEq, TypeInfo, Clone)] pub struct OpaqueKeyOwnershipProof(Vec); impl OpaqueKeyOwnershipProof { /// Create a new `OpaqueKeyOwnershipProof` using the given encoded @@ -416,10 +667,12 @@ impl OpaqueKeyOwnershipProof { } sp_api::decl_runtime_apis! { - /// API necessary for BEEFY voters. - #[api_version(3)] - pub trait BeefyApi where + /// API necessary for BEEFY voters. Due to the significant conceptual + /// overlap, in large part, this is lifted from the GRANDPA API. + #[api_version(4)] + pub trait BeefyApi where AuthorityId : Codec + RuntimeAppPublic, + Hash: Codec, { /// Return the block number where BEEFY consensus is enabled/started fn beefy_genesis() -> Option>; @@ -435,12 +688,28 @@ sp_api::decl_runtime_apis! { /// `None` when creation of the extrinsic fails, e.g. if equivocation /// reporting is disabled for the given runtime (i.e. this method is /// hardcoded to return `None`). Only useful in an offchain context. - fn submit_report_equivocation_unsigned_extrinsic( + fn submit_report_vote_equivocation_unsigned_extrinsic( equivocation_proof: DoubleVotingProof, AuthorityId, ::Signature>, key_owner_proof: OpaqueKeyOwnershipProof, ) -> Option<()>; + /// Submits an unsigned extrinsic to report commitments to an invalid + /// fork. The caller must provide the invalid commitments proof and key + /// ownership proofs (should be obtained using + /// `generate_key_ownership_proof`) for the offenders. The extrinsic + /// will be unsigned and should only be accepted for local authorship + /// (not to be broadcast to the network). This method returns `None` + /// when creation of the extrinsic fails, e.g. if the key owner proofs + /// are not validly encoded or if equivocation reporting is disabled for + /// the given runtime (i.e. this method is hardcoded to return `None`). + /// Only useful in an offchain context. + fn submit_report_fork_equivocation_unsigned_extrinsic( + fork_equivocation_proof: + ForkEquivocationProof, + key_owner_proofs: Vec, + ) -> 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 diff --git a/substrate/primitives/consensus/beefy/src/mmr.rs b/substrate/primitives/consensus/beefy/src/mmr.rs index 0bc303d51c014..57f24c1fcd361 100644 --- a/substrate/primitives/consensus/beefy/src/mmr.rs +++ b/substrate/primitives/consensus/beefy/src/mmr.rs @@ -30,10 +30,10 @@ use crate::{ecdsa_crypto::AuthorityId, ConsensusLog, MmrRootHash, BEEFY_ENGINE_I use alloc::vec::Vec; use codec::{Decode, Encode, MaxEncodedLen}; use scale_info::TypeInfo; -use sp_runtime::{ - generic::OpaqueDigestItemId, - traits::{Block, Header}, -}; +use sp_runtime::{generic::OpaqueDigestItemId, traits::Header}; + +#[cfg(feature = "std")] +use sp_runtime::traits::Block; /// A provider for extra data that gets added to the Mmr leaf pub trait BeefyDataProvider { @@ -135,7 +135,7 @@ pub struct BeefyAuthoritySet { pub type BeefyNextAuthoritySet = BeefyAuthoritySet; /// Extract the MMR root hash from a digest in the given header, if it exists. -pub fn find_mmr_root_digest(header: &B::Header) -> Option { +pub fn find_mmr_root_digest(header: &H) -> Option { let id = OpaqueDigestItemId::Consensus(&BEEFY_ENGINE_ID); let filter = |log: ConsensusLog| match log { @@ -184,7 +184,7 @@ mod mmr_root_provider { /// Simple wrapper that gets MMR root from header digests or from client state. fn mmr_root_from_digest_or_runtime(&self, header: &B::Header) -> Option { - find_mmr_root_digest::(header).or_else(|| { + find_mmr_root_digest::(header).or_else(|| { self.runtime.runtime_api().mmr_root(header.hash()).ok().and_then(|r| r.ok()) }) } @@ -193,7 +193,7 @@ mod mmr_root_provider { impl PayloadProvider for MmrRootProvider where B: Block, - R: ProvideRuntimeApi, + R: ProvideRuntimeApi + Send + Sync + 'static, R::Api: MmrApi>, { fn payload(&self, header: &B::Header) -> Option { @@ -208,7 +208,7 @@ mod mmr_root_provider { mod tests { use super::*; use crate::H256; - use sp_runtime::{traits::BlakeTwo256, Digest, DigestItem, OpaqueExtrinsic}; + use sp_runtime::{traits::BlakeTwo256, Digest, DigestItem}; #[test] fn should_construct_version_correctly() { @@ -236,7 +236,6 @@ mod tests { #[test] fn extract_mmr_root_digest() { type Header = sp_runtime::generic::Header; - type Block = sp_runtime::generic::Block; let mut header = Header::new( 1u64, Default::default(), @@ -246,7 +245,7 @@ mod tests { ); // verify empty digest shows nothing - assert!(find_mmr_root_digest::(&header).is_none()); + assert!(find_mmr_root_digest::
(&header).is_none()); let mmr_root_hash = H256::random(); header.digest_mut().push(DigestItem::Consensus( @@ -255,7 +254,7 @@ mod tests { )); // verify validator set is correctly extracted from digest - let extracted = find_mmr_root_digest::(&header); + let extracted = find_mmr_root_digest::
(&header); assert_eq!(extracted, Some(mmr_root_hash)); } } diff --git a/substrate/primitives/consensus/beefy/src/payload.rs b/substrate/primitives/consensus/beefy/src/payload.rs index 1a06e620e7ad4..d7fc022310d5b 100644 --- a/substrate/primitives/consensus/beefy/src/payload.rs +++ b/substrate/primitives/consensus/beefy/src/payload.rs @@ -75,7 +75,7 @@ impl Payload { } /// Trait for custom BEEFY payload providers. -pub trait PayloadProvider { +pub trait PayloadProvider: Clone + Send + Sync + 'static { /// Provide BEEFY payload if available for `header`. fn payload(&self, header: &B::Header) -> Option; } diff --git a/substrate/primitives/consensus/beefy/src/test_utils.rs b/substrate/primitives/consensus/beefy/src/test_utils.rs index d7fd49214f12f..e32cf15eaa6a3 100644 --- a/substrate/primitives/consensus/beefy/src/test_utils.rs +++ b/substrate/primitives/consensus/beefy/src/test_utils.rs @@ -18,14 +18,15 @@ #[cfg(feature = "bls-experimental")] use crate::ecdsa_bls_crypto; use crate::{ - ecdsa_crypto, AuthorityIdBound, BeefySignatureHasher, Commitment, DoubleVotingProof, Payload, - ValidatorSetId, VoteMessage, + ecdsa_crypto, AuthorityIdBound, BeefySignatureHasher, Commitment, DoubleVotingProof, + ForkEquivocationProof, KnownSignature, Payload, ValidatorSetId, VoteMessage, }; use sp_application_crypto::{AppCrypto, AppPair, RuntimeAppPublic, Wraps}; use sp_core::{ecdsa, Pair}; -use sp_runtime::traits::Hash; +use sp_runtime::traits::{BlockNumber, Hash, Header as HeaderT}; use codec::Encode; +use sp_mmr_primitives::AncestryProof; use std::{collections::HashMap, marker::PhantomData}; use strum::IntoEnumIterator; @@ -136,20 +137,58 @@ impl From> for ecdsa_crypto::Public { } } -/// Create a new `EquivocationProof` based on given arguments. -pub fn generate_equivocation_proof( +/// Create a new `VoteMessage` from commitment primitives and keyring +fn signed_vote( + block_number: Number, + payload: Payload, + validator_set_id: ValidatorSetId, + keyring: &Keyring, +) -> VoteMessage { + let commitment = Commitment { validator_set_id, block_number, payload }; + let signature = keyring.sign(&commitment.encode()); + VoteMessage { commitment, id: keyring.public(), signature } +} + +/// Create a new `VoteEquivocationProof` based on given arguments. +pub fn generate_double_voting_proof( vote1: (u64, Payload, ValidatorSetId, &Keyring), vote2: (u64, Payload, ValidatorSetId, &Keyring), ) -> DoubleVotingProof { - let signed_vote = |block_number: u64, - payload: Payload, - validator_set_id: ValidatorSetId, - keyring: &Keyring| { - let commitment = Commitment { validator_set_id, block_number, payload }; - let signature = keyring.sign(&commitment.encode()); - VoteMessage { commitment, id: keyring.public(), signature } - }; let first = signed_vote(vote1.0, vote1.1, vote1.2, vote1.3); let second = signed_vote(vote2.0, vote2.1, vote2.2, vote2.3); DoubleVotingProof { first, second } } + +/// Create a new `ForkEquivocationProof` based on vote & canonical header. +pub fn generate_fork_equivocation_proof_vote( + vote: (Header::Number, Payload, ValidatorSetId, &Keyring), + canonical_header: Option
, + ancestry_proof: Option>, +) -> ForkEquivocationProof { + let signed_vote = signed_vote::(vote.0, vote.1, vote.2, vote.3); + let signatures = + vec![KnownSignature { validator_id: signed_vote.id, signature: signed_vote.signature }]; + ForkEquivocationProof { + commitment: signed_vote.commitment, + signatures, + canonical_header, + ancestry_proof, + } +} + +/// Create a new `ForkEquivocationProof` based on signed commitment & canonical header. +pub fn generate_fork_equivocation_proof_sc( + commitment: Commitment, + keyrings: Vec>, + canonical_header: Option
, + ancestry_proof: Option>, +) -> ForkEquivocationProof { + let signatures = keyrings + .into_iter() + .map(|k| KnownSignature { + validator_id: k.public(), + signature: k.sign(&commitment.encode()), + }) + .collect::>(); + ForkEquivocationProof { commitment, signatures, canonical_header, ancestry_proof } +} diff --git a/substrate/primitives/merkle-mountain-range/Cargo.toml b/substrate/primitives/merkle-mountain-range/Cargo.toml index 23efc1b687c2b..c5ef4551c65f7 100644 --- a/substrate/primitives/merkle-mountain-range/Cargo.toml +++ b/substrate/primitives/merkle-mountain-range/Cargo.toml @@ -21,6 +21,7 @@ log = { workspace = true } mmr-lib = { package = "ckb-merkle-mountain-range", git = "https://github.com/paritytech/merkle-mountain-range.git", branch = "master", default-features = false } serde = { features = ["alloc", "derive"], optional = true, workspace = true } sp-api = { path = "../api", default-features = false } +sp-std = { path = "../std", default-features = false } sp-core = { path = "../core", default-features = false } sp-debug-derive = { path = "../debug-derive", default-features = false } sp-runtime = { path = "../runtime", default-features = false } @@ -42,6 +43,7 @@ std = [ "sp-core/std", "sp-debug-derive/std", "sp-runtime/std", + "sp-std/std", ] # Serde support without relying on std features. diff --git a/substrate/primitives/merkle-mountain-range/src/lib.rs b/substrate/primitives/merkle-mountain-range/src/lib.rs index 3740047e02782..8dad0de4f2a07 100644 --- a/substrate/primitives/merkle-mountain-range/src/lib.rs +++ b/substrate/primitives/merkle-mountain-range/src/lib.rs @@ -438,7 +438,7 @@ impl Error { sp_api::decl_runtime_apis! { /// API to interact with MMR pallet. - #[api_version(2)] + #[api_version(3)] pub trait MmrApi { /// Return the on-chain MMR root hash. fn mmr_root() -> Result; @@ -469,6 +469,15 @@ sp_api::decl_runtime_apis! { /// same position in both the `leaves` vector and the `leaf_indices` vector contained in the [LeafProof] fn verify_proof_stateless(root: Hash, leaves: Vec, proof: LeafProof) -> Result<(), Error>; + + /// Generate MMR ancestry proof for prior mmr size + fn generate_ancestry_proof( + prev_best_block: BlockNumber, + best_known_block_number: Option + ) -> Result, Error>; + + /// Verifies that a claimed prev_root is in fact an ancestor of the current mmr root + fn verify_ancestry_proof(ancestry_proof: AncestryProof) -> Result<(), Error>; } } diff --git a/substrate/primitives/merkle-mountain-range/src/utils.rs b/substrate/primitives/merkle-mountain-range/src/utils.rs index 72674e24a2728..f5dd6940a3f24 100644 --- a/substrate/primitives/merkle-mountain-range/src/utils.rs +++ b/substrate/primitives/merkle-mountain-range/src/utils.rs @@ -22,9 +22,52 @@ use mmr_lib::helper; #[cfg(not(feature = "std"))] use alloc::vec::Vec; +use core::fmt::Debug; use sp_runtime::traits::{CheckedAdd, CheckedSub, Header, One}; -use crate::{Error, LeafIndex, NodeIndex}; +use crate::{AncestryProof, Error, LeafIndex, NodeIndex}; + +/// Merging & Hashing behavior specific to ancestry proofs. +pub struct AncestryHasher(sp_std::marker::PhantomData); + +impl mmr_lib::Merge for AncestryHasher { + type Item = H::Output; + + fn merge(left: &Self::Item, right: &Self::Item) -> mmr_lib::Result { + let mut concat = left.as_ref().to_vec(); + concat.extend_from_slice(right.as_ref()); + + Ok(::hash(&concat)) + } +} + +/// Stateless verification of the ancestry proof of `root` against a prev_root implicit in the +/// ancestry proof. +pub fn verify_ancestry_proof( + root: H, + mmr_size: NodeIndex, + ancestry_proof: AncestryProof, +) -> Result +where + H: Clone + Debug + PartialEq + Encode, + M: mmr_lib::Merge, +{ + let p: mmr_lib::NodeMerkleProof = + mmr_lib::NodeMerkleProof::::new(mmr_size, ancestry_proof.items.into_iter().collect()); + + let ancestry_proof = mmr_lib::AncestryProof:: { + prev_peaks: ancestry_proof.prev_peaks, + prev_size: ancestry_proof.prev_leaf_count, + proof: p, + }; + + let prev_root = + mmr_lib::ancestry_proof::bagging_peaks_hashes::(ancestry_proof.prev_peaks.clone()) + .map_err(|e| Error::Verify.log_debug(e))?; + ancestry_proof + .verify_ancestor(root, prev_root) + .map_err(|e| Error::Verify.log_debug(e)) +} /// Get the first block with MMR. pub fn first_mmr_block_num(