Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bridge: check submit_finality_proof limits before submission #4549

Merged
merged 1 commit into from
May 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 15 additions & 41 deletions bridges/modules/grandpa/src/call_ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,8 @@ use crate::{
weights::WeightInfo, BestFinalized, BridgedBlockNumber, BridgedHeader, Config,
CurrentAuthoritySet, Error, FreeHeadersRemaining, Pallet,
};
use bp_header_chain::{
justification::GrandpaJustification, max_expected_submit_finality_proof_arguments_size,
ChainWithGrandpa, GrandpaConsensusLogReader,
};
use bp_header_chain::{justification::GrandpaJustification, submit_finality_proof_limits_extras};
use bp_runtime::{BlockNumberOf, Chain, OwnedBridgeModule};
use codec::Encode;
use frame_support::{
dispatch::CallableCallFor,
traits::{Get, IsSubType},
Expand Down Expand Up @@ -303,53 +299,31 @@ pub(crate) fn submit_finality_proof_info_from_args<T: Config<I>, I: 'static>(
current_set_id: Option<SetId>,
is_free_execution_expected: bool,
) -> SubmitFinalityProofInfo<BridgedBlockNumber<T, I>> {
let block_number = *finality_target.number();

// the `submit_finality_proof` call will reject justifications with invalid, duplicate,
// unknown and extra signatures. It'll also reject justifications with less than necessary
// signatures. So we do not care about extra weight because of additional signatures here.
let precommits_len = justification.commit.precommits.len().saturated_into();
let required_precommits = precommits_len;
// check if call exceeds limits. In other words - whether some size or weight is included
// in the call
let extras =
submit_finality_proof_limits_extras::<T::BridgedChain>(finality_target, justification);

// We do care about extra weight because of more-than-expected headers in the votes
// ancestries. But we have problems computing extra weight for additional headers (weight of
// additional header is too small, so that our benchmarks aren't detecting that). So if there
// are more than expected headers in votes ancestries, we will treat the whole call weight
// as an extra weight.
let votes_ancestries_len = justification.votes_ancestries.len().saturated_into();
let extra_weight =
if votes_ancestries_len > T::BridgedChain::REASONABLE_HEADERS_IN_JUSTIFICATION_ANCESTRY {
T::WeightInfo::submit_finality_proof(precommits_len, votes_ancestries_len)
} else {
Weight::zero()
};

// check if the `finality_target` is a mandatory header. If so, we are ready to refund larger
// size
let is_mandatory_finality_target =
GrandpaConsensusLogReader::<BridgedBlockNumber<T, I>>::find_scheduled_change(
finality_target.digest(),
)
.is_some();

// we can estimate extra call size easily, without any additional significant overhead
let actual_call_size: u32 = finality_target
.encoded_size()
.saturating_add(justification.encoded_size())
.saturated_into();
let max_expected_call_size = max_expected_submit_finality_proof_arguments_size::<T::BridgedChain>(
is_mandatory_finality_target,
required_precommits,
);
let extra_size = actual_call_size.saturating_sub(max_expected_call_size);
let extra_weight = if extras.is_weight_limit_exceeded {
let precommits_len = justification.commit.precommits.len().saturated_into();
let votes_ancestries_len = justification.votes_ancestries.len().saturated_into();
T::WeightInfo::submit_finality_proof(precommits_len, votes_ancestries_len)
} else {
Weight::zero()
};

SubmitFinalityProofInfo {
block_number,
block_number: *finality_target.number(),
current_set_id,
is_mandatory: is_mandatory_finality_target,
is_mandatory: extras.is_mandatory_finality_target,
is_free_execution_expected,
extra_weight,
extra_size,
extra_size: extras.extra_size,
}
}

Expand Down
68 changes: 65 additions & 3 deletions bridges/primitives/header-chain/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ use crate::justification::{
GrandpaJustification, JustificationVerificationContext, JustificationVerificationError,
};
use bp_runtime::{
BasicOperatingMode, Chain, HashOf, HasherOf, HeaderOf, RawStorageProof, StorageProofChecker,
StorageProofError, UnderlyingChainProvider,
BasicOperatingMode, BlockNumberOf, Chain, HashOf, HasherOf, HeaderOf, RawStorageProof,
StorageProofChecker, StorageProofError, UnderlyingChainProvider,
};
use codec::{Codec, Decode, Encode, EncodeLike, MaxEncodedLen};
use core::{clone::Clone, cmp::Eq, default::Default, fmt::Debug};
Expand All @@ -35,7 +35,7 @@ use serde::{Deserialize, Serialize};
use sp_consensus_grandpa::{
AuthorityList, ConsensusLog, ScheduledChange, SetId, GRANDPA_ENGINE_ID,
};
use sp_runtime::{traits::Header as HeaderT, Digest, RuntimeDebug};
use sp_runtime::{traits::Header as HeaderT, Digest, RuntimeDebug, SaturatedConversion};
use sp_std::{boxed::Box, vec::Vec};

pub mod justification;
Expand Down Expand Up @@ -325,6 +325,68 @@ where
const AVERAGE_HEADER_SIZE: u32 = <T::Chain as ChainWithGrandpa>::AVERAGE_HEADER_SIZE;
}

/// Result of checking maximal expected submit finality proof call weight and size.
#[derive(Debug)]
pub struct SubmitFinalityProofCallExtras {
/// If true, the call weight is larger than what we have assumed.
///
/// We have some assumptions about headers and justifications of the bridged chain.
/// We know that if our assumptions are correct, then the call must not have the
/// weight above some limit. The fee paid for weight above that limit, is never refunded.
pub is_weight_limit_exceeded: bool,
/// Extra size (in bytes) that we assume are included in the call.
///
/// We have some assumptions about headers and justifications of the bridged chain.
/// We know that if our assumptions are correct, then the call must not have the
/// weight above some limit. The fee paid for bytes above that limit, is never refunded.
pub extra_size: u32,
/// A flag that is true if the header is the mandatory header that enacts new
/// authorities set.
pub is_mandatory_finality_target: bool,
}

/// Checks whether the given `header` and its finality `proof` fit the maximal expected
/// call limits (size and weight). The submission may be refunded sometimes (see pallet
/// configuration for details), but it should fit some limits. If the call has some extra
/// weight and/or size included, though, we won't refund it or refund will be partial.
pub fn submit_finality_proof_limits_extras<C: ChainWithGrandpa>(
header: &C::Header,
proof: &justification::GrandpaJustification<C::Header>,
) -> SubmitFinalityProofCallExtras {
// the `submit_finality_proof` call will reject justifications with invalid, duplicate,
// unknown and extra signatures. It'll also reject justifications with less than necessary
// signatures. So we do not care about extra weight because of additional signatures here.
let precommits_len = proof.commit.precommits.len().saturated_into();
let required_precommits = precommits_len;

// the weight check is simple - we assume that there are no more than the `limit`
// headers in the ancestry proof
let votes_ancestries_len: u32 = proof.votes_ancestries.len().saturated_into();
let is_weight_limit_exceeded =
votes_ancestries_len > C::REASONABLE_HEADERS_IN_JUSTIFICATION_ANCESTRY;

// check if the `finality_target` is a mandatory header. If so, we are ready to refund larger
// size
let is_mandatory_finality_target =
GrandpaConsensusLogReader::<BlockNumberOf<C>>::find_scheduled_change(header.digest())
.is_some();

// we can estimate extra call size easily, without any additional significant overhead
let actual_call_size: u32 =
header.encoded_size().saturating_add(proof.encoded_size()).saturated_into();
let max_expected_call_size = max_expected_submit_finality_proof_arguments_size::<C>(
is_mandatory_finality_target,
required_precommits,
);
let extra_size = actual_call_size.saturating_sub(max_expected_call_size);

SubmitFinalityProofCallExtras {
is_weight_limit_exceeded,
extra_size,
is_mandatory_finality_target,
}
}

/// Returns maximal expected size of `submit_finality_proof` call arguments.
pub fn max_expected_submit_finality_proof_arguments_size<C: ChainWithGrandpa>(
is_mandatory_finality_target: bool,
Expand Down
7 changes: 7 additions & 0 deletions bridges/relays/client-substrate/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
//! Substrate node RPC errors.

use crate::SimpleRuntimeVersion;
use bp_header_chain::SubmitFinalityProofCallExtras;
use bp_polkadot_core::parachains::ParaId;
use jsonrpsee::core::ClientError as RpcError;
use relay_utils::MaybeConnectionError;
Expand Down Expand Up @@ -129,6 +130,12 @@ pub enum Error {
/// Actual runtime version.
actual: SimpleRuntimeVersion,
},
/// Finality proof submission exceeds size and/or weight limits.
#[error("Finality proof submission exceeds limits: {extras:?}")]
FinalityProofWeightLimitExceeded {
/// Finality proof submission extras.
extras: SubmitFinalityProofCallExtras,
},
/// Custom logic error.
#[error("{0}")]
Custom(String),
Expand Down
10 changes: 10 additions & 0 deletions bridges/relays/lib-substrate-relay/src/finality/target.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,16 @@ impl<P: SubstrateFinalitySyncPipeline> TargetClient<FinalitySyncPipelineAdapter<
let context =
P::FinalityEngine::verify_and_optimize_proof(&self.client, &header, &mut proof).await?;

// if free execution is expected, but the call size/weight exceeds hardcoded limits, the
// runtime may still accept the proof, but it may have some cost for relayer. Let's check
// it here to avoid losing relayer funds
if is_free_execution_expected {
let extras = P::FinalityEngine::check_max_expected_call_limits(&header, &proof);
if extras.is_weight_limit_exceeded || extras.extra_size != 0 {
return Err(Error::FinalityProofWeightLimitExceeded { extras })
}
}

// now we may submit optimized finality proof
let mortality = self.transaction_params.mortality;
let call = P::SubmitFinalityProofCallBuilder::build_submit_finality_proof_call(
Expand Down
44 changes: 9 additions & 35 deletions bridges/relays/lib-substrate-relay/src/finality_base/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,8 @@ use bp_header_chain::{
verify_and_optimize_justification, GrandpaEquivocationsFinder, GrandpaJustification,
JustificationVerificationContext,
},
max_expected_submit_finality_proof_arguments_size, AuthoritySet, ConsensusLogReader,
FinalityProof, FindEquivocations, GrandpaConsensusLogReader, HeaderFinalityInfo,
HeaderGrandpaInfo, StoredHeaderGrandpaInfo,
AuthoritySet, ConsensusLogReader, FinalityProof, FindEquivocations, GrandpaConsensusLogReader,
HeaderFinalityInfo, HeaderGrandpaInfo, StoredHeaderGrandpaInfo, SubmitFinalityProofCallExtras,
};
use bp_runtime::{BasicOperatingMode, HeaderIdProvider, OperatingMode};
use codec::{Decode, Encode};
Expand All @@ -36,22 +35,9 @@ use relay_substrate_client::{
};
use sp_consensus_grandpa::{AuthorityList as GrandpaAuthoritiesSet, GRANDPA_ENGINE_ID};
use sp_core::{storage::StorageKey, Bytes};
use sp_runtime::{scale_info::TypeInfo, traits::Header, ConsensusEngineId, SaturatedConversion};
use sp_runtime::{scale_info::TypeInfo, traits::Header, ConsensusEngineId};
use std::{fmt::Debug, marker::PhantomData};

/// Result of checking maximal expected call size.
pub enum MaxExpectedCallSizeCheck {
/// Size is ok and call will be refunded.
Ok,
/// The call size exceeds the maximal expected and relayer will only get partial refund.
Exceeds {
/// Actual call size.
call_size: u32,
/// Maximal expected call size.
max_call_size: u32,
},
}

/// Finality engine, used by the Substrate chain.
#[async_trait]
pub trait Engine<C: Chain>: Send {
Expand Down Expand Up @@ -129,12 +115,11 @@ pub trait Engine<C: Chain>: Send {
) -> Result<Self::FinalityVerificationContext, SubstrateError>;

/// Checks whether the given `header` and its finality `proof` fit the maximal expected
/// call size limit. If result is `MaxExpectedCallSizeCheck::Exceeds { .. }`, this
/// submission won't be fully refunded and relayer will spend its own funds on that.
fn check_max_expected_call_size(
/// call limits (size and weight).
fn check_max_expected_call_limits(
header: &C::Header,
proof: &Self::FinalityProof,
) -> MaxExpectedCallSizeCheck;
) -> SubmitFinalityProofCallExtras;

/// Prepare initialization data for the finality bridge pallet.
async fn prepare_initialization_data(
Expand Down Expand Up @@ -245,22 +230,11 @@ impl<C: ChainWithGrandpa> Engine<C> for Grandpa<C> {
})
}

fn check_max_expected_call_size(
fn check_max_expected_call_limits(
header: &C::Header,
proof: &Self::FinalityProof,
) -> MaxExpectedCallSizeCheck {
let is_mandatory = Self::ConsensusLogReader::schedules_authorities_change(header.digest());
let call_size: u32 =
header.encoded_size().saturating_add(proof.encoded_size()).saturated_into();
let max_call_size = max_expected_submit_finality_proof_arguments_size::<C>(
is_mandatory,
proof.commit.precommits.len().saturated_into(),
);
if call_size > max_call_size {
MaxExpectedCallSizeCheck::Exceeds { call_size, max_call_size }
} else {
MaxExpectedCallSizeCheck::Ok
}
) -> SubmitFinalityProofCallExtras {
bp_header_chain::submit_finality_proof_limits_extras::<C>(header, proof)
}

/// Prepare initialization data for the GRANDPA verifier pallet.
Expand Down
13 changes: 5 additions & 8 deletions bridges/relays/lib-substrate-relay/src/on_demand/headers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,7 @@

//! On-demand Substrate -> Substrate header finality relay.

use crate::{
finality::SubmitFinalityProofCallBuilder, finality_base::engine::MaxExpectedCallSizeCheck,
};
use crate::finality::SubmitFinalityProofCallBuilder;

use async_std::sync::{Arc, Mutex};
use async_trait::async_trait;
Expand Down Expand Up @@ -156,22 +154,21 @@ impl<P: SubstrateFinalitySyncPipeline> OnDemandRelay<P::SourceChain, P::TargetCh

// now we have the header and its proof, but we want to minimize our losses, so let's
// check if we'll get the full refund for submitting this header
let check_result = P::FinalityEngine::check_max_expected_call_size(&header, &proof);
if let MaxExpectedCallSizeCheck::Exceeds { call_size, max_call_size } = check_result {
let check_result = P::FinalityEngine::check_max_expected_call_limits(&header, &proof);
if check_result.is_weight_limit_exceeded || check_result.extra_size != 0 {
iterations += 1;
current_required_header = header_id.number().saturating_add(One::one());
if iterations < MAX_ITERATIONS {
log::debug!(
target: "bridge",
"[{}] Requested to prove {} head {:?}. Selected to prove {} head {:?}. But it is too large: {} vs {}. \
"[{}] Requested to prove {} head {:?}. Selected to prove {} head {:?}. But it exceeds limits: {:?}. \
Going to select next header",
self.relay_task_name,
P::SourceChain::NAME,
required_header,
P::SourceChain::NAME,
header_id,
call_size,
max_call_size,
check_result,
);

continue;
Expand Down
Loading