Skip to content

Commit 95ea631

Browse files
svyatonikTarekkMA
authored andcommitted
Bridge: check submit_finality_proof limits before submission (paritytech#4549)
closes paritytech/parity-bridges-common#2982 closes paritytech/parity-bridges-common#2730 The main change is in the bridges/relays/lib-substrate-relay/src/finality/target.rs, changes in other files are just moving the code ~I haven't been able to run zn tests locally - don't know why, but it keeps failing for me locally with: ` Error running script: /home/svyatonik/dev/polkadot-sdk/bridges/testing/framework/js-helpers/wait-hrmp-channel-opened.js Error: Timeout(300), "custom-js /home/svyatonik/dev/polkadot-sdk/bridges/testing/framework/js-helpers/wait-hrmp-channel-opened.js within 300 secs" didn't complete on time.`~ The issue was an obsolete `polkadot-js-api` binary - did `yarn global upgrade` and it is ok now
1 parent 4ddcbd4 commit 95ea631

File tree

6 files changed

+111
-87
lines changed

6 files changed

+111
-87
lines changed

bridges/modules/grandpa/src/call_ext.rs

Lines changed: 15 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,8 @@ use crate::{
1818
weights::WeightInfo, BestFinalized, BridgedBlockNumber, BridgedHeader, Config,
1919
CurrentAuthoritySet, Error, FreeHeadersRemaining, Pallet,
2020
};
21-
use bp_header_chain::{
22-
justification::GrandpaJustification, max_expected_submit_finality_proof_arguments_size,
23-
ChainWithGrandpa, GrandpaConsensusLogReader,
24-
};
21+
use bp_header_chain::{justification::GrandpaJustification, submit_finality_proof_limits_extras};
2522
use bp_runtime::{BlockNumberOf, Chain, OwnedBridgeModule};
26-
use codec::Encode;
2723
use frame_support::{
2824
dispatch::CallableCallFor,
2925
traits::{Get, IsSubType},
@@ -303,53 +299,31 @@ pub(crate) fn submit_finality_proof_info_from_args<T: Config<I>, I: 'static>(
303299
current_set_id: Option<SetId>,
304300
is_free_execution_expected: bool,
305301
) -> SubmitFinalityProofInfo<BridgedBlockNumber<T, I>> {
306-
let block_number = *finality_target.number();
307-
308-
// the `submit_finality_proof` call will reject justifications with invalid, duplicate,
309-
// unknown and extra signatures. It'll also reject justifications with less than necessary
310-
// signatures. So we do not care about extra weight because of additional signatures here.
311-
let precommits_len = justification.commit.precommits.len().saturated_into();
312-
let required_precommits = precommits_len;
302+
// check if call exceeds limits. In other words - whether some size or weight is included
303+
// in the call
304+
let extras =
305+
submit_finality_proof_limits_extras::<T::BridgedChain>(finality_target, justification);
313306

314307
// We do care about extra weight because of more-than-expected headers in the votes
315308
// ancestries. But we have problems computing extra weight for additional headers (weight of
316309
// additional header is too small, so that our benchmarks aren't detecting that). So if there
317310
// are more than expected headers in votes ancestries, we will treat the whole call weight
318311
// as an extra weight.
319-
let votes_ancestries_len = justification.votes_ancestries.len().saturated_into();
320-
let extra_weight =
321-
if votes_ancestries_len > T::BridgedChain::REASONABLE_HEADERS_IN_JUSTIFICATION_ANCESTRY {
322-
T::WeightInfo::submit_finality_proof(precommits_len, votes_ancestries_len)
323-
} else {
324-
Weight::zero()
325-
};
326-
327-
// check if the `finality_target` is a mandatory header. If so, we are ready to refund larger
328-
// size
329-
let is_mandatory_finality_target =
330-
GrandpaConsensusLogReader::<BridgedBlockNumber<T, I>>::find_scheduled_change(
331-
finality_target.digest(),
332-
)
333-
.is_some();
334-
335-
// we can estimate extra call size easily, without any additional significant overhead
336-
let actual_call_size: u32 = finality_target
337-
.encoded_size()
338-
.saturating_add(justification.encoded_size())
339-
.saturated_into();
340-
let max_expected_call_size = max_expected_submit_finality_proof_arguments_size::<T::BridgedChain>(
341-
is_mandatory_finality_target,
342-
required_precommits,
343-
);
344-
let extra_size = actual_call_size.saturating_sub(max_expected_call_size);
312+
let extra_weight = if extras.is_weight_limit_exceeded {
313+
let precommits_len = justification.commit.precommits.len().saturated_into();
314+
let votes_ancestries_len = justification.votes_ancestries.len().saturated_into();
315+
T::WeightInfo::submit_finality_proof(precommits_len, votes_ancestries_len)
316+
} else {
317+
Weight::zero()
318+
};
345319

346320
SubmitFinalityProofInfo {
347-
block_number,
321+
block_number: *finality_target.number(),
348322
current_set_id,
349-
is_mandatory: is_mandatory_finality_target,
323+
is_mandatory: extras.is_mandatory_finality_target,
350324
is_free_execution_expected,
351325
extra_weight,
352-
extra_size,
326+
extra_size: extras.extra_size,
353327
}
354328
}
355329

bridges/primitives/header-chain/src/lib.rs

Lines changed: 65 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ use crate::justification::{
2424
GrandpaJustification, JustificationVerificationContext, JustificationVerificationError,
2525
};
2626
use bp_runtime::{
27-
BasicOperatingMode, Chain, HashOf, HasherOf, HeaderOf, RawStorageProof, StorageProofChecker,
28-
StorageProofError, UnderlyingChainProvider,
27+
BasicOperatingMode, BlockNumberOf, Chain, HashOf, HasherOf, HeaderOf, RawStorageProof,
28+
StorageProofChecker, StorageProofError, UnderlyingChainProvider,
2929
};
3030
use codec::{Codec, Decode, Encode, EncodeLike, MaxEncodedLen};
3131
use core::{clone::Clone, cmp::Eq, default::Default, fmt::Debug};
@@ -35,7 +35,7 @@ use serde::{Deserialize, Serialize};
3535
use sp_consensus_grandpa::{
3636
AuthorityList, ConsensusLog, ScheduledChange, SetId, GRANDPA_ENGINE_ID,
3737
};
38-
use sp_runtime::{traits::Header as HeaderT, Digest, RuntimeDebug};
38+
use sp_runtime::{traits::Header as HeaderT, Digest, RuntimeDebug, SaturatedConversion};
3939
use sp_std::{boxed::Box, vec::Vec};
4040

4141
pub mod justification;
@@ -325,6 +325,68 @@ where
325325
const AVERAGE_HEADER_SIZE: u32 = <T::Chain as ChainWithGrandpa>::AVERAGE_HEADER_SIZE;
326326
}
327327

328+
/// Result of checking maximal expected submit finality proof call weight and size.
329+
#[derive(Debug)]
330+
pub struct SubmitFinalityProofCallExtras {
331+
/// If true, the call weight is larger than what we have assumed.
332+
///
333+
/// We have some assumptions about headers and justifications of the bridged chain.
334+
/// We know that if our assumptions are correct, then the call must not have the
335+
/// weight above some limit. The fee paid for weight above that limit, is never refunded.
336+
pub is_weight_limit_exceeded: bool,
337+
/// Extra size (in bytes) that we assume are included in the call.
338+
///
339+
/// We have some assumptions about headers and justifications of the bridged chain.
340+
/// We know that if our assumptions are correct, then the call must not have the
341+
/// weight above some limit. The fee paid for bytes above that limit, is never refunded.
342+
pub extra_size: u32,
343+
/// A flag that is true if the header is the mandatory header that enacts new
344+
/// authorities set.
345+
pub is_mandatory_finality_target: bool,
346+
}
347+
348+
/// Checks whether the given `header` and its finality `proof` fit the maximal expected
349+
/// call limits (size and weight). The submission may be refunded sometimes (see pallet
350+
/// configuration for details), but it should fit some limits. If the call has some extra
351+
/// weight and/or size included, though, we won't refund it or refund will be partial.
352+
pub fn submit_finality_proof_limits_extras<C: ChainWithGrandpa>(
353+
header: &C::Header,
354+
proof: &justification::GrandpaJustification<C::Header>,
355+
) -> SubmitFinalityProofCallExtras {
356+
// the `submit_finality_proof` call will reject justifications with invalid, duplicate,
357+
// unknown and extra signatures. It'll also reject justifications with less than necessary
358+
// signatures. So we do not care about extra weight because of additional signatures here.
359+
let precommits_len = proof.commit.precommits.len().saturated_into();
360+
let required_precommits = precommits_len;
361+
362+
// the weight check is simple - we assume that there are no more than the `limit`
363+
// headers in the ancestry proof
364+
let votes_ancestries_len: u32 = proof.votes_ancestries.len().saturated_into();
365+
let is_weight_limit_exceeded =
366+
votes_ancestries_len > C::REASONABLE_HEADERS_IN_JUSTIFICATION_ANCESTRY;
367+
368+
// check if the `finality_target` is a mandatory header. If so, we are ready to refund larger
369+
// size
370+
let is_mandatory_finality_target =
371+
GrandpaConsensusLogReader::<BlockNumberOf<C>>::find_scheduled_change(header.digest())
372+
.is_some();
373+
374+
// we can estimate extra call size easily, without any additional significant overhead
375+
let actual_call_size: u32 =
376+
header.encoded_size().saturating_add(proof.encoded_size()).saturated_into();
377+
let max_expected_call_size = max_expected_submit_finality_proof_arguments_size::<C>(
378+
is_mandatory_finality_target,
379+
required_precommits,
380+
);
381+
let extra_size = actual_call_size.saturating_sub(max_expected_call_size);
382+
383+
SubmitFinalityProofCallExtras {
384+
is_weight_limit_exceeded,
385+
extra_size,
386+
is_mandatory_finality_target,
387+
}
388+
}
389+
328390
/// Returns maximal expected size of `submit_finality_proof` call arguments.
329391
pub fn max_expected_submit_finality_proof_arguments_size<C: ChainWithGrandpa>(
330392
is_mandatory_finality_target: bool,

bridges/relays/client-substrate/src/error.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
//! Substrate node RPC errors.
1818
1919
use crate::SimpleRuntimeVersion;
20+
use bp_header_chain::SubmitFinalityProofCallExtras;
2021
use bp_polkadot_core::parachains::ParaId;
2122
use jsonrpsee::core::ClientError as RpcError;
2223
use relay_utils::MaybeConnectionError;
@@ -129,6 +130,12 @@ pub enum Error {
129130
/// Actual runtime version.
130131
actual: SimpleRuntimeVersion,
131132
},
133+
/// Finality proof submission exceeds size and/or weight limits.
134+
#[error("Finality proof submission exceeds limits: {extras:?}")]
135+
FinalityProofWeightLimitExceeded {
136+
/// Finality proof submission extras.
137+
extras: SubmitFinalityProofCallExtras,
138+
},
132139
/// Custom logic error.
133140
#[error("{0}")]
134141
Custom(String),

bridges/relays/lib-substrate-relay/src/finality/target.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,16 @@ impl<P: SubstrateFinalitySyncPipeline> TargetClient<FinalitySyncPipelineAdapter<
137137
let context =
138138
P::FinalityEngine::verify_and_optimize_proof(&self.client, &header, &mut proof).await?;
139139

140+
// if free execution is expected, but the call size/weight exceeds hardcoded limits, the
141+
// runtime may still accept the proof, but it may have some cost for relayer. Let's check
142+
// it here to avoid losing relayer funds
143+
if is_free_execution_expected {
144+
let extras = P::FinalityEngine::check_max_expected_call_limits(&header, &proof);
145+
if extras.is_weight_limit_exceeded || extras.extra_size != 0 {
146+
return Err(Error::FinalityProofWeightLimitExceeded { extras })
147+
}
148+
}
149+
140150
// now we may submit optimized finality proof
141151
let mortality = self.transaction_params.mortality;
142152
let call = P::SubmitFinalityProofCallBuilder::build_submit_finality_proof_call(

bridges/relays/lib-substrate-relay/src/finality_base/engine.rs

Lines changed: 9 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,8 @@ use bp_header_chain::{
2323
verify_and_optimize_justification, GrandpaEquivocationsFinder, GrandpaJustification,
2424
JustificationVerificationContext,
2525
},
26-
max_expected_submit_finality_proof_arguments_size, AuthoritySet, ConsensusLogReader,
27-
FinalityProof, FindEquivocations, GrandpaConsensusLogReader, HeaderFinalityInfo,
28-
HeaderGrandpaInfo, StoredHeaderGrandpaInfo,
26+
AuthoritySet, ConsensusLogReader, FinalityProof, FindEquivocations, GrandpaConsensusLogReader,
27+
HeaderFinalityInfo, HeaderGrandpaInfo, StoredHeaderGrandpaInfo, SubmitFinalityProofCallExtras,
2928
};
3029
use bp_runtime::{BasicOperatingMode, HeaderIdProvider, OperatingMode};
3130
use codec::{Decode, Encode};
@@ -36,22 +35,9 @@ use relay_substrate_client::{
3635
};
3736
use sp_consensus_grandpa::{AuthorityList as GrandpaAuthoritiesSet, GRANDPA_ENGINE_ID};
3837
use sp_core::{storage::StorageKey, Bytes};
39-
use sp_runtime::{scale_info::TypeInfo, traits::Header, ConsensusEngineId, SaturatedConversion};
38+
use sp_runtime::{scale_info::TypeInfo, traits::Header, ConsensusEngineId};
4039
use std::{fmt::Debug, marker::PhantomData};
4140

42-
/// Result of checking maximal expected call size.
43-
pub enum MaxExpectedCallSizeCheck {
44-
/// Size is ok and call will be refunded.
45-
Ok,
46-
/// The call size exceeds the maximal expected and relayer will only get partial refund.
47-
Exceeds {
48-
/// Actual call size.
49-
call_size: u32,
50-
/// Maximal expected call size.
51-
max_call_size: u32,
52-
},
53-
}
54-
5541
/// Finality engine, used by the Substrate chain.
5642
#[async_trait]
5743
pub trait Engine<C: Chain>: Send {
@@ -129,12 +115,11 @@ pub trait Engine<C: Chain>: Send {
129115
) -> Result<Self::FinalityVerificationContext, SubstrateError>;
130116

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

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

248-
fn check_max_expected_call_size(
233+
fn check_max_expected_call_limits(
249234
header: &C::Header,
250235
proof: &Self::FinalityProof,
251-
) -> MaxExpectedCallSizeCheck {
252-
let is_mandatory = Self::ConsensusLogReader::schedules_authorities_change(header.digest());
253-
let call_size: u32 =
254-
header.encoded_size().saturating_add(proof.encoded_size()).saturated_into();
255-
let max_call_size = max_expected_submit_finality_proof_arguments_size::<C>(
256-
is_mandatory,
257-
proof.commit.precommits.len().saturated_into(),
258-
);
259-
if call_size > max_call_size {
260-
MaxExpectedCallSizeCheck::Exceeds { call_size, max_call_size }
261-
} else {
262-
MaxExpectedCallSizeCheck::Ok
263-
}
236+
) -> SubmitFinalityProofCallExtras {
237+
bp_header_chain::submit_finality_proof_limits_extras::<C>(header, proof)
264238
}
265239

266240
/// Prepare initialization data for the GRANDPA verifier pallet.

bridges/relays/lib-substrate-relay/src/on_demand/headers.rs

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,7 @@
1616

1717
//! On-demand Substrate -> Substrate header finality relay.
1818
19-
use crate::{
20-
finality::SubmitFinalityProofCallBuilder, finality_base::engine::MaxExpectedCallSizeCheck,
21-
};
19+
use crate::finality::SubmitFinalityProofCallBuilder;
2220

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

157155
// now we have the header and its proof, but we want to minimize our losses, so let's
158156
// check if we'll get the full refund for submitting this header
159-
let check_result = P::FinalityEngine::check_max_expected_call_size(&header, &proof);
160-
if let MaxExpectedCallSizeCheck::Exceeds { call_size, max_call_size } = check_result {
157+
let check_result = P::FinalityEngine::check_max_expected_call_limits(&header, &proof);
158+
if check_result.is_weight_limit_exceeded || check_result.extra_size != 0 {
161159
iterations += 1;
162160
current_required_header = header_id.number().saturating_add(One::one());
163161
if iterations < MAX_ITERATIONS {
164162
log::debug!(
165163
target: "bridge",
166-
"[{}] Requested to prove {} head {:?}. Selected to prove {} head {:?}. But it is too large: {} vs {}. \
164+
"[{}] Requested to prove {} head {:?}. Selected to prove {} head {:?}. But it exceeds limits: {:?}. \
167165
Going to select next header",
168166
self.relay_task_name,
169167
P::SourceChain::NAME,
170168
required_header,
171169
P::SourceChain::NAME,
172170
header_id,
173-
call_size,
174-
max_call_size,
171+
check_result,
175172
);
176173

177174
continue;

0 commit comments

Comments
 (0)