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 #[allow(deprecated)]
184 #[pallet::validate_unsigned]
185 impl<T: Config> ValidateUnsigned for Pallet<T> {
186 type Call = Call<T>;
187 fn validate_unsigned(source: TransactionSource, call: &Self::Call) -> TransactionValidity {
188 if let Call::submit_unsigned { paged_solution, .. } = call {
189 match source {
190 TransactionSource::Local | TransactionSource::InBlock => { },
191 _ => return InvalidTransaction::Call.into(),
192 }
193
194 let _ = Self::validate_unsigned_checks(paged_solution.as_ref())
195 .map_err(|err| {
196 sublog!(
197 debug,
198 "unsigned",
199 "unsigned transaction validation failed due to {:?}",
200 err
201 );
202 err
203 })
204 .map_err(base_error_to_invalid)?;
205
206 ValidTransaction::with_tag_prefix("OffchainElection")
207 .priority(
209 T::MinerTxPriority::get()
210 .saturating_add(paged_solution.score.minimal_stake.saturated_into()),
211 )
212 .and_provides(paged_solution.round)
215 .longevity(T::UnsignedPhase::get().saturated_into::<u64>())
217 .propagate(false)
219 .build()
220 } else {
221 InvalidTransaction::Call.into()
222 }
223 }
224
225 fn pre_dispatch(call: &Self::Call) -> Result<(), TransactionValidityError> {
226 if let Call::submit_unsigned { paged_solution, .. } = call {
227 Self::validate_unsigned_checks(paged_solution.as_ref())
228 .map_err(base_error_to_invalid)
229 .map_err(Into::into)
230 } else {
231 Err(InvalidTransaction::Call.into())
232 }
233 }
234 }
235
236 #[pallet::hooks]
237 impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
238 fn integrity_test() {
239 assert!(
240 UnsignedWeightsOf::<T>::submit_unsigned().all_lte(T::BlockWeights::get().max_block),
241 "weight of `submit_unsigned` is too high"
242 );
243 assert!(
244 <T as Config>::MinerPages::get() as usize <=
245 <T as crate::Config>::Pages::get() as usize,
246 "number of pages in the unsigned phase is too high"
247 );
248 }
249
250 #[cfg(feature = "try-runtime")]
251 fn try_state(now: BlockNumberFor<T>) -> Result<(), sp_runtime::TryRuntimeError> {
252 Self::do_try_state(now)
253 }
254
255 fn offchain_worker(now: BlockNumberFor<T>) {
256 use sp_runtime::offchain::storage_lock::{BlockAndTime, StorageLock};
257
258 let mut lock =
262 StorageLock::<BlockAndTime<frame_system::Pallet<T>>>::with_block_deadline(
263 miner::OffchainWorkerMiner::<T>::OFFCHAIN_LOCK,
264 T::UnsignedPhase::get().saturated_into(),
265 );
266
267 match lock.try_lock() {
268 Ok(_guard) => {
269 Self::do_synchronized_offchain_worker(now);
270 },
271 Err(deadline) => {
272 sublog!(
273 trace,
274 "unsigned",
275 "offchain worker lock not released, deadline is {:?}",
276 deadline
277 );
278 },
279 };
280 }
281 }
282
283 impl<T: Config> Pallet<T> {
284 fn do_synchronized_offchain_worker(now: BlockNumberFor<T>) {
287 use miner::OffchainWorkerMiner;
288 let current_phase = crate::Pallet::<T>::current_phase();
289 sublog!(
290 trace,
291 "unsigned",
292 "lock for offchain worker acquired. Phase = {:?}",
293 current_phase
294 );
295
296 if current_phase.is_unsigned() {
298 if let Err(reason) = OffchainWorkerMiner::<T>::ensure_offchain_repeat_frequency(now)
299 {
300 sublog!(
301 debug,
302 "unsigned",
303 "offchain worker repeat frequency check failed: {:?}",
304 reason
305 );
306 return;
307 }
308 }
309
310 if current_phase.is_unsigned_opened_now() {
311 let initial_output = if T::OffchainStorage::get() {
313 OffchainWorkerMiner::<T>::mine_check_maybe_save_submit(true)
314 } else {
315 OffchainWorkerMiner::<T>::mine_check_maybe_save_submit(false)
316 };
317 sublog!(debug, "unsigned", "initial offchain worker output: {:?}", initial_output);
318 } else if current_phase.is_unsigned() {
319 let resubmit_output = if T::OffchainStorage::get() {
321 OffchainWorkerMiner::<T>::restore_or_compute_then_maybe_submit()
322 } else {
323 OffchainWorkerMiner::<T>::mine_check_maybe_save_submit(false)
324 };
325 sublog!(debug, "unsigned", "later offchain worker output: {:?}", resubmit_output);
326 };
327 }
328
329 pub(crate) fn validate_unsigned_checks(
335 paged_solution: &PagedRawSolution<T::MinerConfig>,
336 ) -> Result<(), CommonError> {
337 Self::unsigned_specific_checks(paged_solution)
338 .and(crate::Pallet::<T>::snapshot_independent_checks(paged_solution, None))
339 .map_err(Into::into)
340 }
341
342 pub fn unsigned_specific_checks(
346 paged_solution: &PagedRawSolution<T::MinerConfig>,
347 ) -> Result<(), CommonError> {
348 ensure!(
349 crate::Pallet::<T>::current_phase().is_unsigned(),
350 CommonError::EarlySubmission
351 );
352 ensure!(
353 paged_solution.solution_pages.len() == T::MinerPages::get() as usize,
354 CommonError::WrongPageCount
355 );
356 ensure!(
357 paged_solution.solution_pages.len() <= <T as crate::Config>::Pages::get() as usize,
358 CommonError::WrongPageCount
359 );
360
361 Ok(())
362 }
363
364 #[cfg(any(test, feature = "runtime-benchmarks", feature = "try-runtime"))]
365 pub(crate) fn do_try_state(
366 _now: BlockNumberFor<T>,
367 ) -> Result<(), sp_runtime::TryRuntimeError> {
368 Ok(())
369 }
370 }
371}
372
373#[cfg(test)]
374#[allow(deprecated)]
375mod validate_unsigned {
376 use frame_election_provider_support::Support;
377 use frame_support::{
378 pallet_prelude::InvalidTransaction,
379 unsigned::{TransactionSource, TransactionValidityError, ValidateUnsigned},
380 };
381
382 use super::Call;
383 use crate::{mock::*, types::*, verifier::Verifier};
384
385 #[test]
386 fn retracts_weak_score_accepts_better() {
387 ExtBuilder::mock_signed().build_and_execute(|| {
388 roll_to_snapshot_created();
389
390 let base_minimal_stake = 55;
391 let solution = mine_full_solution().unwrap();
392 load_mock_signed_and_start(solution.clone());
393 roll_to_full_verification();
394
395 assert_eq!(
397 <VerifierPallet as Verifier>::queued_score(),
398 Some(ElectionScore {
399 minimal_stake: base_minimal_stake,
400 sum_stake: 130,
401 sum_stake_squared: 8650
402 })
403 );
404
405 roll_to_unsigned_open();
406
407 let attempt = fake_solution(ElectionScore {
409 minimal_stake: base_minimal_stake - 1,
410 ..Default::default()
411 });
412 let call = Call::submit_unsigned { paged_solution: Box::new(attempt) };
413 assert_eq!(
414 UnsignedPallet::validate_unsigned(TransactionSource::Local, &call).unwrap_err(),
415 TransactionValidityError::Invalid(InvalidTransaction::Custom(2)),
416 );
417
418 let attempt = fake_solution(ElectionScore {
420 minimal_stake: base_minimal_stake + 1,
421 ..Default::default()
422 });
423 let call = Call::submit_unsigned { paged_solution: Box::new(attempt) };
424 assert_eq!(
425 UnsignedPallet::validate_unsigned(TransactionSource::Local, &call).unwrap_err(),
426 TransactionValidityError::Invalid(InvalidTransaction::Custom(4)),
427 );
428
429 let mut paged = raw_paged_from_supports(
432 vec![vec![
433 (40, Support { total: 10, voters: vec![(3, 5)] }),
434 (30, Support { total: 10, voters: vec![(3, 5)] }),
435 ]],
436 0,
437 );
438
439 paged.score =
440 ElectionScore { minimal_stake: base_minimal_stake + 1, ..Default::default() };
441 let call = Call::submit_unsigned { paged_solution: Box::new(paged) };
442 assert!(UnsignedPallet::validate_unsigned(TransactionSource::Local, &call).is_ok());
443 })
444 }
445
446 #[test]
447 fn retracts_wrong_round() {
448 ExtBuilder::mock_signed().build_and_execute(|| {
449 roll_to_unsigned_open();
450
451 let mut attempt =
452 fake_solution(ElectionScore { minimal_stake: 5, ..Default::default() });
453 attempt.round += 1;
454 let call = Call::submit_unsigned { paged_solution: Box::new(attempt) };
455
456 assert_eq!(
457 UnsignedPallet::validate_unsigned(TransactionSource::Local, &call).unwrap_err(),
458 TransactionValidityError::Invalid(InvalidTransaction::Custom(1)),
460 );
461 })
462 }
463
464 #[test]
465 fn retracts_too_many_pages_unsigned() {
466 ExtBuilder::mock_signed().build_and_execute(|| {
467 roll_to_unsigned_open();
470 let attempt = mine_full_solution().unwrap();
471 let call = Call::submit_unsigned { paged_solution: Box::new(attempt) };
472
473 assert_eq!(
474 UnsignedPallet::validate_unsigned(TransactionSource::Local, &call).unwrap_err(),
475 TransactionValidityError::Invalid(InvalidTransaction::Custom(3)),
477 );
478
479 let attempt = mine_solution(2).unwrap();
480 let call = Call::submit_unsigned { paged_solution: Box::new(attempt) };
481
482 assert_eq!(
483 UnsignedPallet::validate_unsigned(TransactionSource::Local, &call).unwrap_err(),
484 TransactionValidityError::Invalid(InvalidTransaction::Custom(3)),
485 );
486
487 let attempt = mine_solution(1).unwrap();
488 let call = Call::submit_unsigned { paged_solution: Box::new(attempt) };
489
490 assert!(UnsignedPallet::validate_unsigned(TransactionSource::Local, &call).is_ok(),);
491 })
492 }
493
494 #[test]
495 fn retracts_wrong_winner_count() {
496 ExtBuilder::mock_signed().desired_targets(2).build_and_execute(|| {
497 roll_to_unsigned_open();
498
499 let paged = raw_paged_from_supports(
500 vec![vec![(40, Support { total: 10, voters: vec![(3, 10)] })]],
501 0,
502 );
503
504 let call = Call::submit_unsigned { paged_solution: Box::new(paged) };
505
506 assert_eq!(
507 UnsignedPallet::validate_unsigned(TransactionSource::Local, &call).unwrap_err(),
508 TransactionValidityError::Invalid(InvalidTransaction::Custom(4)),
510 );
511 });
512 }
513
514 #[test]
515 fn retracts_wrong_phase() {
516 ExtBuilder::mock_signed().signed_phase(5, 6).build_and_execute(|| {
517 let solution = raw_paged_solution_low_score();
518 let call = Call::submit_unsigned { paged_solution: Box::new(solution.clone()) };
519
520 assert_eq!(MultiBlock::current_phase(), Phase::Off);
522 assert!(matches!(
523 <UnsignedPallet as ValidateUnsigned>::validate_unsigned(
524 TransactionSource::Local,
525 &call
526 )
527 .unwrap_err(),
528 TransactionValidityError::Invalid(InvalidTransaction::Custom(0))
530 ));
531 assert!(matches!(
532 <UnsignedPallet as ValidateUnsigned>::pre_dispatch(&call).unwrap_err(),
533 TransactionValidityError::Invalid(InvalidTransaction::Custom(0))
534 ));
535
536 roll_to_signed_open();
538 assert!(MultiBlock::current_phase().is_signed());
539 assert!(matches!(
540 <UnsignedPallet as ValidateUnsigned>::validate_unsigned(
541 TransactionSource::Local,
542 &call
543 )
544 .unwrap_err(),
545 TransactionValidityError::Invalid(InvalidTransaction::Custom(0))
546 ));
547 assert!(matches!(
548 <UnsignedPallet as ValidateUnsigned>::pre_dispatch(&call).unwrap_err(),
549 TransactionValidityError::Invalid(InvalidTransaction::Custom(0))
550 ));
551
552 roll_to_unsigned_open();
554 assert!(MultiBlock::current_phase().is_unsigned());
555
556 assert_ok!(<UnsignedPallet as ValidateUnsigned>::validate_unsigned(
557 TransactionSource::Local,
558 &call
559 ));
560 assert_ok!(<UnsignedPallet as ValidateUnsigned>::pre_dispatch(&call));
561 })
562 }
563
564 #[test]
565 fn priority_is_set() {
566 ExtBuilder::mock_signed()
567 .miner_tx_priority(20)
568 .desired_targets(0)
569 .build_and_execute(|| {
570 roll_to_unsigned_open();
571 assert!(MultiBlock::current_phase().is_unsigned());
572
573 let solution =
574 fake_solution(ElectionScore { minimal_stake: 5, ..Default::default() });
575 let call = Call::submit_unsigned { paged_solution: Box::new(solution.clone()) };
576
577 assert_eq!(
578 <UnsignedPallet as ValidateUnsigned>::validate_unsigned(
579 TransactionSource::Local,
580 &call
581 )
582 .unwrap()
583 .priority,
584 25
585 );
586 })
587 }
588}
589
590#[cfg(test)]
591mod call {
592 use crate::{mock::*, verifier::Verifier, Snapshot};
593
594 #[test]
595 fn unsigned_submission_e2e() {
596 let (mut ext, pool) = ExtBuilder::mock_signed().build_offchainify();
597 ext.execute_with_sanity_checks(|| {
598 roll_to_unsigned_open();
599
600 assert_full_snapshot();
602 assert_eq!(pool.read().transactions.len(), 0);
604 assert_eq!(<VerifierPallet as Verifier>::queued_score(), None);
606
607 roll_next_with_ocw(Some(pool.clone()));
609 assert_eq!(pool.read().transactions.len(), 1);
610 assert_eq!(<VerifierPallet as Verifier>::queued_score(), None);
611
612 roll_next_with_ocw(Some(pool.clone()));
614 assert_eq!(pool.read().transactions.len(), 0);
615 assert!(matches!(<VerifierPallet as Verifier>::queued_score(), Some(_)));
616 })
617 }
618
619 #[test]
620 #[should_panic(
621 expected = "Invalid unsigned submission must produce invalid block and deprive validator from their authoring reward."
622 )]
623 fn unfeasible_solution_panics() {
624 let (mut ext, pool) = ExtBuilder::mock_signed().build_offchainify();
625 ext.execute_with_sanity_checks(|| {
626 roll_to_unsigned_open();
627
628 assert_full_snapshot();
630 assert_eq!(pool.read().transactions.len(), 0);
632 assert_eq!(<VerifierPallet as Verifier>::queued_score(), None);
634
635 roll_next_with_ocw(Some(pool.clone()));
637 assert_eq!(pool.read().transactions.len(), 1);
638 assert_eq!(<VerifierPallet as Verifier>::queued_score(), None);
639
640 Snapshot::<Runtime>::remove_target(2);
643
644 roll_next_with_ocw(Some(pool.clone()));
646 assert_eq!(pool.read().transactions.len(), 0);
647 assert!(matches!(<VerifierPallet as Verifier>::queued_score(), Some(_)));
648 })
649 }
650}