Skip to content

Commit 730be33

Browse files
svyatonikacatangiu
andauthoredFeb 22, 2023
Weight+size limits for bridge GRANDPA pallet calls (paritytech#1882)
* weight+size limits for bridge GRANDPA pallet calls * continue * fixed all tests * some changes to refund computations * post_dispatch_refunds_relayer_in_all_finality_batch_with_extra_weight * - dup code * do not return Pays::No if call is above weight/size limits * relayer_pays_tx_fee_when_submitting_huge_mandatory_header and relayer_pays_tx_fee_when_submitting_justification_with_long_ancestry_votes * clippy * fmt * clippy * small change in docs * fixed GRANDPA-limits constants for Polkadot-like chains * clippy * clippy + spelling * Update primitives/polkadot-core/src/lib.rs Co-authored-by: Adrian Catangiu <adrian@parity.io> * Update bin/runtime-common/src/refund_relayer_extension.rs Co-authored-by: Adrian Catangiu <adrian@parity.io> * reverted unnecessary change * GrandpaJustification::max_reasonable_size --------- Co-authored-by: Adrian Catangiu <adrian@parity.io>
1 parent a910616 commit 730be33

File tree

10 files changed

+540
-92
lines changed

10 files changed

+540
-92
lines changed
 

‎bridges/bin/runtime-common/src/refund_relayer_extension.rs

+89-9
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,12 @@ use codec::{Decode, Encode};
2828
use frame_support::{
2929
dispatch::{CallableCallFor, DispatchInfo, Dispatchable, PostDispatchInfo},
3030
traits::IsSubType,
31+
weights::Weight,
3132
CloneNoBound, DefaultNoBound, EqNoBound, PartialEqNoBound, RuntimeDebugNoBound,
3233
};
33-
use pallet_bridge_grandpa::{CallSubType as GrandpaCallSubType, SubmitFinalityProofHelper};
34+
use pallet_bridge_grandpa::{
35+
CallSubType as GrandpaCallSubType, SubmitFinalityProofHelper, SubmitFinalityProofInfo,
36+
};
3437
use pallet_bridge_messages::Config as MessagesConfig;
3538
use pallet_bridge_parachains::{
3639
BoundedBridgeGrandpaConfig, CallSubType as ParachainsCallSubType, Config as ParachainsConfig,
@@ -140,7 +143,11 @@ pub struct PreDispatchData<AccountId> {
140143
#[derive(RuntimeDebugNoBound, PartialEq)]
141144
pub enum CallInfo {
142145
/// Relay chain finality + parachain finality + message delivery calls.
143-
AllFinalityAndDelivery(RelayBlockNumber, SubmitParachainHeadsInfo, ReceiveMessagesProofInfo),
146+
AllFinalityAndDelivery(
147+
SubmitFinalityProofInfo<RelayBlockNumber>,
148+
SubmitParachainHeadsInfo,
149+
ReceiveMessagesProofInfo,
150+
),
144151
/// Parachain finality + message delivery calls.
145152
ParachainFinalityAndDelivery(SubmitParachainHeadsInfo, ReceiveMessagesProofInfo),
146153
/// Standalone message delivery call.
@@ -149,7 +156,7 @@ pub enum CallInfo {
149156

150157
impl CallInfo {
151158
/// Returns the pre-dispatch `finality_target` sent to the `SubmitFinalityProof` call.
152-
fn submit_finality_proof_info(&self) -> Option<RelayBlockNumber> {
159+
fn submit_finality_proof_info(&self) -> Option<SubmitFinalityProofInfo<RelayBlockNumber>> {
153160
match *self {
154161
Self::AllFinalityAndDelivery(info, _, _) => Some(info),
155162
_ => None,
@@ -318,6 +325,9 @@ where
318325
len: usize,
319326
result: &DispatchResult,
320327
) -> Result<(), TransactionValidityError> {
328+
let mut extra_weight = Weight::zero();
329+
let mut extra_size = 0;
330+
321331
// We don't refund anything if the transaction has failed.
322332
if result.is_err() {
323333
return Ok(())
@@ -330,8 +340,10 @@ where
330340
};
331341

332342
// check if relay chain state has been updated
333-
if let Some(relay_block_number) = call_info.submit_finality_proof_info() {
334-
if !SubmitFinalityProofHelper::<Runtime, Runtime::BridgesGrandpaPalletInstance>::was_successful(relay_block_number) {
343+
if let Some(finality_proof_info) = call_info.submit_finality_proof_info() {
344+
if !SubmitFinalityProofHelper::<Runtime, Runtime::BridgesGrandpaPalletInstance>::was_successful(
345+
finality_proof_info.block_number,
346+
) {
335347
// we only refund relayer if all calls have updated chain state
336348
return Ok(())
337349
}
@@ -342,6 +354,11 @@ where
342354
// `utility.batchAll` transaction always requires payment. But in both cases we'll
343355
// refund relayer - either explicitly here, or using `Pays::No` if he's choosing
344356
// to submit dedicated transaction.
357+
358+
// submitter has means to include extra weight/bytes in the `submit_finality_proof`
359+
// call, so let's subtract extra weight/size to avoid refunding for this extra stuff
360+
extra_weight = finality_proof_info.extra_weight;
361+
extra_size = finality_proof_info.extra_size;
345362
}
346363

347364
// check if parachain state has been updated
@@ -370,8 +387,15 @@ where
370387
// cost of this attack is nothing. Hence we use zero as tip here.
371388
let tip = Zero::zero();
372389

390+
// decrease post-dispatch weight/size using extra weight/size that we know now
391+
let post_info_len = len.saturating_sub(extra_size as usize);
392+
let mut post_info = *post_info;
393+
post_info.actual_weight =
394+
Some(post_info.actual_weight.unwrap_or(info.weight).saturating_sub(extra_weight));
395+
373396
// compute the relayer refund
374-
let refund = Refund::compute_refund(info, post_info, len, tip);
397+
let refund = Refund::compute_refund(info, &post_info, post_info_len, tip);
398+
375399
// finally - register refund in relayers pallet
376400
RelayersPallet::<Runtime>::register_relayer_reward(Msgs::Id::get(), &relayer, refund);
377401

@@ -397,9 +421,9 @@ mod tests {
397421
use bp_parachains::{BestParaHeadHash, ParaInfo};
398422
use bp_polkadot_core::parachains::{ParaHash, ParaHeadsProof, ParaId};
399423
use bp_runtime::HeaderId;
400-
use bp_test_utils::make_default_justification;
424+
use bp_test_utils::{make_default_justification, test_keyring};
401425
use frame_support::{assert_storage_noop, parameter_types, weights::Weight};
402-
use pallet_bridge_grandpa::Call as GrandpaCall;
426+
use pallet_bridge_grandpa::{Call as GrandpaCall, StoredAuthoritySet};
403427
use pallet_bridge_messages::Call as MessagesCall;
404428
use pallet_bridge_parachains::{Call as ParachainsCall, RelayBlockHash};
405429
use sp_runtime::{
@@ -434,7 +458,11 @@ mod tests {
434458
parachain_head_hash: ParaHash,
435459
best_delivered_message: MessageNonce,
436460
) {
461+
let authorities = test_keyring().into_iter().map(|(a, w)| (a.into(), w)).collect();
437462
let best_relay_header = HeaderId(best_relay_header_number, RelayBlockHash::default());
463+
pallet_bridge_grandpa::CurrentAuthoritySet::<TestRuntime>::put(
464+
StoredAuthoritySet::try_new(authorities, 0).unwrap(),
465+
);
438466
pallet_bridge_grandpa::BestFinalized::<TestRuntime>::put(best_relay_header);
439467

440468
let para_id = ParaId(TestParachain::get());
@@ -524,7 +552,11 @@ mod tests {
524552
PreDispatchData {
525553
relayer: relayer_account_at_this_chain(),
526554
call_info: CallInfo::AllFinalityAndDelivery(
527-
200,
555+
SubmitFinalityProofInfo {
556+
block_number: 200,
557+
extra_weight: Weight::zero(),
558+
extra_size: 0,
559+
},
528560
SubmitParachainHeadsInfo {
529561
at_relay_block_number: 200,
530562
para_id: ParaId(TestParachain::get()),
@@ -823,6 +855,54 @@ mod tests {
823855
});
824856
}
825857

858+
#[test]
859+
fn post_dispatch_refunds_relayer_in_all_finality_batch_with_extra_weight() {
860+
run_test(|| {
861+
initialize_environment(200, 200, [1u8; 32].into(), 200);
862+
863+
let mut dispatch_info = dispatch_info();
864+
dispatch_info.weight = Weight::from_ref_time(
865+
frame_support::weights::constants::WEIGHT_REF_TIME_PER_SECOND * 2,
866+
);
867+
868+
// without any size/weight refund: we expect regular reward
869+
let pre_dispatch_data = all_finality_pre_dispatch_data();
870+
let regular_reward = expected_reward();
871+
run_post_dispatch(Some(pre_dispatch_data), Ok(()));
872+
assert_eq!(
873+
RelayersPallet::<TestRuntime>::relayer_reward(
874+
relayer_account_at_this_chain(),
875+
TestLaneId::get()
876+
),
877+
Some(regular_reward),
878+
);
879+
880+
// now repeat the same with size+weight refund: we expect smaller reward
881+
let mut pre_dispatch_data = all_finality_pre_dispatch_data();
882+
match pre_dispatch_data.call_info {
883+
CallInfo::AllFinalityAndDelivery(ref mut info, ..) => {
884+
info.extra_weight.set_ref_time(
885+
frame_support::weights::constants::WEIGHT_REF_TIME_PER_SECOND,
886+
);
887+
info.extra_size = 32;
888+
},
889+
_ => unreachable!(),
890+
}
891+
run_post_dispatch(Some(pre_dispatch_data), Ok(()));
892+
let reward_after_two_calls = RelayersPallet::<TestRuntime>::relayer_reward(
893+
relayer_account_at_this_chain(),
894+
TestLaneId::get(),
895+
)
896+
.unwrap();
897+
assert!(
898+
reward_after_two_calls < 2 * regular_reward,
899+
"{} must be < 2 * {}",
900+
reward_after_two_calls,
901+
2 * regular_reward,
902+
);
903+
});
904+
}
905+
826906
#[test]
827907
fn post_dispatch_refunds_relayer_in_all_finality_batch() {
828908
run_test(|| {

‎bridges/modules/grandpa/src/call_ext.rs

+160-12
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,46 @@
1414
// You should have received a copy of the GNU General Public License
1515
// along with Parity Bridges Common. If not, see <http://www.gnu.org/licenses/>.
1616

17-
use crate::{Config, Error, Pallet};
17+
use crate::{weights::WeightInfo, BridgedBlockNumber, BridgedHeader, Config, Error, Pallet};
18+
use bp_header_chain::{justification::GrandpaJustification, ChainWithGrandpa};
1819
use bp_runtime::BlockNumberOf;
19-
use frame_support::{dispatch::CallableCallFor, traits::IsSubType};
20+
use codec::Encode;
21+
use frame_support::{dispatch::CallableCallFor, traits::IsSubType, weights::Weight, RuntimeDebug};
2022
use sp_runtime::{
21-
traits::Header,
23+
traits::{Header, Zero},
2224
transaction_validity::{InvalidTransaction, TransactionValidity, ValidTransaction},
25+
SaturatedConversion,
2326
};
2427

28+
/// Info about a `SubmitParachainHeads` call which tries to update a single parachain.
29+
#[derive(Copy, Clone, PartialEq, RuntimeDebug)]
30+
pub struct SubmitFinalityProofInfo<N> {
31+
/// Number of the finality target.
32+
pub block_number: N,
33+
/// Extra weight that we assume is included in the call.
34+
///
35+
/// We have some assumptions about headers and justifications of the bridged chain.
36+
/// We know that if our assumptions are correct, then the call must not have the
37+
/// weight above some limit. The fee paid for weight above that limit, is never refunded.
38+
pub extra_weight: Weight,
39+
/// Extra size (in bytes) that we assume are included in the call.
40+
///
41+
/// We have some assumptions about headers and justifications of the bridged chain.
42+
/// We know that if our assumptions are correct, then the call must not have the
43+
/// weight above some limit. The fee paid for bytes above that limit, is never refunded.
44+
pub extra_size: u32,
45+
}
46+
47+
impl<N> SubmitFinalityProofInfo<N> {
48+
/// Returns `true` if call size/weight is below our estimations for regular calls.
49+
pub fn fits_limits(&self) -> bool {
50+
self.extra_weight.is_zero() && self.extra_size.is_zero()
51+
}
52+
}
53+
2554
/// Helper struct that provides methods for working with the `SubmitFinalityProof` call.
2655
pub struct SubmitFinalityProofHelper<T: Config<I>, I: 'static> {
27-
pub _phantom_data: sp_std::marker::PhantomData<(T, I)>,
56+
_phantom_data: sp_std::marker::PhantomData<(T, I)>,
2857
}
2958

3059
impl<T: Config<I>, I: 'static> SubmitFinalityProofHelper<T, I> {
@@ -69,12 +98,17 @@ impl<T: Config<I>, I: 'static> SubmitFinalityProofHelper<T, I> {
6998
pub trait CallSubType<T: Config<I, RuntimeCall = Self>, I: 'static>:
7099
IsSubType<CallableCallFor<Pallet<T, I>, T>>
71100
{
72-
/// Extract the finality target from a `SubmitParachainHeads` call.
73-
fn submit_finality_proof_info(&self) -> Option<BlockNumberOf<T::BridgedChain>> {
74-
if let Some(crate::Call::<T, I>::submit_finality_proof { finality_target, .. }) =
101+
/// Extract finality proof info from a runtime call.
102+
fn submit_finality_proof_info(
103+
&self,
104+
) -> Option<SubmitFinalityProofInfo<BridgedBlockNumber<T, I>>> {
105+
if let Some(crate::Call::<T, I>::submit_finality_proof { finality_target, justification }) =
75106
self.is_sub_type()
76107
{
77-
return Some(*finality_target.number())
108+
return Some(submit_finality_proof_info_from_args::<T, I>(
109+
finality_target,
110+
justification,
111+
))
78112
}
79113

80114
None
@@ -92,7 +126,7 @@ pub trait CallSubType<T: Config<I, RuntimeCall = Self>, I: 'static>:
92126
_ => return Ok(ValidTransaction::default()),
93127
};
94128

95-
match SubmitFinalityProofHelper::<T, I>::check_obsolete(finality_target) {
129+
match SubmitFinalityProofHelper::<T, I>::check_obsolete(finality_target.block_number) {
96130
Ok(_) => Ok(ValidTransaction::default()),
97131
Err(Error::<T, I>::OldHeader) => InvalidTransaction::Stale.into(),
98132
Err(_) => InvalidTransaction::Call.into(),
@@ -105,15 +139,66 @@ impl<T: Config<I>, I: 'static> CallSubType<T, I> for T::RuntimeCall where
105139
{
106140
}
107141

142+
/// Extract finality proof info from the submitted header and justification.
143+
pub(crate) fn submit_finality_proof_info_from_args<T: Config<I>, I: 'static>(
144+
finality_target: &BridgedHeader<T, I>,
145+
justification: &GrandpaJustification<BridgedHeader<T, I>>,
146+
) -> SubmitFinalityProofInfo<BridgedBlockNumber<T, I>> {
147+
let block_number = *finality_target.number();
148+
149+
// the `submit_finality_proof` call will reject justifications with invalid, duplicate,
150+
// unknown and extra signatures. It'll also reject justifications with less than necessary
151+
// signatures. So we do not care about extra weight because of additional signatures here.
152+
let precommits_len = justification.commit.precommits.len().saturated_into();
153+
let required_precommits = precommits_len;
154+
155+
// We do care about extra weight because of more-than-expected headers in the votes
156+
// ancestries. But we have problems computing extra weight for additional headers (weight of
157+
// additional header is too small, so that our benchmarks aren't detecting that). So if there
158+
// are more than expected headers in votes ancestries, we will treat the whole call weight
159+
// as an extra weight.
160+
let votes_ancestries_len = justification.votes_ancestries.len().saturated_into();
161+
let extra_weight =
162+
if votes_ancestries_len > T::BridgedChain::REASONABLE_HEADERS_IN_JUSTIFICATON_ANCESTRY {
163+
T::WeightInfo::submit_finality_proof(precommits_len, votes_ancestries_len)
164+
} else {
165+
Weight::zero()
166+
};
167+
168+
// we can estimate extra call size easily, without any additional significant overhead
169+
let actual_call_size: u32 = finality_target
170+
.encoded_size()
171+
.saturating_add(justification.encoded_size())
172+
.saturated_into();
173+
let max_expected_call_size = max_expected_call_size::<T, I>(required_precommits);
174+
let extra_size = actual_call_size.saturating_sub(max_expected_call_size);
175+
176+
SubmitFinalityProofInfo { block_number, extra_weight, extra_size }
177+
}
178+
179+
/// Returns maximal expected size of `submit_finality_proof` call arguments.
180+
fn max_expected_call_size<T: Config<I>, I: 'static>(required_precommits: u32) -> u32 {
181+
let max_expected_justification_size =
182+
GrandpaJustification::max_reasonable_size::<T::BridgedChain>(required_precommits);
183+
184+
// call arguments are header and justification
185+
T::BridgedChain::MAX_HEADER_SIZE.saturating_add(max_expected_justification_size)
186+
}
187+
108188
#[cfg(test)]
109189
mod tests {
110190
use crate::{
111191
call_ext::CallSubType,
112-
mock::{run_test, test_header, RuntimeCall, TestNumber, TestRuntime},
113-
BestFinalized,
192+
mock::{run_test, test_header, RuntimeCall, TestBridgedChain, TestNumber, TestRuntime},
193+
BestFinalized, Config, WeightInfo,
114194
};
195+
use bp_header_chain::ChainWithGrandpa;
115196
use bp_runtime::HeaderId;
116-
use bp_test_utils::make_default_justification;
197+
use bp_test_utils::{
198+
make_default_justification, make_justification_for_header, JustificationGeneratorParams,
199+
};
200+
use frame_support::weights::Weight;
201+
use sp_runtime::{testing::DigestItem, traits::Header as _, SaturatedConversion};
117202

118203
fn validate_block_submit(num: TestNumber) -> bool {
119204
let bridge_grandpa_call = crate::Call::<TestRuntime, ()>::submit_finality_proof {
@@ -160,4 +245,67 @@ mod tests {
160245
assert!(validate_block_submit(15));
161246
});
162247
}
248+
249+
#[test]
250+
fn extension_returns_correct_extra_size_if_call_arguments_are_too_large() {
251+
// when call arguments are below our limit => no refund
252+
let small_finality_target = test_header(1);
253+
let justification_params = JustificationGeneratorParams {
254+
header: small_finality_target.clone(),
255+
..Default::default()
256+
};
257+
let small_justification = make_justification_for_header(justification_params);
258+
let small_call = RuntimeCall::Grandpa(crate::Call::submit_finality_proof {
259+
finality_target: Box::new(small_finality_target),
260+
justification: small_justification,
261+
});
262+
assert_eq!(small_call.submit_finality_proof_info().unwrap().extra_size, 0);
263+
264+
// when call arguments are too large => partial refund
265+
let mut large_finality_target = test_header(1);
266+
large_finality_target
267+
.digest_mut()
268+
.push(DigestItem::Other(vec![42u8; 1024 * 1024]));
269+
let justification_params = JustificationGeneratorParams {
270+
header: large_finality_target.clone(),
271+
..Default::default()
272+
};
273+
let large_justification = make_justification_for_header(justification_params);
274+
let large_call = RuntimeCall::Grandpa(crate::Call::submit_finality_proof {
275+
finality_target: Box::new(large_finality_target),
276+
justification: large_justification,
277+
});
278+
assert_ne!(large_call.submit_finality_proof_info().unwrap().extra_size, 0);
279+
}
280+
281+
#[test]
282+
fn extension_returns_correct_extra_weight_if_there_are_too_many_headers_in_votes_ancestry() {
283+
let finality_target = test_header(1);
284+
let mut justification_params = JustificationGeneratorParams {
285+
header: finality_target.clone(),
286+
ancestors: TestBridgedChain::REASONABLE_HEADERS_IN_JUSTIFICATON_ANCESTRY,
287+
..Default::default()
288+
};
289+
290+
// when there are `REASONABLE_HEADERS_IN_JUSTIFICATON_ANCESTRY` headers => no refund
291+
let justification = make_justification_for_header(justification_params.clone());
292+
let call = RuntimeCall::Grandpa(crate::Call::submit_finality_proof {
293+
finality_target: Box::new(finality_target.clone()),
294+
justification,
295+
});
296+
assert_eq!(call.submit_finality_proof_info().unwrap().extra_weight, Weight::zero());
297+
298+
// when there are `REASONABLE_HEADERS_IN_JUSTIFICATON_ANCESTRY + 1` headers => full refund
299+
justification_params.ancestors += 1;
300+
let justification = make_justification_for_header(justification_params);
301+
let call_weight = <TestRuntime as Config>::WeightInfo::submit_finality_proof(
302+
justification.commit.precommits.len().saturated_into(),
303+
justification.votes_ancestries.len().saturated_into(),
304+
);
305+
let call = RuntimeCall::Grandpa(crate::Call::submit_finality_proof {
306+
finality_target: Box::new(finality_target),
307+
justification,
308+
});
309+
assert_eq!(call.submit_finality_proof_info().unwrap().extra_weight, call_weight);
310+
}
163311
}

0 commit comments

Comments
 (0)