1use crate::{
18 weights::WeightInfo, BestFinalized, BridgedBlockNumber, BridgedHeader, Config,
19 CurrentAuthoritySet, Error, FreeHeadersRemaining, Pallet,
20};
21use bp_header_chain::{
22 justification::GrandpaJustification, submit_finality_proof_limits_extras,
23 SubmitFinalityProofInfo,
24};
25use bp_runtime::{BlockNumberOf, Chain, OwnedBridgeModule};
26use frame_support::{
27 dispatch::CallableCallFor,
28 traits::{Get, IsSubType},
29 weights::Weight,
30};
31use sp_consensus_grandpa::SetId;
32use sp_runtime::{
33 traits::{CheckedSub, Header, Zero},
34 transaction_validity::{InvalidTransaction, TransactionValidityError},
35 RuntimeDebug, SaturatedConversion,
36};
37use sp_std::fmt::Debug;
38
39#[derive(Copy, Clone, PartialEq, RuntimeDebug)]
41pub struct VerifiedSubmitFinalityProofInfo<N: Debug> {
42 pub base: SubmitFinalityProofInfo<N>,
44 pub improved_by: N,
47}
48
49pub struct SubmitFinalityProofHelper<T: Config<I>, I: 'static> {
51 _phantom_data: sp_std::marker::PhantomData<(T, I)>,
52}
53
54impl<T: Config<I>, I: 'static> SubmitFinalityProofHelper<T, I> {
55 pub fn has_free_header_slots() -> bool {
59 let free_headers_remaining = FreeHeadersRemaining::<T, I>::get().unwrap_or(u32::MAX);
65 free_headers_remaining > 0
66 }
67
68 pub fn check_obsolete_from_extension(
76 call_info: &SubmitFinalityProofInfo<BlockNumberOf<T::BridgedChain>>,
77 ) -> Result<BlockNumberOf<T::BridgedChain>, Error<T, I>> {
78 let improved_by = Self::check_obsolete(call_info.block_number, call_info.current_set_id)?;
80
81 if !call_info.is_free_execution_expected {
83 return Ok(improved_by);
84 }
85
86 if !Self::has_free_header_slots() {
88 tracing::trace!(
89 target: crate::LOG_TARGET,
90 chain_id=?T::BridgedChain::ID,
91 block_number=?call_info.block_number,
92 "Cannot accept free header. No more free slots remaining"
93 );
94
95 return Err(Error::<T, I>::FreeHeadersLimitExceded);
96 }
97
98 if !call_info.is_mandatory {
100 if let Some(free_headers_interval) = T::FreeHeadersInterval::get() {
101 if improved_by < free_headers_interval.into() {
102 tracing::trace!(
103 target: crate::LOG_TARGET,
104 chain_id=?T::BridgedChain::ID,
105 block_number=?call_info.block_number,
106 ?improved_by,
107 %free_headers_interval,
108 "Cannot accept free header. Too small difference between submitted headers"
109 );
110
111 return Err(Error::<T, I>::BelowFreeHeaderInterval);
112 }
113 }
114 }
115
116 if !call_info.fits_limits() {
121 return Err(Error::<T, I>::HeaderOverflowLimits);
122 }
123
124 Ok(improved_by)
125 }
126
127 pub fn check_obsolete(
135 finality_target: BlockNumberOf<T::BridgedChain>,
136 current_set_id: Option<SetId>,
137 ) -> Result<BlockNumberOf<T::BridgedChain>, Error<T, I>> {
138 let best_finalized = BestFinalized::<T, I>::get().ok_or_else(|| {
139 tracing::trace!(
140 target: crate::LOG_TARGET,
141 header=?finality_target,
142 "Cannot finalize header because pallet is not yet initialized"
143 );
144 <Error<T, I>>::NotInitialized
145 })?;
146
147 let improved_by = match finality_target.checked_sub(&best_finalized.number()) {
148 Some(improved_by) if improved_by > Zero::zero() => improved_by,
149 _ => {
150 tracing::trace!(
151 target: crate::LOG_TARGET,
152 bundled=?finality_target,
153 best=?best_finalized,
154 "Cannot finalize obsolete header"
155 );
156
157 return Err(Error::<T, I>::OldHeader)
158 },
159 };
160
161 if let Some(current_set_id) = current_set_id {
162 let actual_set_id = <CurrentAuthoritySet<T, I>>::get().set_id;
163 if current_set_id != actual_set_id {
164 tracing::trace!(
165 target: crate::LOG_TARGET,
166 bundled=?current_set_id,
167 best=?actual_set_id,
168 "Cannot finalize header signed by unknown authority set"
169 );
170
171 return Err(Error::<T, I>::InvalidAuthoritySetId)
172 }
173 }
174
175 Ok(improved_by)
176 }
177
178 pub fn was_successful(finality_target: BlockNumberOf<T::BridgedChain>) -> bool {
180 match BestFinalized::<T, I>::get() {
181 Some(best_finalized) => best_finalized.number() == finality_target,
182 None => false,
183 }
184 }
185}
186
187pub trait CallSubType<T: Config<I, RuntimeCall = Self>, I: 'static>:
189 IsSubType<CallableCallFor<Pallet<T, I>, T>>
190{
191 fn submit_finality_proof_info(
193 &self,
194 ) -> Option<SubmitFinalityProofInfo<BridgedBlockNumber<T, I>>> {
195 if let Some(crate::Call::<T, I>::submit_finality_proof { finality_target, justification }) =
196 self.is_sub_type()
197 {
198 return Some(submit_finality_proof_info_from_args::<T, I>(
199 finality_target,
200 justification,
201 None,
202 false,
203 ))
204 } else if let Some(crate::Call::<T, I>::submit_finality_proof_ex {
205 finality_target,
206 justification,
207 current_set_id,
208 is_free_execution_expected,
209 }) = self.is_sub_type()
210 {
211 return Some(submit_finality_proof_info_from_args::<T, I>(
212 finality_target,
213 justification,
214 Some(*current_set_id),
215 *is_free_execution_expected,
216 ))
217 }
218
219 None
220 }
221
222 fn check_obsolete_submit_finality_proof(
232 &self,
233 ) -> Result<
234 Option<VerifiedSubmitFinalityProofInfo<BridgedBlockNumber<T, I>>>,
235 TransactionValidityError,
236 >
237 where
238 Self: Sized,
239 {
240 let call_info = match self.submit_finality_proof_info() {
241 Some(finality_proof) => finality_proof,
242 _ => return Ok(None),
243 };
244
245 if Pallet::<T, I>::ensure_not_halted().is_err() {
246 return Err(InvalidTransaction::Call.into())
247 }
248
249 let result = SubmitFinalityProofHelper::<T, I>::check_obsolete_from_extension(&call_info);
250 match result {
251 Ok(improved_by) =>
252 Ok(Some(VerifiedSubmitFinalityProofInfo { base: call_info, improved_by })),
253 Err(Error::<T, I>::OldHeader) => Err(InvalidTransaction::Stale.into()),
254 Err(_) => Err(InvalidTransaction::Call.into()),
255 }
256 }
257}
258
259impl<T: Config<I>, I: 'static> CallSubType<T, I> for T::RuntimeCall where
260 T::RuntimeCall: IsSubType<CallableCallFor<Pallet<T, I>, T>>
261{
262}
263
264pub(crate) fn submit_finality_proof_info_from_args<T: Config<I>, I: 'static>(
266 finality_target: &BridgedHeader<T, I>,
267 justification: &GrandpaJustification<BridgedHeader<T, I>>,
268 current_set_id: Option<SetId>,
269 is_free_execution_expected: bool,
270) -> SubmitFinalityProofInfo<BridgedBlockNumber<T, I>> {
271 let extras =
274 submit_finality_proof_limits_extras::<T::BridgedChain>(finality_target, justification);
275
276 let extra_weight = if extras.is_weight_limit_exceeded {
282 let precommits_len = justification.commit.precommits.len().saturated_into();
283 let votes_ancestries_len = justification.votes_ancestries.len().saturated_into();
284 T::WeightInfo::submit_finality_proof(precommits_len, votes_ancestries_len)
285 } else {
286 Weight::zero()
287 };
288
289 SubmitFinalityProofInfo {
290 block_number: *finality_target.number(),
291 current_set_id,
292 is_mandatory: extras.is_mandatory_finality_target,
293 is_free_execution_expected,
294 extra_weight,
295 extra_size: extras.extra_size,
296 }
297}
298
299#[cfg(test)]
300mod tests {
301 use crate::{
302 call_ext::CallSubType,
303 mock::{
304 run_test, test_header, FreeHeadersInterval, RuntimeCall, TestBridgedChain, TestNumber,
305 TestRuntime,
306 },
307 BestFinalized, Config, CurrentAuthoritySet, FreeHeadersRemaining, PalletOperatingMode,
308 StoredAuthoritySet, WeightInfo,
309 };
310 use bp_header_chain::{ChainWithGrandpa, SubmitFinalityProofInfo};
311 use bp_runtime::{BasicOperatingMode, HeaderId};
312 use bp_test_utils::{
313 make_default_justification, make_justification_for_header, JustificationGeneratorParams,
314 TEST_GRANDPA_SET_ID,
315 };
316 use codec::Encode;
317 use frame_support::weights::Weight;
318 use sp_runtime::{testing::DigestItem, traits::Header as _, SaturatedConversion};
319
320 fn validate_block_submit(num: TestNumber) -> bool {
321 let bridge_grandpa_call = crate::Call::<TestRuntime, ()>::submit_finality_proof_ex {
322 finality_target: Box::new(test_header(num)),
323 justification: make_default_justification(&test_header(num)),
324 current_set_id: 0,
326 is_free_execution_expected: false,
327 };
328 RuntimeCall::check_obsolete_submit_finality_proof(&RuntimeCall::Grandpa(
329 bridge_grandpa_call,
330 ))
331 .is_ok()
332 }
333
334 fn sync_to_header_10() {
335 let header10_hash = sp_core::H256::default();
336 BestFinalized::<TestRuntime, ()>::put(HeaderId(10, header10_hash));
337 }
338
339 #[test]
340 fn extension_rejects_obsolete_header() {
341 run_test(|| {
342 sync_to_header_10();
345 assert!(!validate_block_submit(5));
346 });
347 }
348
349 #[test]
350 fn extension_rejects_same_header() {
351 run_test(|| {
352 sync_to_header_10();
355 assert!(!validate_block_submit(10));
356 });
357 }
358
359 #[test]
360 fn extension_rejects_new_header_if_pallet_is_halted() {
361 run_test(|| {
362 sync_to_header_10();
364 PalletOperatingMode::<TestRuntime, ()>::put(BasicOperatingMode::Halted);
365
366 assert!(!validate_block_submit(15));
367 });
368 }
369
370 #[test]
371 fn extension_rejects_new_header_if_set_id_is_invalid() {
372 run_test(|| {
373 sync_to_header_10();
375 let next_set = StoredAuthoritySet::<TestRuntime, ()>::try_new(vec![], 0x42).unwrap();
376 CurrentAuthoritySet::<TestRuntime, ()>::put(next_set);
377
378 assert!(!validate_block_submit(15));
379 });
380 }
381
382 #[test]
383 fn extension_rejects_new_header_if_free_execution_is_requested_and_free_submissions_are_not_accepted(
384 ) {
385 run_test(|| {
386 let bridge_grandpa_call = crate::Call::<TestRuntime, ()>::submit_finality_proof_ex {
387 finality_target: Box::new(test_header(10 + FreeHeadersInterval::get() as u64)),
388 justification: make_default_justification(&test_header(
389 10 + FreeHeadersInterval::get() as u64,
390 )),
391 current_set_id: 0,
392 is_free_execution_expected: true,
393 };
394 sync_to_header_10();
395
396 FreeHeadersRemaining::<TestRuntime, ()>::put(2);
398 assert!(RuntimeCall::check_obsolete_submit_finality_proof(&RuntimeCall::Grandpa(
399 bridge_grandpa_call.clone(),
400 ),)
401 .is_ok());
402
403 FreeHeadersRemaining::<TestRuntime, ()>::put(0);
405 assert!(RuntimeCall::check_obsolete_submit_finality_proof(&RuntimeCall::Grandpa(
406 bridge_grandpa_call.clone(),
407 ),)
408 .is_err());
409
410 FreeHeadersRemaining::<TestRuntime, ()>::kill();
412 assert!(RuntimeCall::check_obsolete_submit_finality_proof(&RuntimeCall::Grandpa(
413 bridge_grandpa_call,
414 ),)
415 .is_ok());
416 })
417 }
418
419 #[test]
420 fn extension_rejects_new_header_if_it_overflow_size_limits() {
421 run_test(|| {
422 let mut large_finality_target = test_header(10 + FreeHeadersInterval::get() as u64);
423 large_finality_target
424 .digest_mut()
425 .push(DigestItem::Other(vec![42u8; 1024 * 1024]));
426 let justification_params = JustificationGeneratorParams {
427 header: large_finality_target.clone(),
428 ..Default::default()
429 };
430 let large_justification = make_justification_for_header(justification_params);
431
432 let bridge_grandpa_call = crate::Call::<TestRuntime, ()>::submit_finality_proof_ex {
433 finality_target: Box::new(large_finality_target),
434 justification: large_justification,
435 current_set_id: 0,
436 is_free_execution_expected: true,
437 };
438 sync_to_header_10();
439
440 FreeHeadersRemaining::<TestRuntime, ()>::put(2);
442 assert!(RuntimeCall::check_obsolete_submit_finality_proof(&RuntimeCall::Grandpa(
443 bridge_grandpa_call.clone(),
444 ),)
445 .is_err());
446 })
447 }
448
449 #[test]
450 fn extension_rejects_new_header_if_it_overflow_weight_limits() {
451 run_test(|| {
452 let finality_target = test_header(10 + FreeHeadersInterval::get() as u64);
453 let justification_params = JustificationGeneratorParams {
454 header: finality_target.clone(),
455 ancestors: TestBridgedChain::REASONABLE_HEADERS_IN_JUSTIFICATION_ANCESTRY,
456 ..Default::default()
457 };
458 let justification = make_justification_for_header(justification_params);
459
460 let bridge_grandpa_call = crate::Call::<TestRuntime, ()>::submit_finality_proof_ex {
461 finality_target: Box::new(finality_target),
462 justification,
463 current_set_id: 0,
464 is_free_execution_expected: true,
465 };
466 sync_to_header_10();
467
468 FreeHeadersRemaining::<TestRuntime, ()>::put(2);
470 assert!(RuntimeCall::check_obsolete_submit_finality_proof(&RuntimeCall::Grandpa(
471 bridge_grandpa_call.clone(),
472 ),)
473 .is_err());
474 })
475 }
476
477 #[test]
478 fn extension_rejects_new_header_if_free_execution_is_requested_and_improved_by_is_below_expected(
479 ) {
480 run_test(|| {
481 let bridge_grandpa_call = crate::Call::<TestRuntime, ()>::submit_finality_proof_ex {
482 finality_target: Box::new(test_header(100)),
483 justification: make_default_justification(&test_header(100)),
484 current_set_id: 0,
485 is_free_execution_expected: true,
486 };
487 sync_to_header_10();
488
489 BestFinalized::<TestRuntime, ()>::put(HeaderId(
491 100 - FreeHeadersInterval::get() as u64 + 1,
492 sp_core::H256::default(),
493 ));
494 assert!(RuntimeCall::check_obsolete_submit_finality_proof(&RuntimeCall::Grandpa(
495 bridge_grandpa_call.clone(),
496 ),)
497 .is_err());
498
499 BestFinalized::<TestRuntime, ()>::put(HeaderId(
501 100 - FreeHeadersInterval::get() as u64,
502 sp_core::H256::default(),
503 ));
504 assert!(RuntimeCall::check_obsolete_submit_finality_proof(&RuntimeCall::Grandpa(
505 bridge_grandpa_call.clone(),
506 ),)
507 .is_ok());
508
509 BestFinalized::<TestRuntime, ()>::put(HeaderId(
511 100 - FreeHeadersInterval::get() as u64 - 1,
512 sp_core::H256::default(),
513 ));
514 assert!(RuntimeCall::check_obsolete_submit_finality_proof(&RuntimeCall::Grandpa(
515 bridge_grandpa_call.clone(),
516 ),)
517 .is_ok());
518
519 let mut mandatory_header = test_header(100);
521 let consensus_log = sp_consensus_grandpa::ConsensusLog::<TestNumber>::ScheduledChange(
522 sp_consensus_grandpa::ScheduledChange {
523 next_authorities: bp_test_utils::authority_list(),
524 delay: 0,
525 },
526 );
527 mandatory_header.digest = sp_runtime::Digest {
528 logs: vec![DigestItem::Consensus(
529 sp_consensus_grandpa::GRANDPA_ENGINE_ID,
530 consensus_log.encode(),
531 )],
532 };
533 let justification = make_justification_for_header(JustificationGeneratorParams {
534 header: mandatory_header.clone(),
535 set_id: 1,
536 ..Default::default()
537 });
538 let bridge_grandpa_call = crate::Call::<TestRuntime, ()>::submit_finality_proof_ex {
539 finality_target: Box::new(mandatory_header),
540 justification,
541 current_set_id: 0,
542 is_free_execution_expected: true,
543 };
544 BestFinalized::<TestRuntime, ()>::put(HeaderId(
545 100 - FreeHeadersInterval::get() as u64 + 1,
546 sp_core::H256::default(),
547 ));
548 assert!(RuntimeCall::check_obsolete_submit_finality_proof(&RuntimeCall::Grandpa(
549 bridge_grandpa_call.clone(),
550 ),)
551 .is_ok());
552 })
553 }
554
555 #[test]
556 fn extension_accepts_new_header() {
557 run_test(|| {
558 sync_to_header_10();
561 assert!(validate_block_submit(15));
562 });
563 }
564
565 #[test]
566 fn submit_finality_proof_info_is_parsed() {
567 let deprecated_call =
569 RuntimeCall::Grandpa(crate::Call::<TestRuntime, ()>::submit_finality_proof {
570 finality_target: Box::new(test_header(42)),
571 justification: make_default_justification(&test_header(42)),
572 });
573 assert_eq!(
574 deprecated_call.submit_finality_proof_info(),
575 Some(SubmitFinalityProofInfo {
576 block_number: 42,
577 current_set_id: None,
578 extra_weight: Weight::zero(),
579 extra_size: 0,
580 is_mandatory: false,
581 is_free_execution_expected: false,
582 })
583 );
584
585 let deprecated_call =
587 RuntimeCall::Grandpa(crate::Call::<TestRuntime, ()>::submit_finality_proof_ex {
588 finality_target: Box::new(test_header(42)),
589 justification: make_default_justification(&test_header(42)),
590 current_set_id: 777,
591 is_free_execution_expected: false,
592 });
593 assert_eq!(
594 deprecated_call.submit_finality_proof_info(),
595 Some(SubmitFinalityProofInfo {
596 block_number: 42,
597 current_set_id: Some(777),
598 extra_weight: Weight::zero(),
599 extra_size: 0,
600 is_mandatory: false,
601 is_free_execution_expected: false,
602 })
603 );
604 }
605
606 #[test]
607 fn extension_returns_correct_extra_size_if_call_arguments_are_too_large() {
608 let small_finality_target = test_header(1);
610 let justification_params = JustificationGeneratorParams {
611 header: small_finality_target.clone(),
612 ..Default::default()
613 };
614 let small_justification = make_justification_for_header(justification_params);
615 let small_call = RuntimeCall::Grandpa(crate::Call::submit_finality_proof_ex {
616 finality_target: Box::new(small_finality_target),
617 justification: small_justification,
618 current_set_id: TEST_GRANDPA_SET_ID,
619 is_free_execution_expected: false,
620 });
621 assert_eq!(small_call.submit_finality_proof_info().unwrap().extra_size, 0);
622
623 let mut large_finality_target = test_header(1);
625 large_finality_target
626 .digest_mut()
627 .push(DigestItem::Other(vec![42u8; 1024 * 1024]));
628 let justification_params = JustificationGeneratorParams {
629 header: large_finality_target.clone(),
630 ..Default::default()
631 };
632 let large_justification = make_justification_for_header(justification_params);
633 let large_call = RuntimeCall::Grandpa(crate::Call::submit_finality_proof_ex {
634 finality_target: Box::new(large_finality_target),
635 justification: large_justification,
636 current_set_id: TEST_GRANDPA_SET_ID,
637 is_free_execution_expected: false,
638 });
639 assert_ne!(large_call.submit_finality_proof_info().unwrap().extra_size, 0);
640 }
641
642 #[test]
643 fn extension_returns_correct_extra_weight_if_there_are_too_many_headers_in_votes_ancestry() {
644 let finality_target = test_header(1);
645 let mut justification_params = JustificationGeneratorParams {
646 header: finality_target.clone(),
647 ancestors: TestBridgedChain::REASONABLE_HEADERS_IN_JUSTIFICATION_ANCESTRY,
648 ..Default::default()
649 };
650
651 let justification = make_justification_for_header(justification_params.clone());
653 let call = RuntimeCall::Grandpa(crate::Call::submit_finality_proof_ex {
654 finality_target: Box::new(finality_target.clone()),
655 justification,
656 current_set_id: TEST_GRANDPA_SET_ID,
657 is_free_execution_expected: false,
658 });
659 assert_eq!(call.submit_finality_proof_info().unwrap().extra_weight, Weight::zero());
660
661 justification_params.ancestors += 1;
663 let justification = make_justification_for_header(justification_params);
664 let call_weight = <TestRuntime as Config>::WeightInfo::submit_finality_proof(
665 justification.commit.precommits.len().saturated_into(),
666 justification.votes_ancestries.len().saturated_into(),
667 );
668 let call = RuntimeCall::Grandpa(crate::Call::submit_finality_proof_ex {
669 finality_target: Box::new(finality_target),
670 justification,
671 current_set_id: TEST_GRANDPA_SET_ID,
672 is_free_execution_expected: false,
673 });
674 assert_eq!(call.submit_finality_proof_info().unwrap().extra_weight, call_weight);
675 }
676
677 #[test]
678 fn check_obsolete_submit_finality_proof_returns_correct_improved_by() {
679 run_test(|| {
680 fn make_call(number: u64) -> RuntimeCall {
681 RuntimeCall::Grandpa(crate::Call::<TestRuntime, ()>::submit_finality_proof_ex {
682 finality_target: Box::new(test_header(number)),
683 justification: make_default_justification(&test_header(number)),
684 current_set_id: 0,
685 is_free_execution_expected: false,
686 })
687 }
688
689 sync_to_header_10();
690
691 assert_eq!(
693 RuntimeCall::check_obsolete_submit_finality_proof(&make_call(11))
694 .unwrap()
695 .unwrap()
696 .improved_by,
697 1,
698 );
699
700 assert_eq!(
702 RuntimeCall::check_obsolete_submit_finality_proof(&make_call(12))
703 .unwrap()
704 .unwrap()
705 .improved_by,
706 2,
707 );
708 })
709 }
710
711 #[test]
712 fn check_obsolete_submit_finality_proof_ignores_other_calls() {
713 run_test(|| {
714 let call =
715 RuntimeCall::System(frame_system::Call::<TestRuntime>::remark { remark: vec![42] });
716
717 assert_eq!(RuntimeCall::check_obsolete_submit_finality_proof(&call), Ok(None));
718 })
719 }
720}