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 core::fmt::Debug;
27use frame_support::{
28 dispatch::CallableCallFor,
29 traits::{Get, IsSubType},
30 weights::Weight,
31};
32use sp_consensus_grandpa::SetId;
33use sp_runtime::{
34 traits::{CheckedSub, Header, Zero},
35 transaction_validity::{InvalidTransaction, TransactionValidityError},
36 SaturatedConversion,
37};
38
39#[derive(Copy, Clone, PartialEq, Debug)]
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 },
254 Err(Error::<T, I>::OldHeader) => Err(InvalidTransaction::Stale.into()),
255 Err(_) => Err(InvalidTransaction::Call.into()),
256 }
257 }
258}
259
260impl<T: Config<I>, I: 'static> CallSubType<T, I> for T::RuntimeCall where
261 T::RuntimeCall: IsSubType<CallableCallFor<Pallet<T, I>, T>>
262{
263}
264
265pub(crate) fn submit_finality_proof_info_from_args<T: Config<I>, I: 'static>(
267 finality_target: &BridgedHeader<T, I>,
268 justification: &GrandpaJustification<BridgedHeader<T, I>>,
269 current_set_id: Option<SetId>,
270 is_free_execution_expected: bool,
271) -> SubmitFinalityProofInfo<BridgedBlockNumber<T, I>> {
272 let extras =
275 submit_finality_proof_limits_extras::<T::BridgedChain>(finality_target, justification);
276
277 let extra_weight = if extras.is_weight_limit_exceeded {
283 let precommits_len = justification.commit.precommits.len().saturated_into();
284 let votes_ancestries_len = justification.votes_ancestries.len().saturated_into();
285 T::WeightInfo::submit_finality_proof(precommits_len, votes_ancestries_len)
286 } else {
287 Weight::zero()
288 };
289
290 SubmitFinalityProofInfo {
291 block_number: *finality_target.number(),
292 current_set_id,
293 is_mandatory: extras.is_mandatory_finality_target,
294 is_free_execution_expected,
295 extra_weight,
296 extra_size: extras.extra_size,
297 }
298}
299
300#[cfg(test)]
301mod tests {
302 use crate::{
303 call_ext::CallSubType,
304 mock::{
305 run_test, test_header, FreeHeadersInterval, RuntimeCall, TestBridgedChain, TestNumber,
306 TestRuntime,
307 },
308 BestFinalized, Config, CurrentAuthoritySet, FreeHeadersRemaining, PalletOperatingMode,
309 StoredAuthoritySet, WeightInfo,
310 };
311 use bp_header_chain::{ChainWithGrandpa, SubmitFinalityProofInfo};
312 use bp_runtime::{BasicOperatingMode, HeaderId};
313 use bp_test_utils::{
314 make_default_justification, make_justification_for_header, JustificationGeneratorParams,
315 TEST_GRANDPA_SET_ID,
316 };
317 use codec::Encode;
318 use frame_support::weights::Weight;
319 use sp_runtime::{testing::DigestItem, traits::Header as _, SaturatedConversion};
320
321 fn validate_block_submit(num: TestNumber) -> bool {
322 let bridge_grandpa_call = crate::Call::<TestRuntime, ()>::submit_finality_proof_ex {
323 finality_target: Box::new(test_header(num)),
324 justification: make_default_justification(&test_header(num)),
325 current_set_id: 0,
327 is_free_execution_expected: false,
328 };
329 RuntimeCall::check_obsolete_submit_finality_proof(&RuntimeCall::Grandpa(
330 bridge_grandpa_call,
331 ))
332 .is_ok()
333 }
334
335 fn sync_to_header_10() {
336 let header10_hash = sp_core::H256::default();
337 BestFinalized::<TestRuntime, ()>::put(HeaderId(10, header10_hash));
338 }
339
340 #[test]
341 fn extension_rejects_obsolete_header() {
342 run_test(|| {
343 sync_to_header_10();
346 assert!(!validate_block_submit(5));
347 });
348 }
349
350 #[test]
351 fn extension_rejects_same_header() {
352 run_test(|| {
353 sync_to_header_10();
356 assert!(!validate_block_submit(10));
357 });
358 }
359
360 #[test]
361 fn extension_rejects_new_header_if_pallet_is_halted() {
362 run_test(|| {
363 sync_to_header_10();
365 PalletOperatingMode::<TestRuntime, ()>::put(BasicOperatingMode::Halted);
366
367 assert!(!validate_block_submit(15));
368 });
369 }
370
371 #[test]
372 fn extension_rejects_new_header_if_set_id_is_invalid() {
373 run_test(|| {
374 sync_to_header_10();
376 let next_set = StoredAuthoritySet::<TestRuntime, ()>::try_new(vec![], 0x42).unwrap();
377 CurrentAuthoritySet::<TestRuntime, ()>::put(next_set);
378
379 assert!(!validate_block_submit(15));
380 });
381 }
382
383 #[test]
384 fn extension_rejects_new_header_if_free_execution_is_requested_and_free_submissions_are_not_accepted(
385 ) {
386 run_test(|| {
387 let bridge_grandpa_call = crate::Call::<TestRuntime, ()>::submit_finality_proof_ex {
388 finality_target: Box::new(test_header(10 + FreeHeadersInterval::get() as u64)),
389 justification: make_default_justification(&test_header(
390 10 + FreeHeadersInterval::get() as u64,
391 )),
392 current_set_id: 0,
393 is_free_execution_expected: true,
394 };
395 sync_to_header_10();
396
397 FreeHeadersRemaining::<TestRuntime, ()>::put(2);
399 assert!(RuntimeCall::check_obsolete_submit_finality_proof(&RuntimeCall::Grandpa(
400 bridge_grandpa_call.clone(),
401 ),)
402 .is_ok());
403
404 FreeHeadersRemaining::<TestRuntime, ()>::put(0);
406 assert!(RuntimeCall::check_obsolete_submit_finality_proof(&RuntimeCall::Grandpa(
407 bridge_grandpa_call.clone(),
408 ),)
409 .is_err());
410
411 FreeHeadersRemaining::<TestRuntime, ()>::kill();
413 assert!(RuntimeCall::check_obsolete_submit_finality_proof(&RuntimeCall::Grandpa(
414 bridge_grandpa_call,
415 ),)
416 .is_ok());
417 })
418 }
419
420 #[test]
421 fn extension_rejects_new_header_if_it_overflow_size_limits() {
422 run_test(|| {
423 let mut large_finality_target = test_header(10 + FreeHeadersInterval::get() as u64);
424 large_finality_target
425 .digest_mut()
426 .push(DigestItem::Other(vec![42u8; 1024 * 1024]));
427 let justification_params = JustificationGeneratorParams {
428 header: large_finality_target.clone(),
429 ..Default::default()
430 };
431 let large_justification = make_justification_for_header(justification_params);
432
433 let bridge_grandpa_call = crate::Call::<TestRuntime, ()>::submit_finality_proof_ex {
434 finality_target: Box::new(large_finality_target),
435 justification: large_justification,
436 current_set_id: 0,
437 is_free_execution_expected: true,
438 };
439 sync_to_header_10();
440
441 FreeHeadersRemaining::<TestRuntime, ()>::put(2);
443 assert!(RuntimeCall::check_obsolete_submit_finality_proof(&RuntimeCall::Grandpa(
444 bridge_grandpa_call.clone(),
445 ),)
446 .is_err());
447 })
448 }
449
450 #[test]
451 fn extension_rejects_new_header_if_it_overflow_weight_limits() {
452 run_test(|| {
453 let finality_target = test_header(10 + FreeHeadersInterval::get() as u64);
454 let justification_params = JustificationGeneratorParams {
455 header: finality_target.clone(),
456 ancestors: TestBridgedChain::REASONABLE_HEADERS_IN_JUSTIFICATION_ANCESTRY,
457 ..Default::default()
458 };
459 let justification = make_justification_for_header(justification_params);
460
461 let bridge_grandpa_call = crate::Call::<TestRuntime, ()>::submit_finality_proof_ex {
462 finality_target: Box::new(finality_target),
463 justification,
464 current_set_id: 0,
465 is_free_execution_expected: true,
466 };
467 sync_to_header_10();
468
469 FreeHeadersRemaining::<TestRuntime, ()>::put(2);
471 assert!(RuntimeCall::check_obsolete_submit_finality_proof(&RuntimeCall::Grandpa(
472 bridge_grandpa_call.clone(),
473 ),)
474 .is_err());
475 })
476 }
477
478 #[test]
479 fn extension_rejects_new_header_if_free_execution_is_requested_and_improved_by_is_below_expected(
480 ) {
481 run_test(|| {
482 let bridge_grandpa_call = crate::Call::<TestRuntime, ()>::submit_finality_proof_ex {
483 finality_target: Box::new(test_header(100)),
484 justification: make_default_justification(&test_header(100)),
485 current_set_id: 0,
486 is_free_execution_expected: true,
487 };
488 sync_to_header_10();
489
490 BestFinalized::<TestRuntime, ()>::put(HeaderId(
492 100 - FreeHeadersInterval::get() as u64 + 1,
493 sp_core::H256::default(),
494 ));
495 assert!(RuntimeCall::check_obsolete_submit_finality_proof(&RuntimeCall::Grandpa(
496 bridge_grandpa_call.clone(),
497 ),)
498 .is_err());
499
500 BestFinalized::<TestRuntime, ()>::put(HeaderId(
502 100 - FreeHeadersInterval::get() as u64,
503 sp_core::H256::default(),
504 ));
505 assert!(RuntimeCall::check_obsolete_submit_finality_proof(&RuntimeCall::Grandpa(
506 bridge_grandpa_call.clone(),
507 ),)
508 .is_ok());
509
510 BestFinalized::<TestRuntime, ()>::put(HeaderId(
512 100 - FreeHeadersInterval::get() as u64 - 1,
513 sp_core::H256::default(),
514 ));
515 assert!(RuntimeCall::check_obsolete_submit_finality_proof(&RuntimeCall::Grandpa(
516 bridge_grandpa_call.clone(),
517 ),)
518 .is_ok());
519
520 let mut mandatory_header = test_header(100);
522 let consensus_log = sp_consensus_grandpa::ConsensusLog::<TestNumber>::ScheduledChange(
523 sp_consensus_grandpa::ScheduledChange {
524 next_authorities: bp_test_utils::authority_list(),
525 delay: 0,
526 },
527 );
528 mandatory_header.digest = sp_runtime::Digest {
529 logs: vec![DigestItem::Consensus(
530 sp_consensus_grandpa::GRANDPA_ENGINE_ID,
531 consensus_log.encode(),
532 )],
533 };
534 let justification = make_justification_for_header(JustificationGeneratorParams {
535 header: mandatory_header.clone(),
536 set_id: 1,
537 ..Default::default()
538 });
539 let bridge_grandpa_call = crate::Call::<TestRuntime, ()>::submit_finality_proof_ex {
540 finality_target: Box::new(mandatory_header),
541 justification,
542 current_set_id: 0,
543 is_free_execution_expected: true,
544 };
545 BestFinalized::<TestRuntime, ()>::put(HeaderId(
546 100 - FreeHeadersInterval::get() as u64 + 1,
547 sp_core::H256::default(),
548 ));
549 assert!(RuntimeCall::check_obsolete_submit_finality_proof(&RuntimeCall::Grandpa(
550 bridge_grandpa_call.clone(),
551 ),)
552 .is_ok());
553 })
554 }
555
556 #[test]
557 fn extension_accepts_new_header() {
558 run_test(|| {
559 sync_to_header_10();
562 assert!(validate_block_submit(15));
563 });
564 }
565
566 #[test]
567 fn submit_finality_proof_info_is_parsed() {
568 let deprecated_call =
570 RuntimeCall::Grandpa(crate::Call::<TestRuntime, ()>::submit_finality_proof {
571 finality_target: Box::new(test_header(42)),
572 justification: make_default_justification(&test_header(42)),
573 });
574 assert_eq!(
575 deprecated_call.submit_finality_proof_info(),
576 Some(SubmitFinalityProofInfo {
577 block_number: 42,
578 current_set_id: None,
579 extra_weight: Weight::zero(),
580 extra_size: 0,
581 is_mandatory: false,
582 is_free_execution_expected: false,
583 })
584 );
585
586 let deprecated_call =
588 RuntimeCall::Grandpa(crate::Call::<TestRuntime, ()>::submit_finality_proof_ex {
589 finality_target: Box::new(test_header(42)),
590 justification: make_default_justification(&test_header(42)),
591 current_set_id: 777,
592 is_free_execution_expected: false,
593 });
594 assert_eq!(
595 deprecated_call.submit_finality_proof_info(),
596 Some(SubmitFinalityProofInfo {
597 block_number: 42,
598 current_set_id: Some(777),
599 extra_weight: Weight::zero(),
600 extra_size: 0,
601 is_mandatory: false,
602 is_free_execution_expected: false,
603 })
604 );
605 }
606
607 #[test]
608 fn extension_returns_correct_extra_size_if_call_arguments_are_too_large() {
609 let small_finality_target = test_header(1);
611 let justification_params = JustificationGeneratorParams {
612 header: small_finality_target.clone(),
613 ..Default::default()
614 };
615 let small_justification = make_justification_for_header(justification_params);
616 let small_call = RuntimeCall::Grandpa(crate::Call::submit_finality_proof_ex {
617 finality_target: Box::new(small_finality_target),
618 justification: small_justification,
619 current_set_id: TEST_GRANDPA_SET_ID,
620 is_free_execution_expected: false,
621 });
622 assert_eq!(small_call.submit_finality_proof_info().unwrap().extra_size, 0);
623
624 let mut large_finality_target = test_header(1);
626 large_finality_target
627 .digest_mut()
628 .push(DigestItem::Other(vec![42u8; 1024 * 1024]));
629 let justification_params = JustificationGeneratorParams {
630 header: large_finality_target.clone(),
631 ..Default::default()
632 };
633 let large_justification = make_justification_for_header(justification_params);
634 let large_call = RuntimeCall::Grandpa(crate::Call::submit_finality_proof_ex {
635 finality_target: Box::new(large_finality_target),
636 justification: large_justification,
637 current_set_id: TEST_GRANDPA_SET_ID,
638 is_free_execution_expected: false,
639 });
640 assert_ne!(large_call.submit_finality_proof_info().unwrap().extra_size, 0);
641 }
642
643 #[test]
644 fn extension_returns_correct_extra_weight_if_there_are_too_many_headers_in_votes_ancestry() {
645 let finality_target = test_header(1);
646 let mut justification_params = JustificationGeneratorParams {
647 header: finality_target.clone(),
648 ancestors: TestBridgedChain::REASONABLE_HEADERS_IN_JUSTIFICATION_ANCESTRY,
649 ..Default::default()
650 };
651
652 let justification = make_justification_for_header(justification_params.clone());
654 let call = RuntimeCall::Grandpa(crate::Call::submit_finality_proof_ex {
655 finality_target: Box::new(finality_target.clone()),
656 justification,
657 current_set_id: TEST_GRANDPA_SET_ID,
658 is_free_execution_expected: false,
659 });
660 assert_eq!(call.submit_finality_proof_info().unwrap().extra_weight, Weight::zero());
661
662 justification_params.ancestors += 1;
664 let justification = make_justification_for_header(justification_params);
665 let call_weight = <TestRuntime as Config>::WeightInfo::submit_finality_proof(
666 justification.commit.precommits.len().saturated_into(),
667 justification.votes_ancestries.len().saturated_into(),
668 );
669 let call = RuntimeCall::Grandpa(crate::Call::submit_finality_proof_ex {
670 finality_target: Box::new(finality_target),
671 justification,
672 current_set_id: TEST_GRANDPA_SET_ID,
673 is_free_execution_expected: false,
674 });
675 assert_eq!(call.submit_finality_proof_info().unwrap().extra_weight, call_weight);
676 }
677
678 #[test]
679 fn check_obsolete_submit_finality_proof_returns_correct_improved_by() {
680 run_test(|| {
681 fn make_call(number: u64) -> RuntimeCall {
682 RuntimeCall::Grandpa(crate::Call::<TestRuntime, ()>::submit_finality_proof_ex {
683 finality_target: Box::new(test_header(number)),
684 justification: make_default_justification(&test_header(number)),
685 current_set_id: 0,
686 is_free_execution_expected: false,
687 })
688 }
689
690 sync_to_header_10();
691
692 assert_eq!(
694 RuntimeCall::check_obsolete_submit_finality_proof(&make_call(11))
695 .unwrap()
696 .unwrap()
697 .improved_by,
698 1,
699 );
700
701 assert_eq!(
703 RuntimeCall::check_obsolete_submit_finality_proof(&make_call(12))
704 .unwrap()
705 .unwrap()
706 .improved_by,
707 2,
708 );
709 })
710 }
711
712 #[test]
713 fn check_obsolete_submit_finality_proof_ignores_other_calls() {
714 run_test(|| {
715 let call =
716 RuntimeCall::System(frame_system::Call::<TestRuntime>::remark { remark: vec![42] });
717
718 assert_eq!(RuntimeCall::check_obsolete_submit_finality_proof(&call), Ok(None));
719 })
720 }
721}