optimize justification before submit (#1887)

* optimize justification before submit

* fmt

* spelling

* clippy

* fmt again

* aaand compilation

* clippy
This commit is contained in:
Svyatoslav Nikolsky
2023-02-20 11:57:54 +03:00
committed by Bastian Köcher
parent bb078b8226
commit 1d6e8a9a26
7 changed files with 290 additions and 8 deletions
+5
View File
@@ -1187,6 +1187,11 @@ mod tests {
bp_header_chain::storage_keys::pallet_operating_mode_key("Grandpa").0,
);
assert_eq!(
CurrentAuthoritySet::<TestRuntime>::storage_value_final_key().to_vec(),
bp_header_chain::storage_keys::current_authority_set_key("Grandpa").0,
);
assert_eq!(
BestFinalized::<TestRuntime>::storage_value_final_key().to_vec(),
bp_header_chain::storage_keys::best_finalized_key("Grandpa").0,
@@ -71,6 +71,14 @@ pub enum Error {
ExtraHeadersInVotesAncestries,
}
/// Given GRANDPA authorities set size, return number of valid authorities votes that the
/// justification must have to be valid.
///
/// This function assumes that all authorities have the same vote weight.
pub fn required_justification_precommits(authorities_set_length: u32) -> u32 {
authorities_set_length - authorities_set_length.saturating_sub(1) / 3
}
/// Decode justification target.
pub fn decode_justification_target<Header: HeaderT>(
raw_justification: &[u8],
@@ -80,6 +88,27 @@ pub fn decode_justification_target<Header: HeaderT>(
.map_err(|_| Error::JustificationDecode)
}
/// Verify and optimize given justification by removing unknown and duplicate votes.
pub fn optimize_justification<Header: HeaderT>(
finalized_target: (Header::Hash, Header::Number),
authorities_set_id: SetId,
authorities_set: &VoterSet<AuthorityId>,
justification: GrandpaJustification<Header>,
) -> Result<GrandpaJustification<Header>, Error>
where
Header::Number: finality_grandpa::BlockNumberOps,
{
let mut optimizer = OptimizationCallbacks(Vec::new());
verify_justification_with_callbacks(
finalized_target,
authorities_set_id,
authorities_set,
&justification,
&mut optimizer,
)?;
Ok(optimizer.optimize(justification))
}
/// Verify that justification, that is generated by given authority set, finalizes given header.
pub fn verify_justification<Header: HeaderT>(
finalized_target: (Header::Hash, Header::Number),
@@ -87,6 +116,83 @@ pub fn verify_justification<Header: HeaderT>(
authorities_set: &VoterSet<AuthorityId>,
justification: &GrandpaJustification<Header>,
) -> Result<(), Error>
where
Header::Number: finality_grandpa::BlockNumberOps,
{
verify_justification_with_callbacks(
finalized_target,
authorities_set_id,
authorities_set,
justification,
&mut (),
)
}
/// Verification callbacks.
trait VerificationCallbacks {
/// Called when we see a precommit from unknown authority.
fn on_unkown_authority(&mut self, precommit_idx: usize) -> Result<(), Error>;
/// Called when we see a precommit with duplicate vote from known authority.
fn on_duplicate_authority_vote(&mut self, precommit_idx: usize) -> Result<(), Error>;
/// Called when we see a precommit after we've collected enough votes from authorities.
fn on_redundant_authority_vote(&mut self, precommit_idx: usize) -> Result<(), Error>;
}
/// Verification callbacks for justification optimization.
struct OptimizationCallbacks(Vec<usize>);
impl OptimizationCallbacks {
fn optimize<Header: HeaderT>(
self,
mut justification: GrandpaJustification<Header>,
) -> GrandpaJustification<Header> {
for invalid_precommit_idx in self.0.into_iter().rev() {
justification.commit.precommits.remove(invalid_precommit_idx);
}
justification
}
}
impl VerificationCallbacks for OptimizationCallbacks {
fn on_unkown_authority(&mut self, precommit_idx: usize) -> Result<(), Error> {
self.0.push(precommit_idx);
Ok(())
}
fn on_duplicate_authority_vote(&mut self, precommit_idx: usize) -> Result<(), Error> {
self.0.push(precommit_idx);
Ok(())
}
fn on_redundant_authority_vote(&mut self, precommit_idx: usize) -> Result<(), Error> {
self.0.push(precommit_idx);
Ok(())
}
}
// this implementation will be removed in https://github.com/paritytech/parity-bridges-common/pull/1882
impl VerificationCallbacks for () {
fn on_unkown_authority(&mut self, _precommit_idx: usize) -> Result<(), Error> {
Ok(())
}
fn on_duplicate_authority_vote(&mut self, _precommit_idx: usize) -> Result<(), Error> {
Ok(())
}
fn on_redundant_authority_vote(&mut self, _precommit_idx: usize) -> Result<(), Error> {
Ok(())
}
}
/// Verify that justification, that is generated by given authority set, finalizes given header.
fn verify_justification_with_callbacks<Header: HeaderT, C: VerificationCallbacks>(
finalized_target: (Header::Hash, Header::Number),
authorities_set_id: SetId,
authorities_set: &VoterSet<AuthorityId>,
justification: &GrandpaJustification<Header>,
callbacks: &mut C,
) -> Result<(), Error>
where
Header::Number: finality_grandpa::BlockNumberOps,
{
@@ -95,17 +201,23 @@ where
return Err(Error::InvalidJustificationTarget)
}
let threshold = authorities_set.threshold().0.into();
let mut chain = AncestryChain::new(&justification.votes_ancestries);
let mut signature_buffer = Vec::new();
let mut votes = BTreeSet::new();
let mut cumulative_weight = 0u64;
for signed in &justification.commit.precommits {
for (precommit_idx, signed) in justification.commit.precommits.iter().enumerate() {
// if we have collected enough precommits, we probabably want to fail/remove extra
// precommits
if cumulative_weight > threshold {
callbacks.on_redundant_authority_vote(precommit_idx)?;
}
// authority must be in the set
let authority_info = match authorities_set.get(&signed.id) {
Some(authority_info) => authority_info,
None => {
// just ignore precommit from unknown authority as
// `finality_grandpa::import_precommit` does
callbacks.on_unkown_authority(precommit_idx)?;
continue
},
};
@@ -116,6 +228,7 @@ where
// `finality-grandpa` crate (mostly related to reporting equivocations). But the only thing
// that we care about is that only first vote from the authority is accepted
if !votes.insert(signed.id.clone()) {
callbacks.on_duplicate_authority_vote(precommit_idx)?;
continue
}
@@ -142,6 +255,7 @@ where
thus we'll never overflow the u64::MAX;\
qed",
);
// verify authority signature
if !sp_finality_grandpa::check_message_signature_with_buffer(
&finality_grandpa::Message::Precommit(signed.precommit.clone()),
@@ -162,7 +276,6 @@ where
// check that the cumulative weight of validators voted for the justification target (or one
// of its descendents) is larger than required threshold.
let threshold = authorities_set.threshold().0.into();
if cumulative_weight >= threshold {
Ok(())
} else {
@@ -20,6 +20,8 @@
pub const PALLET_OPERATING_MODE_VALUE_NAME: &str = "PalletOperatingMode";
/// Name of the `BestFinalized` storage value.
pub const BEST_FINALIZED_VALUE_NAME: &str = "BestFinalized";
/// Name of the `CurrentAuthoritySet` storage value.
pub const CURRENT_AUTHORITY_SET_VALUE_NAME: &str = "CurrentAuthoritySet";
use sp_core::storage::StorageKey;
@@ -34,6 +36,17 @@ pub fn pallet_operating_mode_key(pallet_prefix: &str) -> StorageKey {
)
}
/// Storage key of the `CurrentAuthoritySet` variable in the runtime storage.
pub fn current_authority_set_key(pallet_prefix: &str) -> StorageKey {
StorageKey(
bp_runtime::storage_value_final_key(
pallet_prefix.as_bytes(),
CURRENT_AUTHORITY_SET_VALUE_NAME.as_bytes(),
)
.to_vec(),
)
}
/// Storage key of the best finalized header number and hash value in the runtime storage.
pub fn best_finalized_key(pallet_prefix: &str) -> StorageKey {
StorageKey(
@@ -63,6 +76,19 @@ mod tests {
);
}
#[test]
fn current_authority_set_key_computed_properly() {
// If this test fails, then something has been changed in module storage that is breaking
// compatibility with previous pallet.
let storage_key = current_authority_set_key("BridgeGrandpa").0;
assert_eq!(
storage_key,
hex!("0b06f475eddb98cf933a12262e0388de24a7b8b5717ea33346fa595a66ccbcb0").to_vec(),
"Unexpected storage key: {}",
hex::encode(&storage_key),
);
}
#[test]
fn best_finalized_key_computed_properly() {
// If this test fails, then something has been changed in module storage that is breaking
@@ -16,7 +16,7 @@
//! Tests for Grandpa Justification code.
use bp_header_chain::justification::{verify_justification, Error};
use bp_header_chain::justification::{optimize_justification, verify_justification, Error};
use bp_test_utils::*;
type TestHeader = sp_runtime::testing::Header;
@@ -190,3 +190,87 @@ fn justification_is_invalid_if_we_dont_meet_threshold() {
Err(Error::TooLowCumulativeWeight),
);
}
#[test]
fn optimizer_does_noting_with_minimal_justification() {
let justification = make_default_justification::<TestHeader>(&test_header(1));
let num_precommits_before = justification.commit.precommits.len();
let justification = optimize_justification::<TestHeader>(
header_id::<TestHeader>(1),
TEST_GRANDPA_SET_ID,
&voter_set(),
justification,
)
.unwrap();
let num_precommits_after = justification.commit.precommits.len();
assert_eq!(num_precommits_before, num_precommits_after);
}
#[test]
fn unknown_authority_votes_are_removed_by_optimizer() {
let mut justification = make_default_justification::<TestHeader>(&test_header(1));
justification.commit.precommits.push(signed_precommit::<TestHeader>(
&bp_test_utils::Account(42),
header_id::<TestHeader>(1),
justification.round,
TEST_GRANDPA_SET_ID,
));
let num_precommits_before = justification.commit.precommits.len();
let justification = optimize_justification::<TestHeader>(
header_id::<TestHeader>(1),
TEST_GRANDPA_SET_ID,
&voter_set(),
justification,
)
.unwrap();
let num_precommits_after = justification.commit.precommits.len();
assert_eq!(num_precommits_before - 1, num_precommits_after);
}
#[test]
fn duplicate_authority_votes_are_removed_by_optimizer() {
let mut justification = make_default_justification::<TestHeader>(&test_header(1));
justification
.commit
.precommits
.push(justification.commit.precommits.first().cloned().unwrap());
let num_precommits_before = justification.commit.precommits.len();
let justification = optimize_justification::<TestHeader>(
header_id::<TestHeader>(1),
TEST_GRANDPA_SET_ID,
&voter_set(),
justification,
)
.unwrap();
let num_precommits_after = justification.commit.precommits.len();
assert_eq!(num_precommits_before - 1, num_precommits_after);
}
#[test]
fn redundant_authority_votes_are_removed_by_optimizer() {
let mut justification = make_default_justification::<TestHeader>(&test_header(1));
justification.commit.precommits.push(signed_precommit::<TestHeader>(
&EVE,
header_id::<TestHeader>(1),
justification.round,
TEST_GRANDPA_SET_ID,
));
let num_precommits_before = justification.commit.precommits.len();
let justification = optimize_justification::<TestHeader>(
header_id::<TestHeader>(1),
TEST_GRANDPA_SET_ID,
&voter_set(),
justification,
)
.unwrap();
let num_precommits_after = justification.commit.precommits.len();
assert_eq!(num_precommits_before - 1, num_precommits_after);
}
+3 -2
View File
@@ -18,7 +18,7 @@
#![cfg_attr(not(feature = "std"), no_std)]
use bp_header_chain::justification::GrandpaJustification;
use bp_header_chain::justification::{required_justification_precommits, GrandpaJustification};
use codec::Encode;
use sp_finality_grandpa::{AuthorityId, AuthoritySignature, AuthorityWeight, SetId};
use sp_runtime::traits::{Header as HeaderT, One, Zero};
@@ -57,11 +57,12 @@ pub struct JustificationGeneratorParams<H> {
impl<H: HeaderT> Default for JustificationGeneratorParams<H> {
fn default() -> Self {
let required_signatures = required_justification_precommits(test_keyring().len() as _);
Self {
header: test_header(One::one()),
round: TEST_GRANDPA_ROUND,
set_id: TEST_GRANDPA_SET_ID,
authorities: test_keyring(),
authorities: test_keyring().into_iter().take(required_signatures as _).collect(),
ancestors: 2,
forks: 1,
}
@@ -22,7 +22,7 @@ use bp_header_chain::{
justification::{verify_justification, GrandpaJustification},
ConsensusLogReader, FinalityProof, GrandpaConsensusLogReader,
};
use bp_runtime::{BasicOperatingMode, OperatingMode};
use bp_runtime::{BasicOperatingMode, HeaderIdProvider, OperatingMode};
use codec::{Decode, Encode};
use finality_grandpa::voter_set::VoterSet;
use num_traits::{One, Zero};
@@ -87,6 +87,13 @@ pub trait Engine<C: Chain>: Send {
client.subscribe_finality_justifications::<Self::FinalityClient>().await
}
/// Optimize finality proof before sending it to the target node.
async fn optimize_proof<TargetChain: Chain>(
target_client: &Client<TargetChain>,
header: &C::Header,
proof: Self::FinalityProof,
) -> Result<Self::FinalityProof, SubstrateError>;
/// Prepare initialization data for the finality bridge pallet.
async fn prepare_initialization_data(
client: Client<C>,
@@ -139,6 +146,48 @@ impl<C: ChainWithGrandpa> Engine<C> for Grandpa<C> {
bp_header_chain::storage_keys::pallet_operating_mode_key(C::WITH_CHAIN_GRANDPA_PALLET_NAME)
}
async fn optimize_proof<TargetChain: Chain>(
target_client: &Client<TargetChain>,
header: &C::Header,
proof: Self::FinalityProof,
) -> Result<Self::FinalityProof, SubstrateError> {
let current_authority_set_key = bp_header_chain::storage_keys::current_authority_set_key(
C::WITH_CHAIN_GRANDPA_PALLET_NAME,
);
let (authority_set, authority_set_id): (
sp_finality_grandpa::AuthorityList,
sp_finality_grandpa::SetId,
) = target_client
.storage_value(current_authority_set_key, None)
.await?
.map(Ok)
.unwrap_or(Err(SubstrateError::Custom(format!(
"{} `CurrentAuthoritySet` is missing from the {} storage",
C::NAME,
TargetChain::NAME,
))))?;
let authority_set =
finality_grandpa::voter_set::VoterSet::new(authority_set).expect("TODO");
// we're risking with race here - we have decided to submit justification some time ago and
// actual authorities set (which we have read now) may have changed, so this
// `optimize_justification` may fail. But if target chain is configured properly, it'll fail
// anyway, after we submit transaction and failing earlier is better. So - it is fine
bp_header_chain::justification::optimize_justification(
(header.hash(), *header.number()),
authority_set_id,
&authority_set,
proof,
)
.map_err(|e| {
SubstrateError::Custom(format!(
"Failed to optimize {} GRANDPA jutification for header {:?}: {:?}",
C::NAME,
header.id(),
e,
))
})
}
/// Prepare initialization data for the GRANDPA verifier pallet.
async fn prepare_initialization_data(
source_client: Client<C>,
@@ -111,6 +111,10 @@ where
header: SyncHeader<HeaderOf<P::SourceChain>>,
proof: SubstrateFinalityProof<P>,
) -> Result<Self::TransactionTracker, Error> {
// runtime module at target chain may require optimized finality proof
let proof = P::FinalityEngine::optimize_proof(&self.client, &header, proof).await?;
// now we may submit optimized finality proof
let transaction_params = self.transaction_params.clone();
let call =
P::SubmitFinalityProofCallBuilder::build_submit_finality_proof_call(header, proof);