pallet_election_provider_multi_block/unsigned/
mod.rs1pub use crate::weights::traits::pallet_election_provider_multi_block_unsigned::*;
74pub use pallet::*;
76#[cfg(feature = "runtime-benchmarks")]
77mod benchmarking;
78
79pub mod miner;
81
82#[frame_support::pallet]
83mod pallet {
84 use super::WeightInfo;
85 use crate::{
86 types::*,
87 unsigned::miner::{self},
88 verifier::Verifier,
89 CommonError,
90 };
91 use frame_support::pallet_prelude::*;
92 use frame_system::{offchain::CreateBare, pallet_prelude::*};
93 use sp_runtime::traits::SaturatedConversion;
94 use sp_std::prelude::*;
95
96 fn base_error_to_invalid(error: CommonError) -> InvalidTransaction {
99 let index = error.encode().pop().unwrap_or(0);
100 InvalidTransaction::Custom(index)
101 }
102
103 pub(crate) type UnsignedWeightsOf<T> = <T as Config>::WeightInfo;
104
105 #[pallet::config]
106 #[pallet::disable_frame_system_supertrait_check]
107 pub trait Config: crate::Config + CreateBare<Call<Self>> {
108 type OffchainRepeat: Get<BlockNumberFor<Self>>;
113
114 type OffchainSolver: frame_election_provider_support::NposSolver<
116 AccountId = Self::AccountId,
117 >;
118
119 type OffchainStorage: Get<bool>;
123
124 type MinerTxPriority: Get<TransactionPriority>;
126
127 type MinerPages: Get<PageIndex>;
129
130 type WeightInfo: WeightInfo;
132 }
133
134 #[pallet::pallet]
135 pub struct Pallet<T>(PhantomData<T>);
136
137 #[pallet::call]
138 impl<T: Config> Pallet<T> {
139 #[pallet::weight((UnsignedWeightsOf::<T>::submit_unsigned(), DispatchClass::Operational))]
155 #[pallet::call_index(0)]
156 pub fn submit_unsigned(
157 origin: OriginFor<T>,
158 paged_solution: Box<PagedRawSolution<T::MinerConfig>>,
159 ) -> DispatchResultWithPostInfo {
160 ensure_none(origin)?;
161 let error_message = "Invalid unsigned submission must produce invalid block and \
162 deprive validator from their authoring reward.";
163
164 debug_assert!(Self::validate_unsigned_checks(&paged_solution).is_ok());
167
168 let claimed_score = paged_solution.score;
169
170 let page_indices = crate::Pallet::<T>::msp_range_for(T::MinerPages::get() as usize);
172 <T::Verifier as Verifier>::verify_synchronous_multi(
173 paged_solution.solution_pages,
174 page_indices,
175 claimed_score,
176 )
177 .expect(error_message);
178
179 Ok(None.into())
180 }
181 }
182
183 #[pallet::validate_unsigned]
184 impl<T: Config> ValidateUnsigned for Pallet<T> {
185 type Call = Call<T>;
186 fn validate_unsigned(source: TransactionSource, call: &Self::Call) -> TransactionValidity {
187 if let Call::submit_unsigned { paged_solution, .. } = call {
188 match source {
189 TransactionSource::Local | TransactionSource::InBlock => { },
190 _ => return InvalidTransaction::Call.into(),
191 }
192
193 let _ = Self::validate_unsigned_checks(paged_solution.as_ref())
194 .map_err(|err| {
195 sublog!(
196 debug,
197 "unsigned",
198 "unsigned transaction validation failed due to {:?}",
199 err
200 );
201 err
202 })
203 .map_err(base_error_to_invalid)?;
204
205 ValidTransaction::with_tag_prefix("OffchainElection")
206 .priority(
208 T::MinerTxPriority::get()
209 .saturating_add(paged_solution.score.minimal_stake.saturated_into()),
210 )
211 .and_provides(paged_solution.round)
214 .longevity(T::UnsignedPhase::get().saturated_into::<u64>())
216 .propagate(false)
218 .build()
219 } else {
220 InvalidTransaction::Call.into()
221 }
222 }
223
224 fn pre_dispatch(call: &Self::Call) -> Result<(), TransactionValidityError> {
225 if let Call::submit_unsigned { paged_solution, .. } = call {
226 Self::validate_unsigned_checks(paged_solution.as_ref())
227 .map_err(base_error_to_invalid)
228 .map_err(Into::into)
229 } else {
230 Err(InvalidTransaction::Call.into())
231 }
232 }
233 }
234
235 #[pallet::hooks]
236 impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
237 fn integrity_test() {
238 assert!(
239 UnsignedWeightsOf::<T>::submit_unsigned().all_lte(T::BlockWeights::get().max_block),
240 "weight of `submit_unsigned` is too high"
241 );
242 assert!(
243 <T as Config>::MinerPages::get() as usize <=
244 <T as crate::Config>::Pages::get() as usize,
245 "number of pages in the unsigned phase is too high"
246 );
247 }
248
249 #[cfg(feature = "try-runtime")]
250 fn try_state(now: BlockNumberFor<T>) -> Result<(), sp_runtime::TryRuntimeError> {
251 Self::do_try_state(now)
252 }
253
254 fn offchain_worker(now: BlockNumberFor<T>) {
255 use sp_runtime::offchain::storage_lock::{BlockAndTime, StorageLock};
256
257 let mut lock =
261 StorageLock::<BlockAndTime<frame_system::Pallet<T>>>::with_block_deadline(
262 miner::OffchainWorkerMiner::<T>::OFFCHAIN_LOCK,
263 T::UnsignedPhase::get().saturated_into(),
264 );
265
266 match lock.try_lock() {
267 Ok(_guard) => {
268 Self::do_synchronized_offchain_worker(now);
269 },
270 Err(deadline) => {
271 sublog!(
272 trace,
273 "unsigned",
274 "offchain worker lock not released, deadline is {:?}",
275 deadline
276 );
277 },
278 };
279 }
280 }
281
282 impl<T: Config> Pallet<T> {
283 fn do_synchronized_offchain_worker(now: BlockNumberFor<T>) {
286 use miner::OffchainWorkerMiner;
287 let current_phase = crate::Pallet::<T>::current_phase();
288 sublog!(
289 trace,
290 "unsigned",
291 "lock for offchain worker acquired. Phase = {:?}",
292 current_phase
293 );
294
295 if current_phase.is_unsigned() {
297 if let Err(reason) = OffchainWorkerMiner::<T>::ensure_offchain_repeat_frequency(now)
298 {
299 sublog!(
300 debug,
301 "unsigned",
302 "offchain worker repeat frequency check failed: {:?}",
303 reason
304 );
305 return;
306 }
307 }
308
309 if current_phase.is_unsigned_opened_now() {
310 let initial_output = if T::OffchainStorage::get() {
312 OffchainWorkerMiner::<T>::mine_check_maybe_save_submit(true)
313 } else {
314 OffchainWorkerMiner::<T>::mine_check_maybe_save_submit(false)
315 };
316 sublog!(debug, "unsigned", "initial offchain worker output: {:?}", initial_output);
317 } else if current_phase.is_unsigned() {
318 let resubmit_output = if T::OffchainStorage::get() {
320 OffchainWorkerMiner::<T>::restore_or_compute_then_maybe_submit()
321 } else {
322 OffchainWorkerMiner::<T>::mine_check_maybe_save_submit(false)
323 };
324 sublog!(debug, "unsigned", "later offchain worker output: {:?}", resubmit_output);
325 };
326 }
327
328 pub(crate) fn validate_unsigned_checks(
334 paged_solution: &PagedRawSolution<T::MinerConfig>,
335 ) -> Result<(), CommonError> {
336 Self::unsigned_specific_checks(paged_solution)
337 .and(crate::Pallet::<T>::snapshot_independent_checks(paged_solution, None))
338 .map_err(Into::into)
339 }
340
341 pub fn unsigned_specific_checks(
345 paged_solution: &PagedRawSolution<T::MinerConfig>,
346 ) -> Result<(), CommonError> {
347 ensure!(
348 crate::Pallet::<T>::current_phase().is_unsigned(),
349 CommonError::EarlySubmission
350 );
351 ensure!(
352 paged_solution.solution_pages.len() == T::MinerPages::get() as usize,
353 CommonError::WrongPageCount
354 );
355 ensure!(
356 paged_solution.solution_pages.len() <= <T as crate::Config>::Pages::get() as usize,
357 CommonError::WrongPageCount
358 );
359
360 Ok(())
361 }
362
363 #[cfg(any(test, feature = "runtime-benchmarks", feature = "try-runtime"))]
364 pub(crate) fn do_try_state(
365 _now: BlockNumberFor<T>,
366 ) -> Result<(), sp_runtime::TryRuntimeError> {
367 Ok(())
368 }
369 }
370}
371
372#[cfg(test)]
373mod validate_unsigned {
374 use frame_election_provider_support::Support;
375 use frame_support::{
376 pallet_prelude::InvalidTransaction,
377 unsigned::{TransactionSource, TransactionValidityError, ValidateUnsigned},
378 };
379
380 use super::Call;
381 use crate::{mock::*, types::*, verifier::Verifier};
382
383 #[test]
384 fn retracts_weak_score_accepts_threshold_better() {
385 ExtBuilder::unsigned()
386 .solution_improvement_threshold(sp_runtime::Perbill::from_percent(10))
387 .build_and_execute(|| {
388 roll_to_snapshot_created();
389
390 let solution = mine_full_solution().unwrap();
391 load_mock_signed_and_start(solution.clone());
392 roll_to_full_verification();
393
394 assert_eq!(
396 <VerifierPallet as Verifier>::queued_score(),
397 Some(ElectionScore {
398 minimal_stake: 55,
399 sum_stake: 130,
400 sum_stake_squared: 8650
401 })
402 );
403
404 roll_to_unsigned_open();
405
406 let attempt =
408 fake_solution(ElectionScore { minimal_stake: 20, ..Default::default() });
409 let call = Call::submit_unsigned { paged_solution: Box::new(attempt) };
410 assert_eq!(
411 UnsignedPallet::validate_unsigned(TransactionSource::Local, &call).unwrap_err(),
412 TransactionValidityError::Invalid(InvalidTransaction::Custom(2)),
413 );
414
415 let insufficient_improvement = 55 * 105 / 100;
417 let attempt = fake_solution(ElectionScore {
418 minimal_stake: insufficient_improvement,
419 ..Default::default()
420 });
421 let call = Call::submit_unsigned { paged_solution: Box::new(attempt) };
422 assert_eq!(
423 UnsignedPallet::validate_unsigned(TransactionSource::Local, &call).unwrap_err(),
424 TransactionValidityError::Invalid(InvalidTransaction::Custom(2)),
425 );
426
427 let mut paged = raw_paged_from_supports(
430 vec![vec![
431 (40, Support { total: 10, voters: vec![(3, 5)] }),
432 (30, Support { total: 10, voters: vec![(3, 5)] }),
433 ]],
434 0,
435 );
436 let sufficient_improvement = 55 * 115 / 100;
437 paged.score =
438 ElectionScore { minimal_stake: sufficient_improvement, ..Default::default() };
439 let call = Call::submit_unsigned { paged_solution: Box::new(paged) };
440 assert!(UnsignedPallet::validate_unsigned(TransactionSource::Local, &call).is_ok());
441 })
442 }
443
444 #[test]
445 fn retracts_wrong_round() {
446 ExtBuilder::unsigned().build_and_execute(|| {
447 roll_to_unsigned_open();
448
449 let mut attempt =
450 fake_solution(ElectionScore { minimal_stake: 5, ..Default::default() });
451 attempt.round += 1;
452 let call = Call::submit_unsigned { paged_solution: Box::new(attempt) };
453
454 assert_eq!(
455 UnsignedPallet::validate_unsigned(TransactionSource::Local, &call).unwrap_err(),
456 TransactionValidityError::Invalid(InvalidTransaction::Custom(1)),
458 );
459 })
460 }
461
462 #[test]
463 fn retracts_too_many_pages_unsigned() {
464 ExtBuilder::unsigned().build_and_execute(|| {
465 roll_to_unsigned_open();
468 let attempt = mine_full_solution().unwrap();
469 let call = Call::submit_unsigned { paged_solution: Box::new(attempt) };
470
471 assert_eq!(
472 UnsignedPallet::validate_unsigned(TransactionSource::Local, &call).unwrap_err(),
473 TransactionValidityError::Invalid(InvalidTransaction::Custom(3)),
475 );
476
477 let attempt = mine_solution(2).unwrap();
478 let call = Call::submit_unsigned { paged_solution: Box::new(attempt) };
479
480 assert_eq!(
481 UnsignedPallet::validate_unsigned(TransactionSource::Local, &call).unwrap_err(),
482 TransactionValidityError::Invalid(InvalidTransaction::Custom(3)),
483 );
484
485 let attempt = mine_solution(1).unwrap();
486 let call = Call::submit_unsigned { paged_solution: Box::new(attempt) };
487
488 assert!(UnsignedPallet::validate_unsigned(TransactionSource::Local, &call).is_ok(),);
489 })
490 }
491
492 #[test]
493 fn retracts_wrong_winner_count() {
494 ExtBuilder::unsigned().desired_targets(2).build_and_execute(|| {
495 roll_to_unsigned_open();
496
497 let paged = raw_paged_from_supports(
498 vec![vec![(40, Support { total: 10, voters: vec![(3, 10)] })]],
499 0,
500 );
501
502 let call = Call::submit_unsigned { paged_solution: Box::new(paged) };
503
504 assert_eq!(
505 UnsignedPallet::validate_unsigned(TransactionSource::Local, &call).unwrap_err(),
506 TransactionValidityError::Invalid(InvalidTransaction::Custom(4)),
508 );
509 });
510 }
511
512 #[test]
513 fn retracts_wrong_phase() {
514 ExtBuilder::unsigned().signed_phase(5, 6).build_and_execute(|| {
515 let solution = raw_paged_solution_low_score();
516 let call = Call::submit_unsigned { paged_solution: Box::new(solution.clone()) };
517
518 assert_eq!(MultiBlock::current_phase(), Phase::Off);
520 assert!(matches!(
521 <UnsignedPallet as ValidateUnsigned>::validate_unsigned(
522 TransactionSource::Local,
523 &call
524 )
525 .unwrap_err(),
526 TransactionValidityError::Invalid(InvalidTransaction::Custom(0))
528 ));
529 assert!(matches!(
530 <UnsignedPallet as ValidateUnsigned>::pre_dispatch(&call).unwrap_err(),
531 TransactionValidityError::Invalid(InvalidTransaction::Custom(0))
532 ));
533
534 roll_to_signed_open();
536 assert!(MultiBlock::current_phase().is_signed());
537 assert!(matches!(
538 <UnsignedPallet as ValidateUnsigned>::validate_unsigned(
539 TransactionSource::Local,
540 &call
541 )
542 .unwrap_err(),
543 TransactionValidityError::Invalid(InvalidTransaction::Custom(0))
544 ));
545 assert!(matches!(
546 <UnsignedPallet as ValidateUnsigned>::pre_dispatch(&call).unwrap_err(),
547 TransactionValidityError::Invalid(InvalidTransaction::Custom(0))
548 ));
549
550 roll_to_unsigned_open();
552 assert!(MultiBlock::current_phase().is_unsigned());
553
554 assert_ok!(<UnsignedPallet as ValidateUnsigned>::validate_unsigned(
555 TransactionSource::Local,
556 &call
557 ));
558 assert_ok!(<UnsignedPallet as ValidateUnsigned>::pre_dispatch(&call));
559 })
560 }
561
562 #[test]
563 fn priority_is_set() {
564 ExtBuilder::unsigned()
565 .miner_tx_priority(20)
566 .desired_targets(0)
567 .build_and_execute(|| {
568 roll_to_unsigned_open();
569 assert!(MultiBlock::current_phase().is_unsigned());
570
571 let solution =
572 fake_solution(ElectionScore { minimal_stake: 5, ..Default::default() });
573 let call = Call::submit_unsigned { paged_solution: Box::new(solution.clone()) };
574
575 assert_eq!(
576 <UnsignedPallet as ValidateUnsigned>::validate_unsigned(
577 TransactionSource::Local,
578 &call
579 )
580 .unwrap()
581 .priority,
582 25
583 );
584 })
585 }
586}
587
588#[cfg(test)]
589mod call {
590 use crate::{mock::*, verifier::Verifier, Snapshot};
591
592 #[test]
593 fn unsigned_submission_e2e() {
594 let (mut ext, pool) = ExtBuilder::unsigned().build_offchainify();
595 ext.execute_with_sanity_checks(|| {
596 roll_to_unsigned_open();
597
598 assert_full_snapshot();
600 assert_eq!(pool.read().transactions.len(), 0);
602 assert_eq!(<VerifierPallet as Verifier>::queued_score(), None);
604
605 roll_next_with_ocw(Some(pool.clone()));
607 assert_eq!(pool.read().transactions.len(), 1);
608 assert_eq!(<VerifierPallet as Verifier>::queued_score(), None);
609
610 roll_next_with_ocw(Some(pool.clone()));
612 assert_eq!(pool.read().transactions.len(), 0);
613 assert!(matches!(<VerifierPallet as Verifier>::queued_score(), Some(_)));
614 })
615 }
616
617 #[test]
618 #[should_panic(
619 expected = "Invalid unsigned submission must produce invalid block and deprive validator from their authoring reward."
620 )]
621 fn unfeasible_solution_panics() {
622 let (mut ext, pool) = ExtBuilder::unsigned().build_offchainify();
623 ext.execute_with_sanity_checks(|| {
624 roll_to_unsigned_open();
625
626 assert_full_snapshot();
628 assert_eq!(pool.read().transactions.len(), 0);
630 assert_eq!(<VerifierPallet as Verifier>::queued_score(), None);
632
633 roll_next_with_ocw(Some(pool.clone()));
635 assert_eq!(pool.read().transactions.len(), 1);
636 assert_eq!(<VerifierPallet as Verifier>::queued_score(), None);
637
638 Snapshot::<Runtime>::remove_target(2);
641
642 roll_next_with_ocw(Some(pool.clone()));
644 assert_eq!(pool.read().transactions.len(), 0);
645 assert!(matches!(<VerifierPallet as Verifier>::queued_score(), Some(_)));
646 })
647 }
648}