grandpa: make the VotingRule API async (#8101)

* grandpa: make the VotingRule api async

* grandpa: add docs to VotingRuleResult

* grandpa: formatting

* grandpa: use async blocks

Co-authored-by: Bastian Köcher <bkchr@users.noreply.github.com>

* grandpa: expose VotingRuleResult

* grandpa: revert some broken changes to async syntax

* grandpa: use finality-grandpa v0.14.0

* grandpa: bump impl_version

Co-authored-by: Bastian Köcher <bkchr@users.noreply.github.com>
This commit is contained in:
André Silva
2021-02-15 18:28:04 +00:00
committed by GitHub
parent 11894bc21a
commit 24739d1ab0
12 changed files with 209 additions and 171 deletions
+3 -2
View File
@@ -1589,9 +1589,9 @@ dependencies = [
[[package]]
name = "finality-grandpa"
version = "0.13.0"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2cd795898c348a8ec9edc66ec9e014031c764d4c88cc26d09b492cd93eb41339"
checksum = "c6447e2f8178843749e8c8003206def83ec124a7859475395777a28b5338647c"
dependencies = [
"either",
"futures 0.3.12",
@@ -7089,6 +7089,7 @@ version = "0.9.0"
dependencies = [
"assert_matches",
"derive_more",
"dyn-clone",
"finality-grandpa",
"fork-tree",
"futures 0.3.12",
+1 -1
View File
@@ -113,7 +113,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion {
// implementation changes and behavior does not, then leave spec_version as
// is and increment impl_version.
spec_version: 264,
impl_version: 0,
impl_version: 1,
apis: RUNTIME_API_VERSIONS,
transaction_version: 2,
};
+3 -2
View File
@@ -16,6 +16,7 @@ targets = ["x86_64-unknown-linux-gnu"]
[dependencies]
derive_more = "0.99.2"
dyn-clone = "1.0"
fork-tree = { version = "3.0.0", path = "../../utils/fork-tree" }
futures = "0.3.9"
futures-timer = "3.0.1"
@@ -43,13 +44,13 @@ sc-network-gossip = { version = "0.9.0", path = "../network-gossip" }
sp-finality-grandpa = { version = "3.0.0", path = "../../primitives/finality-grandpa" }
prometheus-endpoint = { package = "substrate-prometheus-endpoint", path = "../../utils/prometheus", version = "0.9.0"}
sc-block-builder = { version = "0.9.0", path = "../block-builder" }
finality-grandpa = { version = "0.13.0", features = ["derive-codec"] }
finality-grandpa = { version = "0.14.0", features = ["derive-codec"] }
pin-project = "1.0.4"
linked-hash-map = "0.5.2"
[dev-dependencies]
assert_matches = "1.3.0"
finality-grandpa = { version = "0.13.0", features = ["derive-codec", "test-helpers"] }
finality-grandpa = { version = "0.14.0", features = ["derive-codec", "test-helpers"] }
sc-network = { version = "0.9.0", path = "../network" }
sc-network-test = { version = "0.8.0", path = "../network/test" }
sp-keyring = { version = "3.0.0", path = "../../primitives/keyring" }
@@ -14,7 +14,7 @@ sc-rpc = { version = "3.0.0", path = "../../rpc" }
sp-blockchain = { version = "3.0.0", path = "../../../primitives/blockchain" }
sp-core = { version = "3.0.0", path = "../../../primitives/core" }
sp-runtime = { version = "3.0.0", path = "../../../primitives/runtime" }
finality-grandpa = { version = "0.13.0", features = ["derive-codec"] }
finality-grandpa = { version = "0.14.0", features = ["derive-codec"] }
jsonrpc-core = "15.1.0"
jsonrpc-core-client = "15.1.0"
jsonrpc-derive = "15.1.0"
@@ -592,100 +592,6 @@ where
fn ancestry(&self, base: Block::Hash, block: Block::Hash) -> Result<Vec<Block::Hash>, GrandpaError> {
ancestry(&self.client, base, block)
}
fn best_chain_containing(&self, block: Block::Hash) -> Option<(Block::Hash, NumberFor<Block>)> {
// NOTE: when we finalize an authority set change through the sync protocol the voter is
// signaled asynchronously. therefore the voter could still vote in the next round
// before activating the new set. the `authority_set` is updated immediately thus we
// restrict the voter based on that.
if self.set_id != self.authority_set.set_id() {
return None;
}
let base_header = match self.client.header(BlockId::Hash(block)).ok()? {
Some(h) => h,
None => {
debug!(target: "afg", "Encountered error finding best chain containing {:?}: couldn't find base block", block);
return None;
}
};
// we refuse to vote beyond the current limit number where transitions are scheduled to
// occur.
// once blocks are finalized that make that transition irrelevant or activate it,
// we will proceed onwards. most of the time there will be no pending transition.
// the limit, if any, is guaranteed to be higher than or equal to the given base number.
let limit = self.authority_set.current_limit(*base_header.number());
debug!(target: "afg", "Finding best chain containing block {:?} with number limit {:?}", block, limit);
match self.select_chain.finality_target(block, None) {
Ok(Some(best_hash)) => {
let best_header = self.client.header(BlockId::Hash(best_hash)).ok()?
.expect("Header known to exist after `finality_target` call; qed");
// check if our vote is currently being limited due to a pending change
let limit = limit.filter(|limit| limit < best_header.number());
let target;
let target_header = if let Some(target_number) = limit {
let mut target_header = best_header.clone();
// walk backwards until we find the target block
loop {
if *target_header.number() < target_number {
unreachable!(
"we are traversing backwards from a known block; \
blocks are stored contiguously; \
qed"
);
}
if *target_header.number() == target_number {
break;
}
target_header = self.client.header(BlockId::Hash(*target_header.parent_hash())).ok()?
.expect("Header known to exist after `finality_target` call; qed");
}
target = target_header;
&target
} else {
// otherwise just use the given best as the target
&best_header
};
// restrict vote according to the given voting rule, if the
// voting rule doesn't restrict the vote then we keep the
// previous target.
//
// note that we pass the original `best_header`, i.e. before the
// authority set limit filter, which can be considered a
// mandatory/implicit voting rule.
//
// we also make sure that the restricted vote is higher than the
// round base (i.e. last finalized), otherwise the value
// returned by the given voting rule is ignored and the original
// target is used instead.
self.voting_rule
.restrict_vote(&*self.client, &base_header, &best_header, target_header)
.filter(|(_, restricted_number)| {
// we can only restrict votes within the interval [base, target]
restricted_number >= base_header.number() &&
restricted_number < target_header.number()
})
.or_else(|| Some((target_header.hash(), *target_header.number())))
},
Ok(None) => {
debug!(target: "afg", "Encountered error finding best chain containing {:?}: couldn't find target block", block);
None
}
Err(e) => {
debug!(target: "afg", "Encountered error finding best chain containing {:?}: {:?}", block, e);
None
}
}
}
}
@@ -733,6 +639,14 @@ where
NumberFor<Block>: BlockNumberOps,
{
type Timer = Pin<Box<dyn Future<Output = Result<(), Self::Error>> + Send + Sync>>;
type BestChain = Pin<
Box<
dyn Future<Output = Result<Option<(Block::Hash, NumberFor<Block>)>, Self::Error>>
+ Send
+ Sync
>,
>;
type Id = AuthorityId;
type Signature = AuthoritySignature;
@@ -747,6 +661,119 @@ where
type Error = CommandOrError<Block::Hash, NumberFor<Block>>;
fn best_chain_containing(&self, block: Block::Hash) -> Self::BestChain {
let find_best_chain = || {
// NOTE: when we finalize an authority set change through the sync protocol the voter is
// signaled asynchronously. therefore the voter could still vote in the next round
// before activating the new set. the `authority_set` is updated immediately thus we
// restrict the voter based on that.
if self.set_id != self.authority_set.set_id() {
return None;
}
let base_header = match self.client.header(BlockId::Hash(block)).ok()? {
Some(h) => h,
None => {
debug!(target: "afg", "Encountered error finding best chain containing {:?}: couldn't find base block", block);
return None;
}
};
// we refuse to vote beyond the current limit number where transitions are scheduled to
// occur.
// once blocks are finalized that make that transition irrelevant or activate it,
// we will proceed onwards. most of the time there will be no pending transition.
// the limit, if any, is guaranteed to be higher than or equal to the given base number.
let limit = self.authority_set.current_limit(*base_header.number());
debug!(target: "afg", "Finding best chain containing block {:?} with number limit {:?}", block, limit);
match self.select_chain.finality_target(block, None) {
Ok(Some(best_hash)) => {
let best_header = self
.client
.header(BlockId::Hash(best_hash))
.ok()?
.expect("Header known to exist after `finality_target` call; qed");
// check if our vote is currently being limited due to a pending change
let limit = limit.filter(|limit| limit < best_header.number());
if let Some(target_number) = limit {
let mut target_header = best_header.clone();
// walk backwards until we find the target block
loop {
if *target_header.number() < target_number {
unreachable!(
"we are traversing backwards from a known block; \
blocks are stored contiguously; \
qed"
);
}
if *target_header.number() == target_number {
break;
}
target_header = self
.client
.header(BlockId::Hash(*target_header.parent_hash()))
.ok()?
.expect("Header known to exist after `finality_target` call; qed");
}
Some((base_header, best_header, target_header))
} else {
// otherwise just use the given best as the target
Some((base_header, best_header.clone(), best_header))
}
}
Ok(None) => {
debug!(target: "afg", "Encountered error finding best chain containing {:?}: couldn't find target block", block);
None
}
Err(e) => {
debug!(target: "afg", "Encountered error finding best chain containing {:?}: {:?}", block, e);
None
}
}
};
if let Some((base_header, best_header, target_header)) = find_best_chain() {
// restrict vote according to the given voting rule, if the
// voting rule doesn't restrict the vote then we keep the
// previous target.
//
// note that we pass the original `best_header`, i.e. before the
// authority set limit filter, which can be considered a
// mandatory/implicit voting rule.
//
// we also make sure that the restricted vote is higher than the
// round base (i.e. last finalized), otherwise the value
// returned by the given voting rule is ignored and the original
// target is used instead.
let rule_fut = self.voting_rule.restrict_vote(
self.client.clone(),
&base_header,
&best_header,
&target_header,
);
Box::pin(async move {
Ok(rule_fut
.await
.filter(|(_, restricted_number)| {
// we can only restrict votes within the interval [base, target]
restricted_number >= base_header.number()
&& restricted_number < target_header.number()
})
.or_else(|| Some((target_header.hash(), *target_header.number()))))
})
} else {
Box::pin(future::ok(None))
}
}
fn round_data(
&self,
round: RoundNumber,
@@ -217,8 +217,4 @@ impl<Block: BlockT> finality_grandpa::Chain<Block::Hash, NumberFor<Block>> for A
Ok(route)
}
fn best_chain_containing(&self, _block: Block::Hash) -> Option<(Block::Hash, NumberFor<Block>)> {
None
}
}
+2 -1
View File
@@ -127,7 +127,8 @@ pub use notification::{GrandpaJustificationSender, GrandpaJustificationStream};
pub use import::GrandpaBlockImport;
pub use justification::GrandpaJustification;
pub use voting_rule::{
BeforeBestBlockBy, ThreeQuartersOfTheUnfinalizedChain, VotingRule, VotingRulesBuilder
BeforeBestBlockBy, ThreeQuartersOfTheUnfinalizedChain, VotingRule, VotingRuleResult,
VotingRulesBuilder,
};
pub use finality_grandpa::voter::report;
pub use finality_proof::{prove_warp_sync, WarpSyncFragmentCache};
@@ -57,11 +57,6 @@ impl<'a, Block, Client> finality_grandpa::Chain<Block::Hash, NumberFor<Block>>
fn ancestry(&self, base: Block::Hash, block: Block::Hash) -> Result<Vec<Block::Hash>, GrandpaError> {
environment::ancestry(&self.client, base, block)
}
fn best_chain_containing(&self, _block: Block::Hash) -> Option<(Block::Hash, NumberFor<Block>)> {
// only used by voter
None
}
}
fn grandpa_observer<BE, Block: BlockT, Client, S, F>(
+13 -13
View File
@@ -1355,7 +1355,7 @@ where
#[test]
fn grandpa_environment_respects_voting_rules() {
use finality_grandpa::Chain;
use finality_grandpa::voter::Environment;
let peers = &[Ed25519Keyring::Alice];
let voters = make_ids(peers);
@@ -1390,25 +1390,25 @@ fn grandpa_environment_respects_voting_rules() {
// the unrestricted environment should just return the best block
assert_eq!(
unrestricted_env.best_chain_containing(
futures::executor::block_on(unrestricted_env.best_chain_containing(
peer.client().info().finalized_hash
).unwrap().1,
)).unwrap().unwrap().1,
21,
);
// both the other environments should return block 16, which is 3/4 of the
// way in the unfinalized chain
assert_eq!(
three_quarters_env.best_chain_containing(
futures::executor::block_on(three_quarters_env.best_chain_containing(
peer.client().info().finalized_hash
).unwrap().1,
)).unwrap().unwrap().1,
16,
);
assert_eq!(
default_env.best_chain_containing(
futures::executor::block_on(default_env.best_chain_containing(
peer.client().info().finalized_hash
).unwrap().1,
)).unwrap().unwrap().1,
16,
);
@@ -1417,18 +1417,18 @@ fn grandpa_environment_respects_voting_rules() {
// the 3/4 environment should propose block 21 for voting
assert_eq!(
three_quarters_env.best_chain_containing(
futures::executor::block_on(three_quarters_env.best_chain_containing(
peer.client().info().finalized_hash
).unwrap().1,
)).unwrap().unwrap().1,
21,
);
// while the default environment will always still make sure we don't vote
// on the best block (2 behind)
assert_eq!(
default_env.best_chain_containing(
futures::executor::block_on(default_env.best_chain_containing(
peer.client().info().finalized_hash
).unwrap().1,
)).unwrap().unwrap().1,
19,
);
@@ -1439,9 +1439,9 @@ fn grandpa_environment_respects_voting_rules() {
// best block, there's a hard rule that we can't cast any votes lower than
// the given base (#21).
assert_eq!(
default_env.best_chain_containing(
futures::executor::block_on(default_env.best_chain_containing(
peer.client().info().finalized_hash
).unwrap().1,
)).unwrap().unwrap().1,
21,
);
}
@@ -22,14 +22,22 @@
//! restrictions that are taken into account by the GRANDPA environment when
//! selecting a finality target to vote on.
use std::future::Future;
use std::sync::Arc;
use std::pin::Pin;
use dyn_clone::DynClone;
use sc_client_api::blockchain::HeaderBackend;
use sp_runtime::generic::BlockId;
use sp_runtime::traits::{Block as BlockT, Header, NumberFor, One, Zero};
/// A future returned by a `VotingRule` to restrict a given vote, if any restriction is necessary.
pub type VotingRuleResult<Block> =
Pin<Box<dyn Future<Output = Option<(<Block as BlockT>::Hash, NumberFor<Block>)>> + Send + Sync>>;
/// A trait for custom voting rules in GRANDPA.
pub trait VotingRule<Block, B>: Send + Sync where
pub trait VotingRule<Block, B>: DynClone + Send + Sync where
Block: BlockT,
B: HeaderBackend<Block>,
{
@@ -47,11 +55,11 @@ pub trait VotingRule<Block, B>: Send + Sync where
/// execution of voting rules wherein `current_target <= best_target`.
fn restrict_vote(
&self,
backend: &B,
backend: Arc<B>,
base: &Block::Header,
best_target: &Block::Header,
current_target: &Block::Header,
) -> Option<(Block::Hash, NumberFor<Block>)>;
) -> VotingRuleResult<Block>;
}
impl<Block, B> VotingRule<Block, B> for () where
@@ -60,12 +68,12 @@ impl<Block, B> VotingRule<Block, B> for () where
{
fn restrict_vote(
&self,
_backend: &B,
_backend: Arc<B>,
_base: &Block::Header,
_best_target: &Block::Header,
_current_target: &Block::Header,
) -> Option<(Block::Hash, NumberFor<Block>)> {
None
) -> VotingRuleResult<Block> {
Box::pin(async { None })
}
}
@@ -80,15 +88,15 @@ impl<Block, B> VotingRule<Block, B> for BeforeBestBlockBy<NumberFor<Block>> wher
{
fn restrict_vote(
&self,
backend: &B,
backend: Arc<B>,
_base: &Block::Header,
best_target: &Block::Header,
current_target: &Block::Header,
) -> Option<(Block::Hash, NumberFor<Block>)> {
) -> VotingRuleResult<Block> {
use sp_arithmetic::traits::Saturating;
if current_target.number().is_zero() {
return None;
return Box::pin(async { None });
}
// find the target number restricted by this rule
@@ -96,21 +104,24 @@ impl<Block, B> VotingRule<Block, B> for BeforeBestBlockBy<NumberFor<Block>> wher
// our current target is already lower than this rule would restrict
if target_number >= *current_target.number() {
return None;
return Box::pin(async { None });
}
let current_target = current_target.clone();
// find the block at the given target height
find_target(
backend,
target_number,
current_target,
)
Box::pin(std::future::ready(find_target(
&*backend,
target_number.clone(),
&current_target,
)))
}
}
/// A custom voting rule that limits votes towards 3/4 of the unfinalized chain,
/// using the given `base` and `best_target` to figure where the 3/4 target
/// should fall.
#[derive(Clone)]
pub struct ThreeQuartersOfTheUnfinalizedChain;
impl<Block, B> VotingRule<Block, B> for ThreeQuartersOfTheUnfinalizedChain where
@@ -119,11 +130,11 @@ impl<Block, B> VotingRule<Block, B> for ThreeQuartersOfTheUnfinalizedChain where
{
fn restrict_vote(
&self,
backend: &B,
backend: Arc<B>,
base: &Block::Header,
best_target: &Block::Header,
current_target: &Block::Header,
) -> Option<(Block::Hash, NumberFor<Block>)> {
) -> VotingRuleResult<Block> {
// target a vote towards 3/4 of the unfinalized chain (rounding up)
let target_number = {
let two = NumberFor::<Block>::one() + One::one();
@@ -138,15 +149,15 @@ impl<Block, B> VotingRule<Block, B> for ThreeQuartersOfTheUnfinalizedChain where
// our current target is already lower than this rule would restrict
if target_number >= *current_target.number() {
return None;
return Box::pin(async { None });
}
// find the block at the given target height
find_target(
backend,
Box::pin(std::future::ready(find_target(
&*backend,
target_number,
current_target,
)
)))
}
}
@@ -195,37 +206,42 @@ impl<B, Block> Clone for VotingRules<B, Block> {
impl<Block, B> VotingRule<Block, B> for VotingRules<Block, B> where
Block: BlockT,
B: HeaderBackend<Block>,
B: HeaderBackend<Block> + 'static,
{
fn restrict_vote(
&self,
backend: &B,
backend: Arc<B>,
base: &Block::Header,
best_target: &Block::Header,
current_target: &Block::Header,
) -> Option<(Block::Hash, NumberFor<Block>)> {
let restricted_target = self.rules.iter().fold(
current_target.clone(),
|current_target, rule| {
rule.restrict_vote(
backend,
base,
best_target,
&current_target,
)
) -> VotingRuleResult<Block> {
let rules = self.rules.clone();
let base = base.clone();
let best_target = best_target.clone();
let current_target = current_target.clone();
Box::pin(async move {
let mut restricted_target = current_target.clone();
for rule in rules.iter() {
if let Some(header) = rule
.restrict_vote(backend.clone(), &base, &best_target, &restricted_target)
.await
.and_then(|(hash, _)| backend.header(BlockId::Hash(hash)).ok())
.and_then(std::convert::identity)
.unwrap_or(current_target)
},
);
{
restricted_target = header;
}
}
let restricted_hash = restricted_target.hash();
let restricted_hash = restricted_target.hash();
if restricted_hash != current_target.hash() {
Some((restricted_hash, *restricted_target.number()))
} else {
None
}
if restricted_hash != current_target.hash() {
Some((restricted_hash, *restricted_target.number()))
} else {
None
}
})
}
}
@@ -237,7 +253,7 @@ pub struct VotingRulesBuilder<Block, B> {
impl<Block, B> Default for VotingRulesBuilder<Block, B> where
Block: BlockT,
B: HeaderBackend<Block>,
B: HeaderBackend<Block> + 'static,
{
fn default() -> Self {
VotingRulesBuilder::new()
@@ -248,7 +264,7 @@ impl<Block, B> Default for VotingRulesBuilder<Block, B> where
impl<Block, B> VotingRulesBuilder<Block, B> where
Block: BlockT,
B: HeaderBackend<Block>,
B: HeaderBackend<Block> + 'static,
{
/// Return a new voting rule builder using the given backend.
pub fn new() -> Self {
@@ -285,14 +301,15 @@ impl<Block, B> VotingRulesBuilder<Block, B> where
impl<Block, B> VotingRule<Block, B> for Box<dyn VotingRule<Block, B>> where
Block: BlockT,
B: HeaderBackend<Block>,
Self: Clone,
{
fn restrict_vote(
&self,
backend: &B,
backend: Arc<B>,
base: &Block::Header,
best_target: &Block::Header,
current_target: &Block::Header,
) -> Option<(Block::Hash, NumberFor<Block>)> {
) -> VotingRuleResult<Block> {
(**self).restrict_vote(backend, base, best_target, current_target)
}
}
+1 -1
View File
@@ -30,7 +30,7 @@ pallet-session = { version = "3.0.0", default-features = false, path = "../sessi
[dev-dependencies]
frame-benchmarking = { version = "3.0.0", path = "../benchmarking" }
grandpa = { package = "finality-grandpa", version = "0.13.0", features = ["derive-codec"] }
grandpa = { package = "finality-grandpa", version = "0.14.0", features = ["derive-codec"] }
sp-io = { version = "3.0.0", path = "../../primitives/io" }
sp-keyring = { version = "3.0.0", path = "../../primitives/keyring" }
pallet-balances = { version = "3.0.0", path = "../balances" }
@@ -16,7 +16,7 @@ targets = ["x86_64-unknown-linux-gnu"]
[dependencies]
codec = { package = "parity-scale-codec", version = "2.0.0", default-features = false, features = ["derive"] }
grandpa = { package = "finality-grandpa", version = "0.13.0", default-features = false, features = ["derive-codec"] }
grandpa = { package = "finality-grandpa", version = "0.14.0", default-features = false, features = ["derive-codec"] }
log = { version = "0.4.8", optional = true }
serde = { version = "1.0.101", optional = true, features = ["derive"] }
sp-api = { version = "3.0.0", default-features = false, path = "../api" }