From 24c2bdd181110780cc845175db6aa3a0b1e8db42 Mon Sep 17 00:00:00 2001 From: Adrian Catangiu Date: Mon, 21 Nov 2022 16:56:29 +0200 Subject: [PATCH] client/beefy: persist voter state (#12712) * client/beefy: prepare worker for persisting state * client/beefy: persist voter state * client/beefy: initialize persistent state * client/beefy: try to vote from the very beginning Now that voter is initialized from persistent state, it makes sense that it can attempt voting right away. This also helps the genesis case when we consider block `One` as mandatory. * client/beefy: add tests for voter state db * client/beefy: persist voter state as soon as initialized * client/beefy: make sure min-block-delta is at least 1 * client/beefy: persist state after voting Persist state after handling self vote to avoid double voting in case of voter restarts. * client/beefy: persist state after handling mandatory block vote For mandatory blocks we want to make sure we're not losing votes in case of crashes or restarts, since voter will not make further progress without finalizing them. * frame/beefy: use GENESIS_AUTHORITY_SET_ID on pallet genesis * client/beefy: initialize voter at either genesis or last finalized To guarantee unbroken chain of mandatory blocks justifications, voter will always resume from either last BEEFY-justified block or `pallet-beefy` genesis, whichever is more recent. Initialization walks back the chain from latest GRANDPA finalized block looking for one of the above. Along the way, it also records and enqueues for processing any BEEFY mandatory blocks that have been already GRANDPA finalized but not BEEFY finalized. * client/beefy: decouple voter init from aux db state load * client/beefy: fix voter init tests * remove debug prints * gadget future must be type () * fix init from last justification Signed-off-by: Adrian Catangiu --- client/beefy/src/aux_schema.rs | 105 +++++ client/beefy/src/lib.rs | 278 ++++++++++-- client/beefy/src/round.rs | 16 +- client/beefy/src/tests.rs | 245 +++++++++-- client/beefy/src/worker.rs | 770 +++++++++++++++------------------ frame/beefy/src/lib.rs | 3 +- primitives/beefy/src/lib.rs | 2 +- 7 files changed, 918 insertions(+), 501 deletions(-) create mode 100644 client/beefy/src/aux_schema.rs diff --git a/client/beefy/src/aux_schema.rs b/client/beefy/src/aux_schema.rs new file mode 100644 index 0000000000000..e9a2e9b9e6126 --- /dev/null +++ b/client/beefy/src/aux_schema.rs @@ -0,0 +1,105 @@ +// This file is part of Substrate. + +// Copyright (C) 2019-2022 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +//! Schema for BEEFY state persisted in the aux-db. + +use crate::worker::PersistedState; +use codec::{Decode, Encode}; +use log::{info, trace}; +use sc_client_api::{backend::AuxStore, Backend}; +use sp_blockchain::{Error as ClientError, Result as ClientResult}; +use sp_runtime::traits::Block as BlockT; + +const VERSION_KEY: &[u8] = b"beefy_auxschema_version"; +const WORKER_STATE: &[u8] = b"beefy_voter_state"; + +const CURRENT_VERSION: u32 = 1; + +pub(crate) fn write_current_version(backend: &B) -> ClientResult<()> { + info!(target: "beefy", "🥩 write aux schema version {:?}", CURRENT_VERSION); + AuxStore::insert_aux(backend, &[(VERSION_KEY, CURRENT_VERSION.encode().as_slice())], &[]) +} + +/// Write voter state. +pub(crate) fn write_voter_state( + backend: &B, + state: &PersistedState, +) -> ClientResult<()> { + trace!(target: "beefy", "🥩 persisting {:?}", state); + backend.insert_aux(&[(WORKER_STATE, state.encode().as_slice())], &[]) +} + +fn load_decode(backend: &B, key: &[u8]) -> ClientResult> { + match backend.get_aux(key)? { + None => Ok(None), + Some(t) => T::decode(&mut &t[..]) + .map_err(|e| ClientError::Backend(format!("BEEFY DB is corrupted: {}", e))) + .map(Some), + } +} + +/// Load or initialize persistent data from backend. +pub(crate) fn load_persistent(backend: &BE) -> ClientResult>> +where + B: BlockT, + BE: Backend, +{ + let version: Option = load_decode(backend, VERSION_KEY)?; + + match version { + None => (), + Some(1) => return load_decode::<_, PersistedState>(backend, WORKER_STATE), + other => + return Err(ClientError::Backend(format!("Unsupported BEEFY DB version: {:?}", other))), + } + + // No persistent state found in DB. + Ok(None) +} + +#[cfg(test)] +pub(crate) mod tests { + use super::*; + use crate::tests::BeefyTestNet; + use sc_network_test::TestNetFactory; + + // also used in tests.rs + pub fn verify_persisted_version>(backend: &BE) -> bool { + let version: u32 = load_decode(backend, VERSION_KEY).unwrap().unwrap(); + version == CURRENT_VERSION + } + + #[test] + fn should_load_persistent_sanity_checks() { + let mut net = BeefyTestNet::new(1); + let backend = net.peer(0).client().as_backend(); + + // version not available in db -> None + assert_eq!(load_persistent(&*backend).unwrap(), None); + + // populate version in db + write_current_version(&*backend).unwrap(); + // verify correct version is retrieved + assert_eq!(load_decode(&*backend, VERSION_KEY).unwrap(), Some(CURRENT_VERSION)); + + // version is available in db but state isn't -> None + assert_eq!(load_persistent(&*backend).unwrap(), None); + + // full `PersistedState` load is tested in `tests.rs`. + } +} diff --git a/client/beefy/src/lib.rs b/client/beefy/src/lib.rs index 441f6e4248117..3bdd13982aea2 100644 --- a/client/beefy/src/lib.rs +++ b/client/beefy/src/lib.rs @@ -16,22 +16,48 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use beefy_primitives::{BeefyApi, MmrRootHash, PayloadProvider}; +use crate::{ + communication::{ + notification::{ + BeefyBestBlockSender, BeefyBestBlockStream, BeefyVersionedFinalityProofSender, + BeefyVersionedFinalityProofStream, + }, + peers::KnownPeers, + request_response::{ + outgoing_requests_engine::OnDemandJustificationsEngine, BeefyJustifsRequestHandler, + }, + }, + import::BeefyBlockImport, + round::Rounds, + worker::PersistedState, +}; +use beefy_primitives::{ + crypto::AuthorityId, BeefyApi, MmrRootHash, PayloadProvider, ValidatorSet, BEEFY_ENGINE_ID, + GENESIS_AUTHORITY_SET_ID, +}; +use futures::{stream::Fuse, StreamExt}; +use log::{debug, error, info}; use parking_lot::Mutex; use prometheus::Registry; -use sc_client_api::{Backend, BlockBackend, BlockchainEvents, Finalizer}; +use sc_client_api::{Backend, BlockBackend, BlockchainEvents, FinalityNotifications, Finalizer}; use sc_consensus::BlockImport; use sc_network::ProtocolName; use sc_network_common::service::NetworkRequest; -use sc_network_gossip::Network as GossipNetwork; -use sp_api::{NumberFor, ProvideRuntimeApi}; -use sp_blockchain::HeaderBackend; +use sc_network_gossip::{GossipEngine, Network as GossipNetwork}; +use sp_api::{HeaderT, NumberFor, ProvideRuntimeApi}; +use sp_blockchain::{ + Backend as BlockchainBackend, Error as ClientError, HeaderBackend, Result as ClientResult, +}; use sp_consensus::{Error as ConsensusError, SyncOracle}; use sp_keystore::SyncCryptoStorePtr; use sp_mmr_primitives::MmrApi; -use sp_runtime::traits::Block; -use std::{marker::PhantomData, sync::Arc}; +use sp_runtime::{ + generic::BlockId, + traits::{Block, One, Zero}, +}; +use std::{collections::VecDeque, marker::PhantomData, sync::Arc}; +mod aux_schema; mod error; mod keystore; mod metrics; @@ -42,27 +68,13 @@ pub mod communication; pub mod import; pub mod justification; -#[cfg(test)] -mod tests; - -use crate::{ - communication::{ - notification::{ - BeefyBestBlockSender, BeefyBestBlockStream, BeefyVersionedFinalityProofSender, - BeefyVersionedFinalityProofStream, - }, - peers::KnownPeers, - request_response::{ - outgoing_requests_engine::OnDemandJustificationsEngine, BeefyJustifsRequestHandler, - }, - }, - import::BeefyBlockImport, -}; - pub use communication::beefy_protocol_name::{ gossip_protocol_name, justifications_protocol_name as justifs_protocol_name, }; +#[cfg(test)] +mod tests; + /// A convenience BEEFY client trait that defines all the type bounds a BEEFY client /// has to satisfy. Ideally that should actually be a trait alias. Unfortunately as /// of today, Rust does not allow a type alias to be used as a trait bound. Tracking @@ -222,7 +234,7 @@ where let known_peers = Arc::new(Mutex::new(KnownPeers::new())); let gossip_validator = Arc::new(communication::gossip::GossipValidator::new(known_peers.clone())); - let gossip_engine = sc_network_gossip::GossipEngine::new( + let mut gossip_engine = sc_network_gossip::GossipEngine::new( network.clone(), gossip_protocol_name, gossip_validator.clone(), @@ -240,21 +252,38 @@ where prometheus_registry.as_ref().map(metrics::Metrics::register).and_then( |result| match result { Ok(metrics) => { - log::debug!(target: "beefy", "🥩 Registered metrics"); + debug!(target: "beefy", "🥩 Registered metrics"); Some(metrics) }, Err(err) => { - log::debug!(target: "beefy", "🥩 Failed to register metrics: {:?}", err); + debug!(target: "beefy", "🥩 Failed to register metrics: {:?}", err); None }, }, ); + // Subscribe to finality notifications and justifications before waiting for runtime pallet and + // reuse the streams, so we don't miss notifications while waiting for pallet to be available. + let mut finality_notifications = client.finality_notification_stream().fuse(); + let block_import_justif = links.from_block_import_justif_stream.subscribe().fuse(); + + // Wait for BEEFY pallet to be active before starting voter. + let persisted_state = + match wait_for_runtime_pallet(&*runtime, &mut gossip_engine, &mut finality_notifications) + .await + .and_then(|best_grandpa| { + load_or_init_voter_state(&*backend, &*runtime, best_grandpa, min_block_delta) + }) { + Ok(state) => state, + Err(e) => { + error!(target: "beefy", "Error: {:?}. Terminating.", e); + return + }, + }; + let worker_params = worker::WorkerParams { - client, backend, payload_provider, - runtime, network, key_store: key_store.into(), known_peers, @@ -263,10 +292,195 @@ where on_demand_justifications, links, metrics, - min_block_delta, + persisted_state, + }; + + let worker = worker::BeefyWorker::<_, _, _, _, _>::new(worker_params); + + futures::future::join( + worker.run(block_import_justif, finality_notifications), + on_demand_justifications_handler.run(), + ) + .await; +} + +fn load_or_init_voter_state( + backend: &BE, + runtime: &R, + best_grandpa: ::Header, + min_block_delta: u32, +) -> ClientResult> +where + B: Block, + BE: Backend, + R: ProvideRuntimeApi, + R::Api: BeefyApi, +{ + // Initialize voter state from AUX DB or from pallet genesis. + if let Some(mut state) = crate::aux_schema::load_persistent(backend)? { + // Overwrite persisted state with current best GRANDPA block. + state.set_best_grandpa(best_grandpa); + // Overwrite persisted data with newly provided `min_block_delta`. + state.set_min_block_delta(min_block_delta); + info!(target: "beefy", "🥩 Loading BEEFY voter state from db: {:?}.", state); + Ok(state) + } else { + initialize_voter_state(backend, runtime, best_grandpa, min_block_delta) + } +} + +// If no persisted state present, walk back the chain from first GRANDPA notification to either: +// - latest BEEFY finalized block, or if none found on the way, +// - BEEFY pallet genesis; +// Enqueue any BEEFY mandatory blocks (session boundaries) found on the way, for voter to finalize. +fn initialize_voter_state( + backend: &BE, + runtime: &R, + best_grandpa: ::Header, + min_block_delta: u32, +) -> ClientResult> +where + B: Block, + BE: Backend, + R: ProvideRuntimeApi, + R::Api: BeefyApi, +{ + // Walk back the imported blocks and initialize voter either, at the last block with + // a BEEFY justification, or at pallet genesis block; voter will resume from there. + let blockchain = backend.blockchain(); + let mut sessions = VecDeque::new(); + let mut header = best_grandpa.clone(); + let state = loop { + if let Some(true) = blockchain + .justifications(header.hash()) + .ok() + .flatten() + .map(|justifs| justifs.get(BEEFY_ENGINE_ID).is_some()) + { + info!( + target: "beefy", + "🥩 Initialize BEEFY voter at last BEEFY finalized block: {:?}.", + *header.number() + ); + let best_beefy = *header.number(); + // If no session boundaries detected so far, just initialize new rounds here. + if sessions.is_empty() { + let active_set = expect_validator_set(runtime, BlockId::hash(header.hash()))?; + let mut rounds = Rounds::new(best_beefy, active_set); + // Mark the round as already finalized. + rounds.conclude(best_beefy); + sessions.push_front(rounds); + } + let state = + PersistedState::checked_new(best_grandpa, best_beefy, sessions, min_block_delta) + .ok_or_else(|| ClientError::Backend("Invalid BEEFY chain".into()))?; + break state + } + + // Check if we should move up the chain. + let parent_hash = *header.parent_hash(); + if *header.number() == One::one() || + runtime + .runtime_api() + .validator_set(&BlockId::hash(parent_hash)) + .ok() + .flatten() + .is_none() + { + // We've reached pallet genesis, initialize voter here. + let genesis_num = *header.number(); + let genesis_set = expect_validator_set(runtime, BlockId::hash(header.hash())) + .and_then(genesis_set_sanity_check)?; + info!( + target: "beefy", + "🥩 Loading BEEFY voter state from genesis on what appears to be first startup. \ + Starting voting rounds at block {:?}, genesis validator set {:?}.", + genesis_num, genesis_set, + ); + + sessions.push_front(Rounds::new(genesis_num, genesis_set)); + break PersistedState::checked_new(best_grandpa, Zero::zero(), sessions, min_block_delta) + .ok_or_else(|| ClientError::Backend("Invalid BEEFY chain".into()))? + } + + if let Some(active) = worker::find_authorities_change::(&header) { + info!(target: "beefy", "🥩 Marking block {:?} as BEEFY Mandatory.", *header.number()); + sessions.push_front(Rounds::new(*header.number(), active)); + } + + // Move up the chain. + header = blockchain.expect_header(BlockId::Hash(parent_hash))?; }; - let worker = worker::BeefyWorker::<_, _, _, _, _, _>::new(worker_params); + aux_schema::write_current_version(backend)?; + aux_schema::write_voter_state(backend, &state)?; + Ok(state) +} - futures::future::join(worker.run(), on_demand_justifications_handler.run()).await; +/// Wait for BEEFY runtime pallet to be available, return active validator set. +/// Should be called only once during worker initialization. +async fn wait_for_runtime_pallet( + runtime: &R, + mut gossip_engine: &mut GossipEngine, + finality: &mut Fuse>, +) -> ClientResult<::Header> +where + B: Block, + R: ProvideRuntimeApi, + R::Api: BeefyApi, +{ + info!(target: "beefy", "🥩 BEEFY gadget waiting for BEEFY pallet to become available..."); + loop { + futures::select! { + notif = finality.next() => { + let notif = match notif { + Some(notif) => notif, + None => break + }; + let at = BlockId::hash(notif.header.hash()); + if let Some(active) = runtime.runtime_api().validator_set(&at).ok().flatten() { + // Beefy pallet available, return best grandpa at the time. + info!( + target: "beefy", "🥩 BEEFY pallet available: block {:?} validator set {:?}", + notif.header.number(), active + ); + return Ok(notif.header) + } + }, + _ = gossip_engine => { + break + } + } + } + let err_msg = "🥩 Gossip engine has unexpectedly terminated.".into(); + error!(target: "beefy", "{}", err_msg); + Err(ClientError::Backend(err_msg)) +} + +fn genesis_set_sanity_check( + active: ValidatorSet, +) -> ClientResult> { + if active.id() == GENESIS_AUTHORITY_SET_ID { + Ok(active) + } else { + error!(target: "beefy", "🥩 Unexpected ID for genesis validator set {:?}.", active); + Err(ClientError::Backend("BEEFY Genesis sanity check failed.".into())) + } +} + +fn expect_validator_set( + runtime: &R, + at: BlockId, +) -> ClientResult> +where + B: Block, + R: ProvideRuntimeApi, + R::Api: BeefyApi, +{ + runtime + .runtime_api() + .validator_set(&at) + .ok() + .flatten() + .ok_or_else(|| ClientError::Backend("BEEFY pallet expected to be active.".into())) } diff --git a/client/beefy/src/round.rs b/client/beefy/src/round.rs index 45d346ccd85eb..7a8cc4171a155 100644 --- a/client/beefy/src/round.rs +++ b/client/beefy/src/round.rs @@ -16,27 +16,23 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use std::{ - collections::{BTreeMap, HashMap}, - hash::Hash, -}; - -use log::{debug, trace}; - use beefy_primitives::{ crypto::{Public, Signature}, ValidatorSet, ValidatorSetId, }; +use codec::{Decode, Encode}; +use log::{debug, trace}; use sp_runtime::traits::{Block, NumberFor}; +use std::{collections::BTreeMap, hash::Hash}; /// Tracks for each round which validators have voted/signed and /// whether the local `self` validator has voted/signed. /// /// Does not do any validation on votes or signatures, layers above need to handle that (gossip). -#[derive(Debug, Default)] +#[derive(Debug, Decode, Default, Encode, PartialEq)] struct RoundTracker { self_vote: bool, - votes: HashMap, + votes: BTreeMap, } impl RoundTracker { @@ -69,7 +65,7 @@ pub fn threshold(authorities: usize) -> usize { /// Only round numbers > `best_done` are of interest, all others are considered stale. /// /// Does not do any validation on votes or signatures, layers above need to handle that (gossip). -#[derive(Debug)] +#[derive(Debug, Decode, Encode, PartialEq)] pub(crate) struct Rounds { rounds: BTreeMap<(Payload, NumberFor), RoundTracker>, session_start: NumberFor, diff --git a/client/beefy/src/tests.rs b/client/beefy/src/tests.rs index 1d5da4aaefba3..9a31d4a583d0e 100644 --- a/client/beefy/src/tests.rs +++ b/client/beefy/src/tests.rs @@ -18,56 +18,53 @@ //! Tests and test helpers for BEEFY. +use crate::{ + aux_schema::{load_persistent, tests::verify_persisted_version}, + beefy_block_import_and_links, + communication::request_response::{ + on_demand_justifications_protocol_config, BeefyJustifsRequestHandler, + }, + gossip_protocol_name, + justification::*, + keystore::tests::Keyring as BeefyKeyring, + load_or_init_voter_state, wait_for_runtime_pallet, BeefyRPCLinks, BeefyVoterLinks, KnownPeers, + PersistedState, +}; +use beefy_primitives::{ + crypto::{AuthorityId, Signature}, + known_payloads, + mmr::MmrRootProvider, + BeefyApi, Commitment, ConsensusLog, MmrRootHash, Payload, SignedCommitment, ValidatorSet, + VersionedFinalityProof, BEEFY_ENGINE_ID, KEY_TYPE as BeefyKeyType, +}; use futures::{future, stream::FuturesUnordered, Future, StreamExt}; use parking_lot::Mutex; -use serde::{Deserialize, Serialize}; -use std::{collections::HashMap, marker::PhantomData, sync::Arc, task::Poll}; -use tokio::{runtime::Runtime, time::Duration}; - -use sc_client_api::HeaderBackend; +use sc_client_api::{Backend as BackendT, BlockchainEvents, FinalityNotifications, HeaderBackend}; use sc_consensus::{ BlockImport, BlockImportParams, BoxJustificationImport, ForkChoiceStrategy, ImportResult, ImportedAux, }; +use sc_network::{config::RequestResponseConfig, ProtocolName}; use sc_network_test::{ Block, BlockImportAdapter, FullPeerConfig, PassThroughVerifier, Peer, PeersClient, PeersFullClient, TestNetFactory, }; use sc_utils::notification::NotificationReceiver; -use sp_keystore::testing::KeyStore as TestKeystore; - -use beefy_primitives::{ - crypto::{AuthorityId, Signature}, - mmr::MmrRootProvider, - BeefyApi, ConsensusLog, MmrRootHash, ValidatorSet, VersionedFinalityProof, BEEFY_ENGINE_ID, - KEY_TYPE as BeefyKeyType, -}; -use sc_network::{config::RequestResponseConfig, ProtocolName}; -use sp_mmr_primitives::{EncodableOpaqueLeaf, Error as MmrError, MmrApi, Proof}; - +use serde::{Deserialize, Serialize}; use sp_api::{ApiRef, ProvideRuntimeApi}; use sp_consensus::BlockOrigin; use sp_core::H256; -use sp_keystore::{SyncCryptoStore, SyncCryptoStorePtr}; +use sp_keystore::{testing::KeyStore as TestKeystore, SyncCryptoStore, SyncCryptoStorePtr}; +use sp_mmr_primitives::{EncodableOpaqueLeaf, Error as MmrError, MmrApi, Proof}; use sp_runtime::{ codec::Encode, generic::BlockId, traits::{Header as HeaderT, NumberFor}, BuildStorage, DigestItem, Justifications, Storage, }; - +use std::{collections::HashMap, marker::PhantomData, sync::Arc, task::Poll}; use substrate_test_runtime_client::{runtime::Header, ClientExt}; - -use crate::{ - beefy_block_import_and_links, - communication::request_response::{ - on_demand_justifications_protocol_config, BeefyJustifsRequestHandler, - }, - gossip_protocol_name, - justification::*, - keystore::tests::Keyring as BeefyKeyring, - BeefyRPCLinks, BeefyVoterLinks, -}; +use tokio::{runtime::Runtime, time::Duration}; const GENESIS_HASH: H256 = H256::zero(); fn beefy_gossip_proto_name() -> ProtocolName { @@ -531,7 +528,7 @@ fn beefy_finalizing_blocks() { let peers = peers.into_iter().enumerate(); // finalize block #5 -> BEEFY should finalize #1 (mandatory) and #5 from diff-power-of-two rule. - finalize_block_and_wait_for_beefy(&net, peers.clone(), &mut runtime, &[5], &[1, 5]); + finalize_block_and_wait_for_beefy(&net, peers.clone(), &mut runtime, &[1, 5], &[1, 5]); // GRANDPA finalize #10 -> BEEFY finalize #10 (mandatory) finalize_block_and_wait_for_beefy(&net, peers.clone(), &mut runtime, &[10], &[10]); @@ -573,7 +570,7 @@ fn lagging_validators() { &net, peers.clone(), &mut runtime, - &[15], + &[1, 15], &[1, 9, 13, 14, 15], ); @@ -661,7 +658,7 @@ fn correct_beefy_payload() { let net = Arc::new(Mutex::new(net)); let peers = peers.into_iter().enumerate(); // with 3 good voters and 1 bad one, consensus should happen and best blocks produced. - finalize_block_and_wait_for_beefy(&net, peers, &mut runtime, &[10], &[1, 9]); + finalize_block_and_wait_for_beefy(&net, peers, &mut runtime, &[1, 10], &[1, 9]); let (best_blocks, versioned_finality_proof) = get_beefy_streams(&mut net.lock(), [(0, BeefyKeyring::Alice)].into_iter()); @@ -945,3 +942,187 @@ fn on_demand_beefy_justification_sync() { // Now that Dave has caught up, sanity check voting works for all of them. finalize_block_and_wait_for_beefy(&net, all_peers, &mut runtime, &[30], &[30]); } + +fn test_voter_init_setup( + net: &mut BeefyTestNet, + finality: &mut futures::stream::Fuse>, +) -> sp_blockchain::Result> { + let backend = net.peer(0).client().as_backend(); + let api = Arc::new(crate::tests::two_validators::TestApi {}); + let known_peers = Arc::new(Mutex::new(KnownPeers::new())); + let gossip_validator = + Arc::new(crate::communication::gossip::GossipValidator::new(known_peers)); + let mut gossip_engine = sc_network_gossip::GossipEngine::new( + net.peer(0).network_service().clone(), + "/beefy/whatever", + gossip_validator, + None, + ); + let best_grandpa = + futures::executor::block_on(wait_for_runtime_pallet(&*api, &mut gossip_engine, finality)) + .unwrap(); + load_or_init_voter_state(&*backend, &*api, best_grandpa, 1) +} + +#[test] +fn should_initialize_voter_at_genesis() { + let keys = &[BeefyKeyring::Alice]; + 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(); + + // push 15 blocks with `AuthorityChange` digests every 10 blocks + net.generate_blocks_and_sync(15, 10, &validator_set, false); + + let mut finality = net.peer(0).client().as_client().finality_notification_stream().fuse(); + + // finalize 13 without justifications + let hashof13 = backend.blockchain().expect_block_hash_from_id(&BlockId::Number(13)).unwrap(); + net.peer(0).client().as_client().finalize_block(hashof13, None).unwrap(); + + // load persistent state - nothing in DB, should init at session boundary + let persisted_state = test_voter_init_setup(&mut net, &mut finality).unwrap(); + + // Test initialization at session boundary. + // verify voter initialized with two sessions starting at blocks 1 and 10 + let sessions = persisted_state.voting_oracle().sessions(); + assert_eq!(sessions.len(), 2); + assert_eq!(sessions[0].session_start(), 1); + assert_eq!(sessions[1].session_start(), 10); + let rounds = persisted_state.active_round().unwrap(); + assert_eq!(rounds.session_start(), 1); + assert_eq!(rounds.validator_set_id(), validator_set.id()); + + // verify next vote target is mandatory block 1 + assert_eq!(persisted_state.best_beefy_block(), 0); + assert_eq!(persisted_state.best_grandpa_block(), 13); + assert_eq!( + persisted_state + .voting_oracle() + .voting_target(persisted_state.best_beefy_block(), 13), + Some(1) + ); + + // verify state also saved to db + assert!(verify_persisted_version(&*backend)); + let state = load_persistent(&*backend).unwrap().unwrap(); + assert_eq!(state, persisted_state); +} + +#[test] +fn should_initialize_voter_when_last_final_is_session_boundary() { + let keys = &[BeefyKeyring::Alice]; + 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(); + + // push 15 blocks with `AuthorityChange` digests every 10 blocks + net.generate_blocks_and_sync(15, 10, &validator_set, false); + + let mut finality = net.peer(0).client().as_client().finality_notification_stream().fuse(); + + // finalize 13 without justifications + let hashof13 = backend.blockchain().expect_block_hash_from_id(&BlockId::Number(13)).unwrap(); + net.peer(0).client().as_client().finalize_block(hashof13, None).unwrap(); + + // import/append BEEFY justification for session boundary block 10 + let commitment = Commitment { + payload: Payload::from_single_entry(known_payloads::MMR_ROOT_ID, vec![]), + block_number: 10, + validator_set_id: validator_set.id(), + }; + let justif = VersionedFinalityProof::<_, Signature>::V1(SignedCommitment { + commitment, + signatures: vec![None], + }); + let hashof10 = backend.blockchain().expect_block_hash_from_id(&BlockId::Number(10)).unwrap(); + backend + .append_justification(hashof10, (BEEFY_ENGINE_ID, justif.encode())) + .unwrap(); + + // Test corner-case where session boundary == last beefy finalized, + // expect rounds initialized at last beefy finalized 10. + + // load persistent state - nothing in DB, should init at session boundary + let persisted_state = test_voter_init_setup(&mut net, &mut finality).unwrap(); + + // verify voter initialized with single session starting at block 10 + assert_eq!(persisted_state.voting_oracle().sessions().len(), 1); + let rounds = persisted_state.active_round().unwrap(); + assert_eq!(rounds.session_start(), 10); + assert_eq!(rounds.validator_set_id(), validator_set.id()); + + // verify block 10 is correctly marked as finalized + assert_eq!(persisted_state.best_beefy_block(), 10); + assert_eq!(persisted_state.best_grandpa_block(), 13); + // verify next vote target is diff-power-of-two block 12 + assert_eq!( + persisted_state + .voting_oracle() + .voting_target(persisted_state.best_beefy_block(), 13), + Some(12) + ); + + // verify state also saved to db + assert!(verify_persisted_version(&*backend)); + let state = load_persistent(&*backend).unwrap().unwrap(); + assert_eq!(state, persisted_state); +} + +#[test] +fn should_initialize_voter_at_latest_finalized() { + let keys = &[BeefyKeyring::Alice]; + 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(); + + // push 15 blocks with `AuthorityChange` digests every 10 blocks + net.generate_blocks_and_sync(15, 10, &validator_set, false); + + let mut finality = net.peer(0).client().as_client().finality_notification_stream().fuse(); + + // finalize 13 without justifications + let hashof13 = backend.blockchain().expect_block_hash_from_id(&BlockId::Number(13)).unwrap(); + net.peer(0).client().as_client().finalize_block(hashof13, None).unwrap(); + + // import/append BEEFY justification for block 12 + let commitment = Commitment { + payload: Payload::from_single_entry(known_payloads::MMR_ROOT_ID, vec![]), + block_number: 12, + validator_set_id: validator_set.id(), + }; + let justif = VersionedFinalityProof::<_, Signature>::V1(SignedCommitment { + commitment, + signatures: vec![None], + }); + let hashof12 = backend.blockchain().expect_block_hash_from_id(&BlockId::Number(12)).unwrap(); + backend + .append_justification(hashof12, (BEEFY_ENGINE_ID, justif.encode())) + .unwrap(); + + // Test initialization at last BEEFY finalized. + + // load persistent state - nothing in DB, should init at last BEEFY finalized + let persisted_state = test_voter_init_setup(&mut net, &mut finality).unwrap(); + + // verify voter initialized with single session starting at block 12 + assert_eq!(persisted_state.voting_oracle().sessions().len(), 1); + let rounds = persisted_state.active_round().unwrap(); + assert_eq!(rounds.session_start(), 12); + assert_eq!(rounds.validator_set_id(), validator_set.id()); + + // verify next vote target is 13 + assert_eq!(persisted_state.best_beefy_block(), 12); + assert_eq!(persisted_state.best_grandpa_block(), 13); + assert_eq!( + persisted_state + .voting_oracle() + .voting_target(persisted_state.best_beefy_block(), 13), + Some(13) + ); + + // verify state also saved to db + assert!(verify_persisted_version(&*backend)); + let state = load_persistent(&*backend).unwrap().unwrap(); + assert_eq!(state, persisted_state); +} diff --git a/client/beefy/src/worker.rs b/client/beefy/src/worker.rs index 9c14128624518..e387fed79c6a0 100644 --- a/client/beefy/src/worker.rs +++ b/client/beefy/src/worker.rs @@ -37,20 +37,20 @@ use sc_network_gossip::GossipEngine; use sp_api::{BlockId, ProvideRuntimeApi}; use sp_arithmetic::traits::{AtLeast32Bit, Saturating}; -use sp_blockchain::Backend as BlockchainBackend; use sp_consensus::SyncOracle; use sp_mmr_primitives::MmrApi; use sp_runtime::{ generic::OpaqueDigestItemId, - traits::{Block, Header, NumberFor}, + traits::{Block, Header, NumberFor, Zero}, SaturatedConversion, }; use beefy_primitives::{ crypto::{AuthorityId, Signature}, BeefyApi, Commitment, ConsensusLog, MmrRootHash, Payload, PayloadProvider, SignedCommitment, - ValidatorSet, VersionedFinalityProof, VoteMessage, BEEFY_ENGINE_ID, GENESIS_AUTHORITY_SET_ID, + ValidatorSet, VersionedFinalityProof, VoteMessage, BEEFY_ENGINE_ID, }; +use sc_utils::notification::NotificationReceiver; use crate::{ communication::{ @@ -63,10 +63,10 @@ use crate::{ metric_inc, metric_set, metrics::Metrics, round::Rounds, - BeefyVoterLinks, Client, KnownPeers, + BeefyVoterLinks, KnownPeers, }; -enum RoundAction { +pub(crate) enum RoundAction { Drop, Process, Enqueue, @@ -74,7 +74,9 @@ enum RoundAction { /// Responsible for the voting strategy. /// It chooses which incoming votes to accept and which votes to generate. -struct VoterOracle { +/// Keeps track of voting seen for current and future rounds. +#[derive(Debug, Decode, Encode, PartialEq)] +pub(crate) struct VoterOracle { /// Queue of known sessions. Keeps track of voting rounds (block numbers) within each session. /// /// There are three voter states coresponding to three queue states: @@ -90,35 +92,87 @@ struct VoterOracle { } impl VoterOracle { - pub fn new(min_block_delta: u32) -> Self { - Self { - sessions: VecDeque::new(), - // Always target at least one block better than current best beefy. - min_block_delta: min_block_delta.max(1), + /// Verify provided `sessions` satisfies requirements, then build `VoterOracle`. + pub fn checked_new( + sessions: VecDeque>, + min_block_delta: u32, + ) -> Option { + let mut prev_start = Zero::zero(); + let mut prev_validator_id = None; + // verifies the + let mut validate = || -> bool { + if sessions.is_empty() { + return false + } + for (idx, session) in sessions.iter().enumerate() { + if session.validators().is_empty() { + return false + } + if session.session_start() <= prev_start { + return false + } + #[cfg(not(test))] + if let Some(prev_id) = prev_validator_id { + if session.validator_set_id() <= prev_id { + return false + } + } + if idx != 0 && session.mandatory_done() { + return false + } + prev_start = session.session_start(); + prev_validator_id = Some(session.validator_set_id()); + } + true + }; + if validate() { + Some(VoterOracle { + sessions, + // Always target at least one block better than current best beefy. + min_block_delta: min_block_delta.max(1), + }) + } else { + error!(target: "beefy", "🥩 Invalid sessions queue: {:?}.", sessions); + None } } - /// Return mutable reference to rounds pertaining to first session in the queue. - /// Voting will always happen at the head of the queue. - pub fn rounds_mut(&mut self) -> Option<&mut Rounds> { + // Return reference to rounds pertaining to first session in the queue. + // Voting will always happen at the head of the queue. + fn active_rounds(&self) -> Option<&Rounds> { + self.sessions.front() + } + + // Return mutable reference to rounds pertaining to first session in the queue. + // Voting will always happen at the head of the queue. + fn active_rounds_mut(&mut self) -> Option<&mut Rounds> { self.sessions.front_mut() } + // Prune the sessions queue to keep the Oracle in one of the expected three states. + // + // To be called on each BEEFY finality and on each new rounds/session addition. + fn try_prune(&mut self) { + if self.sessions.len() > 1 { + // when there's multiple sessions, only keep the `!mandatory_done()` ones. + self.sessions.retain(|s| !s.mandatory_done()) + } + } + /// Add new observed session to the Oracle. pub fn add_session(&mut self, rounds: Rounds) { self.sessions.push_back(rounds); + // Once we add a new session we can drop/prune previous session if it's been finalized. self.try_prune(); } - /// Prune the queue to keep the Oracle in one of the expected three states. - /// - /// Call this function on each BEEFY finality, - /// or at the very least on each BEEFY mandatory block finality. - pub fn try_prune(&mut self) { - if self.sessions.len() > 1 { - // when there's multiple sessions, only keep the `!mandatory_done()` ones. - self.sessions.retain(|s| !s.mandatory_done()) - } + /// Finalize a particular block. + pub fn finalize(&mut self, block: NumberFor) -> Result<(), Error> { + // Conclude voting round for this block. + self.active_rounds_mut().ok_or(Error::UninitSession)?.conclude(block); + // Prune any now "finalized" sessions from queue. + self.try_prune(); + Ok(()) } /// Return current pending mandatory block, if any. @@ -170,7 +224,7 @@ impl VoterOracle { /// return `None` if there is no block we should vote on. pub fn voting_target( &self, - best_beefy: Option>, + best_beefy: NumberFor, best_grandpa: NumberFor, ) -> Option> { let rounds = if let Some(r) = self.sessions.front() { @@ -194,11 +248,9 @@ impl VoterOracle { } } -pub(crate) struct WorkerParams { - pub client: Arc, +pub(crate) struct WorkerParams { pub backend: Arc, pub payload_provider: P, - pub runtime: Arc, pub network: N, pub key_store: BeefyKeystore, pub known_peers: Arc>>, @@ -207,16 +259,48 @@ pub(crate) struct WorkerParams { pub on_demand_justifications: OnDemandJustificationsEngine, pub links: BeefyVoterLinks, pub metrics: Option, - pub min_block_delta: u32, + pub persisted_state: PersistedState, +} + +#[derive(Debug, Decode, Encode, PartialEq)] +pub(crate) struct PersistedState { + /// Best block we received a GRANDPA finality for. + best_grandpa_block_header: ::Header, + /// Best block a BEEFY voting round has been concluded for. + best_beefy_block: NumberFor, + /// Chooses which incoming votes to accept and which votes to generate. + /// Keeps track of voting seen for current and future rounds. + voting_oracle: VoterOracle, +} + +impl PersistedState { + pub fn checked_new( + grandpa_header: ::Header, + best_beefy: NumberFor, + sessions: VecDeque>, + min_block_delta: u32, + ) -> Option { + VoterOracle::checked_new(sessions, min_block_delta).map(|voting_oracle| PersistedState { + best_grandpa_block_header: grandpa_header, + best_beefy_block: best_beefy, + voting_oracle, + }) + } + + pub(crate) fn set_min_block_delta(&mut self, min_block_delta: u32) { + self.voting_oracle.min_block_delta = min_block_delta.max(1); + } + + pub(crate) fn set_best_grandpa(&mut self, best_grandpa: ::Header) { + self.best_grandpa_block_header = best_grandpa; + } } /// A BEEFY worker plays the BEEFY protocol -pub(crate) struct BeefyWorker { +pub(crate) struct BeefyWorker { // utilities - client: Arc, backend: Arc, payload_provider: P, - runtime: Arc, network: N, key_store: BeefyKeystore, @@ -233,23 +317,18 @@ pub(crate) struct BeefyWorker { // voter state /// BEEFY client metrics. metrics: Option, - /// Best block we received a GRANDPA finality for. - best_grandpa_block_header: ::Header, - /// Best block a BEEFY voting round has been concluded for. - best_beefy_block: Option>, /// Buffer holding votes for future processing. pending_votes: BTreeMap, Vec, AuthorityId, Signature>>>, /// Buffer holding justifications for future processing. pending_justifications: BTreeMap, BeefyVersionedFinalityProof>, - /// Chooses which incoming votes to accept and which votes to generate. - voting_oracle: VoterOracle, + /// Persisted voter state. + persisted_state: PersistedState, } -impl BeefyWorker +impl BeefyWorker where B: Block + Codec, BE: Backend, - C: Client, P: PayloadProvider, R: ProvideRuntimeApi, R::Api: BeefyApi + MmrApi>, @@ -261,12 +340,10 @@ where /// BEEFY pallet has been deployed on-chain. /// /// The BEEFY pallet is needed in order to keep track of the BEEFY authority set. - pub(crate) fn new(worker_params: WorkerParams) -> Self { + pub(crate) fn new(worker_params: WorkerParams) -> Self { let WorkerParams { - client, backend, payload_provider, - runtime, key_store, network, gossip_engine, @@ -275,19 +352,12 @@ where known_peers, links, metrics, - min_block_delta, + persisted_state, } = worker_params; - let last_finalized_header = backend - .blockchain() - .expect_header(BlockId::number(backend.blockchain().info().finalized_number)) - .expect("latest block always has header available; qed."); - BeefyWorker { - client: client.clone(), backend, payload_provider, - runtime, network, known_peers, key_store, @@ -296,14 +366,28 @@ where on_demand_justifications, links, metrics, - best_grandpa_block_header: last_finalized_header, - best_beefy_block: None, pending_votes: BTreeMap::new(), pending_justifications: BTreeMap::new(), - voting_oracle: VoterOracle::new(min_block_delta), + persisted_state, } } + fn best_grandpa_block(&self) -> NumberFor { + *self.persisted_state.best_grandpa_block_header.number() + } + + fn best_beefy_block(&self) -> NumberFor { + self.persisted_state.best_beefy_block + } + + fn voting_oracle(&self) -> &VoterOracle { + &self.persisted_state.voting_oracle + } + + fn active_rounds(&mut self) -> Option<&Rounds> { + self.persisted_state.voting_oracle.active_rounds() + } + /// Verify `active` validator set for `block` against the key store /// /// We want to make sure that we have _at least one_ key in our keystore that @@ -340,7 +424,7 @@ where debug!(target: "beefy", "🥩 New active validator set: {:?}", validator_set); // BEEFY should finalize a mandatory block during each session. - if let Some(active_session) = self.voting_oracle.rounds_mut() { + if let Some(active_session) = self.active_rounds() { if !active_session.mandatory_done() { debug!( target: "beefy", "🥩 New session {} while active session {} is still lagging.", @@ -357,7 +441,9 @@ where } let id = validator_set.id(); - self.voting_oracle.add_session(Rounds::new(new_session_start, validator_set)); + self.persisted_state + .voting_oracle + .add_session(Rounds::new(new_session_start, validator_set)); metric_set!(self, beefy_validator_set_id, id); info!( target: "beefy", @@ -370,9 +456,9 @@ where debug!(target: "beefy", "🥩 Finality notification: {:?}", notification); let header = ¬ification.header; - if *header.number() > *self.best_grandpa_block_header.number() { + if *header.number() > self.best_grandpa_block() { // update best GRANDPA finalized block we have seen - self.best_grandpa_block_header = header.clone(); + self.persisted_state.best_grandpa_block_header = header.clone(); // Check all (newly) finalized blocks for new session(s). let backend = self.backend.clone(); @@ -400,8 +486,8 @@ where vote: VoteMessage, AuthorityId, Signature>, ) -> Result<(), Error> { let block_num = vote.commitment.block_number; - let best_grandpa = *self.best_grandpa_block_header.number(); - match self.voting_oracle.triage_round(block_num, best_grandpa)? { + let best_grandpa = self.best_grandpa_block(); + match self.voting_oracle().triage_round(block_num, best_grandpa)? { RoundAction::Process => self.handle_vote( (vote.commitment.payload, vote.commitment.block_number), (vote.id, vote.signature), @@ -427,8 +513,8 @@ where VersionedFinalityProof::V1(ref sc) => sc, }; let block_num = signed_commitment.commitment.block_number; - let best_grandpa = *self.best_grandpa_block_header.number(); - match self.voting_oracle.triage_round(block_num, best_grandpa)? { + let best_grandpa = self.best_grandpa_block(); + match self.voting_oracle().triage_round(block_num, best_grandpa)? { RoundAction::Process => { debug!(target: "beefy", "🥩 Process justification for round: {:?}.", block_num); self.finalize(justification)? @@ -450,7 +536,11 @@ where ) -> Result<(), Error> { self.gossip_validator.note_round(round.1); - let rounds = self.voting_oracle.rounds_mut().ok_or(Error::UninitSession)?; + let rounds = self + .persisted_state + .voting_oracle + .active_rounds_mut() + .ok_or(Error::UninitSession)?; if rounds.add_vote(&round, vote, self_vote) { if let Some(signatures) = rounds.should_conclude(&round) { @@ -471,16 +561,26 @@ where info!(target: "beefy", "🥩 Round #{} concluded, finality_proof: {:?}.", round.1, finality_proof); // We created the `finality_proof` and know to be valid. + // New state is persisted after finalization. self.finalize(finality_proof)?; + } else { + if self_vote || self.voting_oracle().mandatory_pending() == Some(round.1) { + // Persist state after handling self vote to avoid double voting in case + // of voter restarts. + // Also persist state after handling mandatory block vote. + crate::aux_schema::write_voter_state(&*self.backend, &self.persisted_state) + .map_err(|e| Error::Backend(e.to_string()))?; + } } } Ok(()) } /// Provide BEEFY finality for block based on `finality_proof`: - /// 1. Prune irrelevant past sessions from the oracle, + /// 1. Prune now-irrelevant past sessions from the oracle, /// 2. Set BEEFY best block, - /// 3. Send best block hash and `finality_proof` to RPC worker. + /// 3. Persist voter state, + /// 4. Send best block hash and `finality_proof` to RPC worker. /// /// Expects `finality proof` to be valid. fn finalize(&mut self, finality_proof: BeefyVersionedFinalityProof) -> Result<(), Error> { @@ -488,14 +588,15 @@ where VersionedFinalityProof::V1(ref sc) => sc.commitment.block_number, }; - // Conclude voting round for this block. - self.voting_oracle.rounds_mut().ok_or(Error::UninitSession)?.conclude(block_num); - // Prune any now "finalized" sessions from queue. - self.voting_oracle.try_prune(); + // Finalize inner round and update voting_oracle state. + self.persisted_state.voting_oracle.finalize(block_num)?; - if Some(block_num) > self.best_beefy_block { + if block_num > self.best_beefy_block() { // Set new best BEEFY block number. - self.best_beefy_block = Some(block_num); + self.persisted_state.best_beefy_block = block_num; + crate::aux_schema::write_voter_state(&*self.backend, &self.persisted_state) + .map_err(|e| Error::Backend(e.to_string()))?; + metric_set!(self, beefy_best_block, block_num); self.on_demand_justifications.cancel_requests_older_than(block_num); @@ -528,7 +629,7 @@ where /// Handle previously buffered justifications and votes that now land in the voting interval. fn try_pending_justif_and_votes(&mut self) -> Result<(), Error> { - let best_grandpa = *self.best_grandpa_block_header.number(); + let best_grandpa = self.best_grandpa_block(); let _ph = PhantomData::::default(); fn to_process_for( @@ -546,7 +647,7 @@ where to_handle } // Interval of blocks for which we can process justifications and votes right now. - let mut interval = self.voting_oracle.accepted_interval(best_grandpa)?; + let mut interval = self.voting_oracle().accepted_interval(best_grandpa)?; // Process pending justifications. if !self.pending_justifications.is_empty() { @@ -558,7 +659,7 @@ where } } // Possibly new interval after processing justifications. - interval = self.voting_oracle.accepted_interval(best_grandpa)?; + interval = self.voting_oracle().accepted_interval(best_grandpa)?; } // Process pending votes. @@ -584,8 +685,8 @@ where fn try_to_vote(&mut self) -> Result<(), Error> { // Vote if there's now a new vote target. if let Some(target) = self - .voting_oracle - .voting_target(self.best_beefy_block, *self.best_grandpa_block_header.number()) + .voting_oracle() + .voting_target(self.best_beefy_block(), self.best_grandpa_block()) { metric_set!(self, beefy_should_vote_on, target); self.do_vote(target)?; @@ -601,8 +702,8 @@ where // Most of the time we get here, `target` is actually `best_grandpa`, // avoid getting header from backend in that case. - let target_header = if target_number == *self.best_grandpa_block_header.number() { - self.best_grandpa_block_header.clone() + let target_header = if target_number == self.best_grandpa_block() { + self.persisted_state.best_grandpa_block_header.clone() } else { self.backend .blockchain() @@ -624,7 +725,11 @@ where return Ok(()) }; - let rounds = self.voting_oracle.rounds_mut().ok_or(Error::UninitSession)?; + let rounds = self + .persisted_state + .voting_oracle + .active_rounds_mut() + .ok_or(Error::UninitSession)?; if !rounds.should_self_vote(&(payload.clone(), target_number)) { debug!(target: "beefy", "🥩 Don't double vote for block number: {:?}", target_number); return Ok(()) @@ -678,122 +783,16 @@ where Ok(()) } - /// Initialize BEEFY voter state. - /// - /// Should be called only once during worker initialization with latest GRANDPA finalized - /// `header` and the validator set `active` at that point. - fn initialize_voter(&mut self, header: &B::Header, active: ValidatorSet) { - // just a sanity check. - if let Some(rounds) = self.voting_oracle.rounds_mut() { - error!( - target: "beefy", - "🥩 Voting session already initialized at: {:?}, validator set id {}.", - rounds.session_start(), - rounds.validator_set_id(), - ); - return - } - - self.best_grandpa_block_header = header.clone(); - if active.id() == GENESIS_AUTHORITY_SET_ID { - // When starting from genesis, there is no session boundary digest. - // Just initialize `rounds` to Block #1 as BEEFY mandatory block. - info!(target: "beefy", "🥩 Initialize voting session at genesis, block 1."); - self.init_session_at(active, 1u32.into()); - } else { - // TODO (issue #11837): persist local progress to avoid following look-up during init. - let blockchain = self.backend.blockchain(); - let mut header = header.clone(); - - // Walk back the imported blocks and initialize voter either, at the last block with - // a BEEFY justification, or at this session's boundary; voter will resume from there. - loop { - if let Some(true) = blockchain - .justifications(header.hash()) - .ok() - .flatten() - .map(|justifs| justifs.get(BEEFY_ENGINE_ID).is_some()) - { - info!( - target: "beefy", - "🥩 Initialize voting session at last BEEFY finalized block: {:?}.", - *header.number() - ); - self.init_session_at(active, *header.number()); - // Mark the round as already finalized. - if let Some(round) = self.voting_oracle.rounds_mut() { - round.conclude(*header.number()); - } - self.best_beefy_block = Some(*header.number()); - break - } - - if let Some(validator_set) = find_authorities_change::(&header) { - info!( - target: "beefy", - "🥩 Initialize voting session at current session boundary: {:?}.", - *header.number() - ); - self.init_session_at(validator_set, *header.number()); - break - } - - // Move up the chain. - header = self - .client - .expect_header(BlockId::Hash(*header.parent_hash())) - // in case of db failure here we want to kill the worker - .expect("db failure, voter going down."); - } - } - } - - /// Wait for BEEFY runtime pallet to be available. - /// Should be called only once during worker initialization. - async fn wait_for_runtime_pallet(&mut self, finality: &mut Fuse>) { - let mut gossip_engine = &mut self.gossip_engine; - loop { - futures::select! { - notif = finality.next() => { - let notif = match notif { - Some(notif) => notif, - None => break - }; - let at = BlockId::hash(notif.header.hash()); - if let Some(active) = self.runtime.runtime_api().validator_set(&at).ok().flatten() { - self.initialize_voter(¬if.header, active); - if !self.network.is_major_syncing() { - if let Err(err) = self.try_to_vote() { - debug!(target: "beefy", "🥩 {}", err); - } - } - // Beefy pallet available and voter initialized. - break - } else { - trace!(target: "beefy", "🥩 Finality notification: {:?}", notif); - debug!(target: "beefy", "🥩 Waiting for BEEFY pallet to become available..."); - } - }, - _ = gossip_engine => { - break - } - } - } - } - /// Main loop for BEEFY worker. /// /// Wait for BEEFY runtime pallet to be available, then start the main async loop /// which is driven by finality notifications and gossiped votes. - pub(crate) async fn run(mut self) { - info!(target: "beefy", "🥩 run BEEFY worker, best grandpa: #{:?}.", self.best_grandpa_block_header.number()); - let mut block_import_justif = self.links.from_block_import_justif_stream.subscribe().fuse(); - // Subscribe to finality notifications before waiting for runtime pallet and reuse stream, - // so we process notifications for all finalized blocks after pallet is available. - let mut finality_notifications = self.client.finality_notification_stream().fuse(); - - self.wait_for_runtime_pallet(&mut finality_notifications).await; - trace!(target: "beefy", "🥩 BEEFY pallet available, starting voter."); + pub(crate) async fn run( + mut self, + mut block_import_justif: Fuse>>, + mut finality_notifications: Fuse>, + ) { + info!(target: "beefy", "🥩 run BEEFY worker, best grandpa: #{:?}.", self.best_grandpa_block()); let mut network_events = self.network.event_stream("network-gossip").fuse(); let mut votes = Box::pin( @@ -811,6 +810,22 @@ where ); loop { + // Don't bother voting or requesting justifications during major sync. + if !self.network.is_major_syncing() { + // If the current target is a mandatory block, + // make sure there's also an on-demand justification request out for it. + if let Some(block) = self.voting_oracle().mandatory_pending() { + // This only starts new request if there isn't already an active one. + self.on_demand_justifications.request(block); + } + // There were external events, 'state' is changed, author a vote if needed/possible. + if let Err(err) = self.try_to_vote() { + debug!(target: "beefy", "🥩 {}", err); + } + } else { + debug!(target: "beefy", "🥩 Skipping voting while major syncing."); + } + let mut gossip_engine = &mut self.gossip_engine; // Wait for, and handle external events. // The branches below only change 'state', actual voting happen afterwards, @@ -878,22 +893,6 @@ where if let Err(err) = self.try_pending_justif_and_votes() { debug!(target: "beefy", "🥩 {}", err); } - - // Don't bother voting or requesting justifications during major sync. - if !self.network.is_major_syncing() { - // If the current target is a mandatory block, - // make sure there's also an on-demand justification request out for it. - if let Some(block) = self.voting_oracle.mandatory_pending() { - // This only starts new request if there isn't already an active one. - self.on_demand_justifications.request(block); - } - // There were external events, 'state' is changed, author a vote if needed/possible. - if let Err(err) = self.try_to_vote() { - debug!(target: "beefy", "🥩 {}", err); - } - } else { - debug!(target: "beefy", "🥩 Skipping voting while major syncing."); - } } } @@ -914,7 +913,7 @@ where /// 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. -fn find_authorities_change(header: &B::Header) -> Option> +pub(crate) fn find_authorities_change(header: &B::Header) -> Option> where B: Block, { @@ -930,49 +929,32 @@ where /// Calculate next block number to vote on. /// /// Return `None` if there is no voteable target yet. -fn vote_target( - best_grandpa: N, - best_beefy: Option, - session_start: N, - min_delta: u32, -) -> Option +fn vote_target(best_grandpa: N, best_beefy: N, session_start: N, min_delta: u32) -> Option where N: AtLeast32Bit + Copy + Debug, { // if the mandatory block (session_start) does not have a beefy justification yet, // we vote on it - let target = match best_beefy { - None => { - debug!( - target: "beefy", - "🥩 vote target - mandatory block: #{:?}", - session_start, - ); - session_start - }, - Some(bbb) if bbb < session_start => { - debug!( - target: "beefy", - "🥩 vote target - mandatory block: #{:?}", - session_start, - ); - session_start - }, - Some(bbb) => { - let diff = best_grandpa.saturating_sub(bbb) + 1u32.into(); - let diff = diff.saturated_into::() / 2; - let target = bbb + min_delta.max(diff.next_power_of_two()).into(); - - debug!( - target: "beefy", - "🥩 vote target - diff: {:?}, next_power_of_two: {:?}, target block: #{:?}", - diff, - diff.next_power_of_two(), - target, - ); + let target = if best_beefy < session_start { + debug!( + target: "beefy", + "🥩 vote target - mandatory block: #{:?}", + session_start, + ); + session_start + } else { + let diff = best_grandpa.saturating_sub(best_beefy) + 1u32.into(); + let diff = diff.saturated_into::() / 2; + let target = best_beefy + min_delta.max(diff.next_power_of_two()).into(); + trace!( + target: "beefy", + "🥩 vote target - diff: {:?}, next_power_of_two: {:?}, target block: #{:?}", + diff, + diff.next_power_of_two(), + target, + ); - target - }, + target }; // Don't vote for targets until they've been finalized @@ -1001,22 +983,47 @@ pub(crate) mod tests { use futures::{executor::block_on, future::poll_fn, task::Poll}; use sc_client_api::{Backend as BackendT, HeaderBackend}; use sc_network::NetworkService; - use sc_network_test::{PeersFullClient, TestNetFactory}; + use sc_network_test::TestNetFactory; use sp_api::HeaderT; use sp_blockchain::Backend as BlockchainBackendT; + use sp_runtime::traits::{One, Zero}; use substrate_test_runtime_client::{ runtime::{Block, Digest, DigestItem, Header, H256}, - Backend, ClientExt, + Backend, }; + impl PersistedState { + pub fn voting_oracle(&self) -> &VoterOracle { + &self.voting_oracle + } + + pub fn active_round(&self) -> Option<&Rounds> { + self.voting_oracle.active_rounds() + } + + pub fn best_beefy_block(&self) -> NumberFor { + self.best_beefy_block + } + + pub fn best_grandpa_block(&self) -> NumberFor { + *self.best_grandpa_block_header.number() + } + } + + impl VoterOracle { + pub fn sessions(&self) -> &VecDeque> { + &self.sessions + } + } + fn create_beefy_worker( peer: &BeefyPeer, key: &Keyring, min_block_delta: u32, + genesis_validator_set: ValidatorSet, ) -> BeefyWorker< Block, Backend, - PeersFullClient, MmrRootProvider, TestApi, Arc>, @@ -1040,6 +1047,7 @@ pub(crate) mod tests { to_rpc_best_block_sender, }; + let backend = peer.client().as_backend(); let api = Arc::new(TestApi {}); let network = peer.network_service().clone(); let known_peers = Arc::new(Mutex::new(KnownPeers::new())); @@ -1052,123 +1060,130 @@ pub(crate) mod tests { "/beefy/justifs/1".into(), known_peers.clone(), ); - let payload_provider = MmrRootProvider::new(api.clone()); + let at = BlockId::number(Zero::zero()); + let genesis_header = backend.blockchain().expect_header(at).unwrap(); + let persisted_state = PersistedState::checked_new( + genesis_header, + Zero::zero(), + vec![Rounds::new(One::one(), genesis_validator_set)].into(), + min_block_delta, + ) + .unwrap(); + let payload_provider = MmrRootProvider::new(api); let worker_params = crate::worker::WorkerParams { - client: peer.client().as_client(), - backend: peer.client().as_backend(), + backend, payload_provider, - runtime: api, key_store: Some(keystore).into(), known_peers, links, gossip_engine, gossip_validator, - min_block_delta, metrics: None, network, on_demand_justifications, + persisted_state, }; - BeefyWorker::<_, _, _, _, _, _>::new(worker_params) + BeefyWorker::<_, _, _, _, _>::new(worker_params) } #[test] fn vote_on_min_block_delta() { - let t = vote_target(1u32, Some(1), 1, 4); + let t = vote_target(1u32, 1, 1, 4); assert_eq!(None, t); - let t = vote_target(2u32, Some(1), 1, 4); + let t = vote_target(2u32, 1, 1, 4); assert_eq!(None, t); - let t = vote_target(4u32, Some(2), 1, 4); + let t = vote_target(4u32, 2, 1, 4); assert_eq!(None, t); - let t = vote_target(6u32, Some(2), 1, 4); + let t = vote_target(6u32, 2, 1, 4); assert_eq!(Some(6), t); - let t = vote_target(9u32, Some(4), 1, 4); + let t = vote_target(9u32, 4, 1, 4); assert_eq!(Some(8), t); - let t = vote_target(10u32, Some(10), 1, 8); + let t = vote_target(10u32, 10, 1, 8); assert_eq!(None, t); - let t = vote_target(12u32, Some(10), 1, 8); + let t = vote_target(12u32, 10, 1, 8); assert_eq!(None, t); - let t = vote_target(18u32, Some(10), 1, 8); + let t = vote_target(18u32, 10, 1, 8); assert_eq!(Some(18), t); } #[test] fn vote_on_power_of_two() { - let t = vote_target(1008u32, Some(1000), 1, 4); + let t = vote_target(1008u32, 1000, 1, 4); assert_eq!(Some(1004), t); - let t = vote_target(1016u32, Some(1000), 1, 4); + let t = vote_target(1016u32, 1000, 1, 4); assert_eq!(Some(1008), t); - let t = vote_target(1032u32, Some(1000), 1, 4); + let t = vote_target(1032u32, 1000, 1, 4); assert_eq!(Some(1016), t); - let t = vote_target(1064u32, Some(1000), 1, 4); + let t = vote_target(1064u32, 1000, 1, 4); assert_eq!(Some(1032), t); - let t = vote_target(1128u32, Some(1000), 1, 4); + let t = vote_target(1128u32, 1000, 1, 4); assert_eq!(Some(1064), t); - let t = vote_target(1256u32, Some(1000), 1, 4); + let t = vote_target(1256u32, 1000, 1, 4); assert_eq!(Some(1128), t); - let t = vote_target(1512u32, Some(1000), 1, 4); + let t = vote_target(1512u32, 1000, 1, 4); assert_eq!(Some(1256), t); - let t = vote_target(1024u32, Some(1), 1, 4); + let t = vote_target(1024u32, 1, 1, 4); assert_eq!(Some(513), t); } #[test] fn vote_on_target_block() { - let t = vote_target(1008u32, Some(1002), 1, 4); + let t = vote_target(1008u32, 1002, 1, 4); assert_eq!(Some(1006), t); - let t = vote_target(1010u32, Some(1002), 1, 4); + let t = vote_target(1010u32, 1002, 1, 4); assert_eq!(Some(1006), t); - let t = vote_target(1016u32, Some(1006), 1, 4); + let t = vote_target(1016u32, 1006, 1, 4); assert_eq!(Some(1014), t); - let t = vote_target(1022u32, Some(1006), 1, 4); + let t = vote_target(1022u32, 1006, 1, 4); assert_eq!(Some(1014), t); - let t = vote_target(1032u32, Some(1012), 1, 4); + let t = vote_target(1032u32, 1012, 1, 4); assert_eq!(Some(1028), t); - let t = vote_target(1044u32, Some(1012), 1, 4); + let t = vote_target(1044u32, 1012, 1, 4); assert_eq!(Some(1028), t); - let t = vote_target(1064u32, Some(1014), 1, 4); + let t = vote_target(1064u32, 1014, 1, 4); assert_eq!(Some(1046), t); - let t = vote_target(1078u32, Some(1014), 1, 4); + let t = vote_target(1078u32, 1014, 1, 4); assert_eq!(Some(1046), t); - let t = vote_target(1128u32, Some(1008), 1, 4); + let t = vote_target(1128u32, 1008, 1, 4); assert_eq!(Some(1072), t); - let t = vote_target(1136u32, Some(1008), 1, 4); + let t = vote_target(1136u32, 1008, 1, 4); assert_eq!(Some(1072), t); } #[test] fn vote_on_mandatory_block() { - let t = vote_target(1008u32, Some(1002), 1004, 4); + let t = vote_target(1008u32, 1002, 1004, 4); assert_eq!(Some(1004), t); - let t = vote_target(1016u32, Some(1006), 1007, 4); + let t = vote_target(1016u32, 1006, 1007, 4); assert_eq!(Some(1007), t); - let t = vote_target(1064u32, Some(1014), 1063, 4); + let t = vote_target(1064u32, 1014, 1063, 4); assert_eq!(Some(1063), t); - let t = vote_target(1320u32, Some(1012), 1234, 4); + let t = vote_target(1320u32, 1012, 1234, 4); assert_eq!(Some(1234), t); - let t = vote_target(1128u32, Some(1008), 1008, 4); + let t = vote_target(1128u32, 1008, 1008, 4); assert_eq!(Some(1072), t); } #[test] fn should_vote_target() { - let mut oracle = VoterOracle::::new(1); + let mut oracle = VoterOracle:: { min_block_delta: 1, sessions: VecDeque::new() }; // rounds not initialized -> should vote: `None` - assert_eq!(oracle.voting_target(None, 1), None); + assert_eq!(oracle.voting_target(0, 1), None); let keys = &[Keyring::Alice]; let validator_set = ValidatorSet::new(make_beefy_ids(keys), 0).unwrap(); @@ -1177,29 +1192,29 @@ pub(crate) mod tests { // under min delta oracle.min_block_delta = 4; - assert_eq!(oracle.voting_target(Some(1), 1), None); - assert_eq!(oracle.voting_target(Some(2), 5), None); + assert_eq!(oracle.voting_target(1, 1), None); + assert_eq!(oracle.voting_target(2, 5), None); // vote on min delta - assert_eq!(oracle.voting_target(Some(4), 9), Some(8)); + assert_eq!(oracle.voting_target(4, 9), Some(8)); oracle.min_block_delta = 8; - assert_eq!(oracle.voting_target(Some(10), 18), Some(18)); + assert_eq!(oracle.voting_target(10, 18), Some(18)); // vote on power of two oracle.min_block_delta = 1; - assert_eq!(oracle.voting_target(Some(1000), 1008), Some(1004)); - assert_eq!(oracle.voting_target(Some(1000), 1016), Some(1008)); + assert_eq!(oracle.voting_target(1000, 1008), Some(1004)); + assert_eq!(oracle.voting_target(1000, 1016), Some(1008)); // nothing new to vote on - assert_eq!(oracle.voting_target(Some(1000), 1000), None); + assert_eq!(oracle.voting_target(1000, 1000), None); // vote on mandatory oracle.sessions.clear(); oracle.add_session(Rounds::new(1000, validator_set.clone())); - assert_eq!(oracle.voting_target(None, 1008), Some(1000)); + assert_eq!(oracle.voting_target(0, 1008), Some(1000)); oracle.sessions.clear(); oracle.add_session(Rounds::new(1001, validator_set.clone())); - assert_eq!(oracle.voting_target(Some(1000), 1008), Some(1001)); + assert_eq!(oracle.voting_target(1000, 1008), Some(1001)); } #[test] @@ -1207,7 +1222,7 @@ pub(crate) mod tests { let keys = &[Keyring::Alice]; let validator_set = ValidatorSet::new(make_beefy_ids(keys), 0).unwrap(); - let mut oracle = VoterOracle::::new(1); + let mut oracle = VoterOracle:: { min_block_delta: 1, sessions: VecDeque::new() }; // rounds not initialized -> should accept votes: `None` assert!(oracle.accepted_interval(1).is_err()); @@ -1295,7 +1310,7 @@ pub(crate) mod tests { let keys = &[Keyring::Alice]; 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); + let mut worker = create_beefy_worker(&net.peer(0), &keys[0], 1, validator_set.clone()); // keystore doesn't contain other keys than validators' assert_eq!(worker.verify_validator_set(&1, &validator_set), Ok(())); @@ -1319,7 +1334,9 @@ 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); + let mut worker = create_beefy_worker(&net.peer(0), &keys[0], 1, validator_set.clone()); + // remove default session, will manually add custom one. + worker.persisted_state.voting_oracle.sessions.clear(); let keys = keys.iter().cloned().enumerate(); let (mut best_block_streams, mut finality_proofs) = @@ -1337,7 +1354,7 @@ pub(crate) mod tests { }; // no 'best beefy block' or finality proofs - assert_eq!(worker.best_beefy_block, None); + assert_eq!(worker.best_beefy_block(), 0); block_on(poll_fn(move |cx| { assert_eq!(best_block_stream.poll_next_unpin(cx), Poll::Pending); assert_eq!(finality_proof.poll_next_unpin(cx), Poll::Pending); @@ -1351,11 +1368,14 @@ pub(crate) mod tests { let mut finality_proof = finality_proofs.drain(..).next().unwrap(); let justif = create_finality_proof(1); // create new session at block #1 - worker.voting_oracle.add_session(Rounds::new(1, validator_set.clone())); + worker + .persisted_state + .voting_oracle + .add_session(Rounds::new(1, validator_set.clone())); // try to finalize block #1 worker.finalize(justif.clone()).unwrap(); // verify block finalized - assert_eq!(worker.best_beefy_block, Some(1)); + assert_eq!(worker.best_beefy_block(), 1); block_on(poll_fn(move |cx| { // unknown hash -> nothing streamed assert_eq!(best_block_stream.poll_next_unpin(cx), Poll::Pending); @@ -1380,14 +1400,14 @@ pub(crate) mod tests { let justif = create_finality_proof(2); // create new session at block #2 - worker.voting_oracle.add_session(Rounds::new(2, validator_set)); + worker.persisted_state.voting_oracle.add_session(Rounds::new(2, validator_set)); worker.finalize(justif).unwrap(); // verify old session pruned - assert_eq!(worker.voting_oracle.sessions.len(), 1); + assert_eq!(worker.voting_oracle().sessions.len(), 1); // new session starting at #2 is in front - assert_eq!(worker.voting_oracle.rounds_mut().unwrap().session_start(), 2); + assert_eq!(worker.active_rounds().unwrap().session_start(), 2); // verify block finalized - assert_eq!(worker.best_beefy_block, Some(2)); + assert_eq!(worker.best_beefy_block(), 2); block_on(poll_fn(move |cx| { match best_block_stream.poll_next_unpin(cx) { // expect Some(hash-of-block-2) @@ -1407,15 +1427,12 @@ pub(crate) mod tests { #[test] fn should_init_session() { - let keys = &[Keyring::Alice]; + 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); - - assert!(worker.voting_oracle.sessions.is_empty()); + let mut worker = create_beefy_worker(&net.peer(0), &keys[0], 1, validator_set.clone()); - worker.init_session_at(validator_set.clone(), 1); - let worker_rounds = worker.voting_oracle.rounds_mut().unwrap(); + let worker_rounds = worker.active_rounds().unwrap(); assert_eq!(worker_rounds.session_start(), 1); assert_eq!(worker_rounds.validators(), validator_set.validators()); assert_eq!(worker_rounds.validator_set_id(), validator_set.id()); @@ -1426,13 +1443,13 @@ pub(crate) mod tests { worker.init_session_at(new_validator_set.clone(), 11); // Since mandatory is not done for old rounds, we still get those. - let rounds = worker.voting_oracle.rounds_mut().unwrap(); + let rounds = worker.persisted_state.voting_oracle.active_rounds_mut().unwrap(); assert_eq!(rounds.validator_set_id(), validator_set.id()); // Let's finalize mandatory. rounds.test_set_mandatory_done(true); - worker.voting_oracle.try_prune(); + worker.persisted_state.voting_oracle.try_prune(); // Now we should get the next round. - let rounds = worker.voting_oracle.rounds_mut().unwrap(); + let rounds = worker.active_rounds().unwrap(); // Expect new values. assert_eq!(rounds.session_start(), 11); assert_eq!(rounds.validators(), new_validator_set.validators()); @@ -1444,7 +1461,9 @@ 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); + let mut worker = create_beefy_worker(&net.peer(0), &keys[0], 1, validator_set.clone()); + // remove default session, will manually add custom one. + worker.persisted_state.voting_oracle.sessions.clear(); fn new_vote( block_number: NumberFor, @@ -1470,8 +1489,11 @@ pub(crate) mod tests { Digest::default(), ); - worker.voting_oracle.add_session(Rounds::new(10, validator_set.clone())); - worker.best_grandpa_block_header = best_grandpa_header; + worker + .persisted_state + .voting_oracle + .add_session(Rounds::new(10, validator_set.clone())); + worker.persisted_state.best_grandpa_block_header = best_grandpa_header; // triage votes for blocks 10..13 worker.triage_incoming_vote(new_vote(10)).unwrap(); @@ -1492,118 +1514,16 @@ pub(crate) mod tests { assert!(votes.next().is_none()); // simulate mandatory done, and retry buffered votes - worker.voting_oracle.rounds_mut().unwrap().test_set_mandatory_done(true); + worker + .persisted_state + .voting_oracle + .active_rounds_mut() + .unwrap() + .test_set_mandatory_done(true); worker.try_pending_justif_and_votes().unwrap(); // all blocks <= grandpa finalized should have been handled, rest still buffered let mut votes = worker.pending_votes.values(); assert_eq!(votes.next().unwrap().first().unwrap().commitment.block_number, 21); assert_eq!(votes.next().unwrap().first().unwrap().commitment.block_number, 22); } - - #[test] - fn should_initialize_correct_voter() { - let keys = &[Keyring::Alice]; - let validator_set = ValidatorSet::new(make_beefy_ids(keys), 1).unwrap(); - let mut net = BeefyTestNet::new(1); - let backend = net.peer(0).client().as_backend(); - - // push 15 blocks with `AuthorityChange` digests every 10 blocks - net.generate_blocks_and_sync(15, 10, &validator_set, false); - // finalize 13 without justifications - let hashof13 = - backend.blockchain().expect_block_hash_from_id(&BlockId::Number(13)).unwrap(); - net.peer(0).client().as_client().finalize_block(hashof13, None).unwrap(); - - // Test initialization at session boundary. - { - let mut worker = create_beefy_worker(&net.peer(0), &keys[0], 1); - - // initialize voter at block 13, expect rounds initialized at session_start = 10 - let header = backend.blockchain().header(BlockId::number(13)).unwrap().unwrap(); - worker.initialize_voter(&header, validator_set.clone()); - - // verify voter initialized with single session starting at block 10 - assert_eq!(worker.voting_oracle.sessions.len(), 1); - let rounds = worker.voting_oracle.rounds_mut().unwrap(); - assert_eq!(rounds.session_start(), 10); - assert_eq!(rounds.validator_set_id(), validator_set.id()); - - // verify next vote target is mandatory block 10 - assert_eq!(worker.best_beefy_block, None); - assert_eq!(*worker.best_grandpa_block_header.number(), 13); - assert_eq!(worker.voting_oracle.voting_target(worker.best_beefy_block, 13), Some(10)); - } - - // Test corner-case where session boundary == last beefy finalized. - { - let mut worker = create_beefy_worker(&net.peer(0), &keys[0], 1); - - // import/append BEEFY justification for session boundary block 10 - let commitment = Commitment { - payload: Payload::from_single_entry(known_payloads::MMR_ROOT_ID, vec![]), - block_number: 10, - validator_set_id: validator_set.id(), - }; - let justif = VersionedFinalityProof::<_, Signature>::V1(SignedCommitment { - commitment, - signatures: vec![None], - }); - let hashof10 = - backend.blockchain().expect_block_hash_from_id(&BlockId::Number(10)).unwrap(); - backend - .append_justification(hashof10, (BEEFY_ENGINE_ID, justif.encode())) - .unwrap(); - - // initialize voter at block 13, expect rounds initialized at last beefy finalized 10 - let header = backend.blockchain().header(BlockId::number(13)).unwrap().unwrap(); - worker.initialize_voter(&header, validator_set.clone()); - - // verify voter initialized with single session starting at block 10 - assert_eq!(worker.voting_oracle.sessions.len(), 1); - let rounds = worker.voting_oracle.rounds_mut().unwrap(); - assert_eq!(rounds.session_start(), 10); - assert_eq!(rounds.validator_set_id(), validator_set.id()); - - // verify next vote target is mandatory block 10 - assert_eq!(worker.best_beefy_block, Some(10)); - assert_eq!(*worker.best_grandpa_block_header.number(), 13); - assert_eq!(worker.voting_oracle.voting_target(worker.best_beefy_block, 13), Some(12)); - } - - // Test initialization at last BEEFY finalized. - { - let mut worker = create_beefy_worker(&net.peer(0), &keys[0], 1); - - // import/append BEEFY justification for block 12 - let commitment = Commitment { - payload: Payload::from_single_entry(known_payloads::MMR_ROOT_ID, vec![]), - block_number: 12, - validator_set_id: validator_set.id(), - }; - let justif = VersionedFinalityProof::<_, Signature>::V1(SignedCommitment { - commitment, - signatures: vec![None], - }); - let hashof12 = - backend.blockchain().expect_block_hash_from_id(&BlockId::Number(12)).unwrap(); - backend - .append_justification(hashof12, (BEEFY_ENGINE_ID, justif.encode())) - .unwrap(); - - // initialize voter at block 13, expect rounds initialized at last beefy finalized 12 - let header = backend.blockchain().header(BlockId::number(13)).unwrap().unwrap(); - worker.initialize_voter(&header, validator_set.clone()); - - // verify voter initialized with single session starting at block 12 - assert_eq!(worker.voting_oracle.sessions.len(), 1); - let rounds = worker.voting_oracle.rounds_mut().unwrap(); - assert_eq!(rounds.session_start(), 12); - assert_eq!(rounds.validator_set_id(), validator_set.id()); - - // verify next vote target is 13 - assert_eq!(worker.best_beefy_block, Some(12)); - assert_eq!(*worker.best_grandpa_block_header.number(), 13); - assert_eq!(worker.voting_oracle.voting_target(worker.best_beefy_block, 13), Some(13)); - } - } } diff --git a/frame/beefy/src/lib.rs b/frame/beefy/src/lib.rs index 305b158124b67..4cb23107e7843 100644 --- a/frame/beefy/src/lib.rs +++ b/frame/beefy/src/lib.rs @@ -34,6 +34,7 @@ use sp_std::prelude::*; use beefy_primitives::{ AuthorityIndex, ConsensusLog, OnNewValidatorSet, ValidatorSet, BEEFY_ENGINE_ID, + GENESIS_AUTHORITY_SET_ID, }; #[cfg(test)] @@ -162,7 +163,7 @@ impl Pallet { BoundedSlice::::try_from(authorities.as_slice()) .map_err(|_| ())?; - let id = 0; + let id = GENESIS_AUTHORITY_SET_ID; >::put(bounded_authorities); >::put(id); // Like `pallet_session`, initialize the next validator set as well. diff --git a/primitives/beefy/src/lib.rs b/primitives/beefy/src/lib.rs index 453eb67315d4e..d7ac091491bff 100644 --- a/primitives/beefy/src/lib.rs +++ b/primitives/beefy/src/lib.rs @@ -113,7 +113,7 @@ pub mod crypto { /// The `ConsensusEngineId` of BEEFY. pub const BEEFY_ENGINE_ID: sp_runtime::ConsensusEngineId = *b"BEEF"; -/// Authority set id starts with zero at genesis +/// Authority set id starts with zero at BEEFY pallet genesis. pub const GENESIS_AUTHORITY_SET_ID: u64 = 0; /// A typedef for validator set id.