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::CreateAuthorizedTransaction, 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 + CreateAuthorizedTransaction<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::weight_of_authorize(UnsignedWeightsOf::<T>::authorize_submit_unsigned())]
156 #[pallet::authorize(Self::authorize_submit_unsigned)]
157 #[pallet::call_index(0)]
158 pub fn submit_unsigned(
159 origin: OriginFor<T>,
160 paged_solution: Box<PagedRawSolution<T::MinerConfig>>,
161 ) -> DispatchResultWithPostInfo {
162 ensure_authorized(origin)?;
163 let error_message = "Invalid unsigned submission must produce invalid block and \
164 deprive validator from their authoring reward.";
165
166 debug_assert!(Self::validate_unsigned_checks(&paged_solution).is_ok());
169
170 let claimed_score = paged_solution.score;
171
172 let page_indices = crate::Pallet::<T>::msp_range_for(T::MinerPages::get() as usize);
174 <T::Verifier as Verifier>::verify_synchronous_multi(
175 paged_solution.solution_pages,
176 page_indices,
177 claimed_score,
178 )
179 .expect(error_message);
180
181 Ok(None.into())
182 }
183 }
184
185 impl<T: Config> Pallet<T> {
186 fn authorize_submit_unsigned(
188 source: TransactionSource,
189 paged_solution: &Box<PagedRawSolution<T::MinerConfig>>,
190 ) -> TransactionValidityWithRefund {
191 match source {
192 TransactionSource::Local | TransactionSource::InBlock => { },
193 _ => return Err(InvalidTransaction::Call.into()),
194 }
195
196 Self::validate_unsigned_checks(paged_solution.as_ref())
197 .map_err(|err| {
198 sublog!(debug, "unsigned", "solution authorization failed due to {:?}", err);
199 err
200 })
201 .map_err(base_error_to_invalid)?;
202
203 ValidTransaction::with_tag_prefix("OffchainElection")
204 .priority(
206 T::MinerTxPriority::get()
207 .saturating_add(paged_solution.score.minimal_stake.saturated_into()),
208 )
209 .and_provides(paged_solution.round)
212 .longevity(T::UnsignedPhase::get().saturated_into::<u64>())
214 .propagate(false)
216 .build()
217 .map(|validity| (validity, Weight::zero()))
218 }
219 }
220
221 #[pallet::hooks]
222 impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
223 fn integrity_test() {
224 assert!(
225 UnsignedWeightsOf::<T>::submit_unsigned().all_lte(T::BlockWeights::get().max_block),
226 "weight of `submit_unsigned` is too high"
227 );
228 assert!(
229 <T as Config>::MinerPages::get() as usize <=
230 <T as crate::Config>::Pages::get() as usize,
231 "number of pages in the unsigned phase is too high"
232 );
233 }
234
235 #[cfg(feature = "try-runtime")]
236 fn try_state(now: BlockNumberFor<T>) -> Result<(), sp_runtime::TryRuntimeError> {
237 Self::do_try_state(now)
238 }
239
240 fn offchain_worker(now: BlockNumberFor<T>) {
241 use sp_runtime::offchain::storage_lock::{BlockAndTime, StorageLock};
242
243 let mut lock =
247 StorageLock::<BlockAndTime<frame_system::Pallet<T>>>::with_block_deadline(
248 miner::OffchainWorkerMiner::<T>::OFFCHAIN_LOCK,
249 T::UnsignedPhase::get().saturated_into(),
250 );
251
252 match lock.try_lock() {
253 Ok(_guard) => {
254 Self::do_synchronized_offchain_worker(now);
255 },
256 Err(deadline) => {
257 sublog!(
258 trace,
259 "unsigned",
260 "offchain worker lock not released, deadline is {:?}",
261 deadline
262 );
263 },
264 };
265 }
266 }
267
268 impl<T: Config> Pallet<T> {
269 fn do_synchronized_offchain_worker(now: BlockNumberFor<T>) {
272 use miner::OffchainWorkerMiner;
273 let current_phase = crate::Pallet::<T>::current_phase();
274 sublog!(
275 trace,
276 "unsigned",
277 "lock for offchain worker acquired. Phase = {:?}",
278 current_phase
279 );
280
281 if current_phase.is_unsigned() {
283 if let Err(reason) = OffchainWorkerMiner::<T>::ensure_offchain_repeat_frequency(now)
284 {
285 sublog!(
286 debug,
287 "unsigned",
288 "offchain worker repeat frequency check failed: {:?}",
289 reason
290 );
291 return;
292 }
293 }
294
295 if current_phase.is_unsigned_opened_now() {
296 let initial_output = if T::OffchainStorage::get() {
298 OffchainWorkerMiner::<T>::mine_check_maybe_save_submit(true)
299 } else {
300 OffchainWorkerMiner::<T>::mine_check_maybe_save_submit(false)
301 };
302 sublog!(debug, "unsigned", "initial offchain worker output: {:?}", initial_output);
303 } else if current_phase.is_unsigned() {
304 let resubmit_output = if T::OffchainStorage::get() {
306 OffchainWorkerMiner::<T>::restore_or_compute_then_maybe_submit()
307 } else {
308 OffchainWorkerMiner::<T>::mine_check_maybe_save_submit(false)
309 };
310 sublog!(debug, "unsigned", "later offchain worker output: {:?}", resubmit_output);
311 };
312 }
313
314 pub(crate) fn validate_unsigned_checks(
319 paged_solution: &PagedRawSolution<T::MinerConfig>,
320 ) -> Result<(), CommonError> {
321 Self::unsigned_specific_checks(paged_solution)
322 .and(crate::Pallet::<T>::snapshot_independent_checks(paged_solution, None))
323 .map_err(Into::into)
324 }
325
326 pub fn unsigned_specific_checks(
330 paged_solution: &PagedRawSolution<T::MinerConfig>,
331 ) -> Result<(), CommonError> {
332 ensure!(
333 crate::Pallet::<T>::current_phase().is_unsigned(),
334 CommonError::EarlySubmission
335 );
336 ensure!(
337 paged_solution.solution_pages.len() == T::MinerPages::get() as usize,
338 CommonError::WrongPageCount
339 );
340 ensure!(
341 paged_solution.solution_pages.len() <= <T as crate::Config>::Pages::get() as usize,
342 CommonError::WrongPageCount
343 );
344
345 Ok(())
346 }
347
348 #[cfg(any(test, feature = "runtime-benchmarks", feature = "try-runtime"))]
349 pub(crate) fn do_try_state(
350 _now: BlockNumberFor<T>,
351 ) -> Result<(), sp_runtime::TryRuntimeError> {
352 Ok(())
353 }
354 }
355}
356
357#[cfg(test)]
358mod authorize {
359 use frame_election_provider_support::Support;
360 use frame_support::{
361 pallet_prelude::InvalidTransaction,
362 traits::Authorize,
363 unsigned::{TransactionSource, TransactionValidityError},
364 };
365
366 use super::Call;
367 use crate::{mock::*, types::*, verifier::Verifier};
368
369 #[test]
370 fn retracts_weak_score_accepts_better() {
371 ExtBuilder::mock_signed().build_and_execute(|| {
372 roll_to_snapshot_created();
373
374 let base_minimal_stake = 55;
375 let solution = mine_full_solution().unwrap();
376 load_mock_signed_and_start(solution.clone());
377 roll_to_full_verification();
378
379 assert_eq!(
381 <VerifierPallet as Verifier>::queued_score(),
382 Some(ElectionScore {
383 minimal_stake: base_minimal_stake,
384 sum_stake: 130,
385 sum_stake_squared: 8650
386 })
387 );
388
389 roll_to_unsigned_open();
390
391 let attempt = fake_solution(ElectionScore {
393 minimal_stake: base_minimal_stake - 1,
394 ..Default::default()
395 });
396 let call = Call::<Runtime>::submit_unsigned { paged_solution: Box::new(attempt) };
397 assert_eq!(
398 call.authorize(TransactionSource::Local).unwrap().unwrap_err(),
399 TransactionValidityError::Invalid(InvalidTransaction::Custom(2)),
400 );
401
402 let attempt = fake_solution(ElectionScore {
404 minimal_stake: base_minimal_stake + 1,
405 ..Default::default()
406 });
407 let call = Call::<Runtime>::submit_unsigned { paged_solution: Box::new(attempt) };
408 assert_eq!(
409 call.authorize(TransactionSource::Local).unwrap().unwrap_err(),
410 TransactionValidityError::Invalid(InvalidTransaction::Custom(4)),
411 );
412
413 let mut paged = raw_paged_from_supports(
416 vec![vec![
417 (40, Support { total: 10, voters: vec![(3, 5)] }),
418 (30, Support { total: 10, voters: vec![(3, 5)] }),
419 ]],
420 0,
421 );
422
423 paged.score =
424 ElectionScore { minimal_stake: base_minimal_stake + 1, ..Default::default() };
425 let call = Call::<Runtime>::submit_unsigned { paged_solution: Box::new(paged) };
426 assert!(call.authorize(TransactionSource::Local).unwrap().is_ok());
427 })
428 }
429
430 #[test]
431 fn retracts_wrong_round() {
432 ExtBuilder::mock_signed().build_and_execute(|| {
433 roll_to_unsigned_open();
434
435 let mut attempt =
436 fake_solution(ElectionScore { minimal_stake: 5, ..Default::default() });
437 attempt.round += 1;
438 let call = Call::<Runtime>::submit_unsigned { paged_solution: Box::new(attempt) };
439
440 assert_eq!(
441 call.authorize(TransactionSource::Local).unwrap().unwrap_err(),
442 TransactionValidityError::Invalid(InvalidTransaction::Custom(1)),
444 );
445 })
446 }
447
448 #[test]
449 fn retracts_too_many_pages_unsigned() {
450 ExtBuilder::mock_signed().build_and_execute(|| {
451 roll_to_unsigned_open();
454 let attempt = mine_full_solution().unwrap();
455 let call = Call::<Runtime>::submit_unsigned { paged_solution: Box::new(attempt) };
456
457 assert_eq!(
458 call.authorize(TransactionSource::Local).unwrap().unwrap_err(),
459 TransactionValidityError::Invalid(InvalidTransaction::Custom(3)),
461 );
462
463 let attempt = mine_solution(2).unwrap();
464 let call = Call::<Runtime>::submit_unsigned { paged_solution: Box::new(attempt) };
465
466 assert_eq!(
467 call.authorize(TransactionSource::Local).unwrap().unwrap_err(),
468 TransactionValidityError::Invalid(InvalidTransaction::Custom(3)),
469 );
470
471 let attempt = mine_solution(1).unwrap();
472 let call = Call::<Runtime>::submit_unsigned { paged_solution: Box::new(attempt) };
473
474 assert!(call.authorize(TransactionSource::Local).unwrap().is_ok(),);
475 })
476 }
477
478 #[test]
479 fn retracts_wrong_winner_count() {
480 ExtBuilder::mock_signed().desired_targets(2).build_and_execute(|| {
481 roll_to_unsigned_open();
482
483 let paged = raw_paged_from_supports(
484 vec![vec![(40, Support { total: 10, voters: vec![(3, 10)] })]],
485 0,
486 );
487
488 let call = Call::<Runtime>::submit_unsigned { paged_solution: Box::new(paged) };
489
490 assert_eq!(
491 call.authorize(TransactionSource::Local).unwrap().unwrap_err(),
492 TransactionValidityError::Invalid(InvalidTransaction::Custom(4)),
494 );
495 });
496 }
497
498 #[test]
499 fn retracts_wrong_phase() {
500 ExtBuilder::mock_signed().signed_phase(5, 6).build_and_execute(|| {
501 let solution = raw_paged_solution_low_score();
502 let call =
503 Call::<Runtime>::submit_unsigned { paged_solution: Box::new(solution.clone()) };
504
505 assert_eq!(MultiBlock::current_phase(), Phase::Off);
507 assert!(matches!(
508 call.authorize(TransactionSource::Local).unwrap().unwrap_err(),
509 TransactionValidityError::Invalid(InvalidTransaction::Custom(0))
511 ));
512 assert!(matches!(
513 call.authorize(TransactionSource::InBlock).unwrap().unwrap_err(),
514 TransactionValidityError::Invalid(InvalidTransaction::Custom(0))
515 ));
516
517 roll_to_signed_open();
519 assert!(MultiBlock::current_phase().is_signed());
520 assert!(matches!(
521 call.authorize(TransactionSource::Local).unwrap().unwrap_err(),
522 TransactionValidityError::Invalid(InvalidTransaction::Custom(0))
523 ));
524 assert!(matches!(
525 call.authorize(TransactionSource::InBlock).unwrap().unwrap_err(),
526 TransactionValidityError::Invalid(InvalidTransaction::Custom(0))
527 ));
528
529 roll_to_unsigned_open();
531 assert!(MultiBlock::current_phase().is_unsigned());
532
533 assert_ok!(call.authorize(TransactionSource::Local).unwrap());
534 assert_ok!(call.authorize(TransactionSource::InBlock).unwrap());
535 assert_eq!(
536 call.authorize(TransactionSource::External).unwrap().unwrap_err(),
537 TransactionValidityError::Invalid(InvalidTransaction::Call),
538 );
539 })
540 }
541
542 #[test]
543 fn priority_is_set() {
544 ExtBuilder::mock_signed()
545 .miner_tx_priority(20)
546 .desired_targets(0)
547 .build_and_execute(|| {
548 roll_to_unsigned_open();
549 assert!(MultiBlock::current_phase().is_unsigned());
550
551 let solution =
552 fake_solution(ElectionScore { minimal_stake: 5, ..Default::default() });
553 let call =
554 Call::<Runtime>::submit_unsigned { paged_solution: Box::new(solution.clone()) };
555
556 assert_eq!(
557 call.authorize(TransactionSource::Local).unwrap().unwrap().0.priority,
558 25
559 );
560 })
561 }
562}
563
564#[cfg(test)]
565mod call {
566 use crate::{mock::*, verifier::Verifier, Snapshot};
567
568 #[test]
569 fn unsigned_submission_e2e() {
570 let (mut ext, pool) = ExtBuilder::mock_signed().build_offchainify();
571 ext.execute_with_sanity_checks(|| {
572 roll_to_unsigned_open();
573
574 assert_full_snapshot();
576 assert_eq!(pool.read().transactions.len(), 0);
578 assert_eq!(<VerifierPallet as Verifier>::queued_score(), None);
580
581 roll_next_with_ocw(Some(pool.clone()));
583 assert_eq!(pool.read().transactions.len(), 1);
584 assert_eq!(<VerifierPallet as Verifier>::queued_score(), None);
585
586 roll_next_with_ocw(Some(pool.clone()));
588 assert_eq!(pool.read().transactions.len(), 0);
589 assert!(matches!(<VerifierPallet as Verifier>::queued_score(), Some(_)));
590 })
591 }
592
593 #[test]
594 #[should_panic(
595 expected = "Invalid unsigned submission must produce invalid block and deprive validator from their authoring reward."
596 )]
597 fn unfeasible_solution_panics() {
598 let (mut ext, pool) = ExtBuilder::mock_signed().build_offchainify();
599 ext.execute_with_sanity_checks(|| {
600 roll_to_unsigned_open();
601
602 assert_full_snapshot();
604 assert_eq!(pool.read().transactions.len(), 0);
606 assert_eq!(<VerifierPallet as Verifier>::queued_score(), None);
608
609 roll_next_with_ocw(Some(pool.clone()));
611 assert_eq!(pool.read().transactions.len(), 1);
612 assert_eq!(<VerifierPallet as Verifier>::queued_score(), None);
613
614 Snapshot::<Runtime>::remove_target(2);
617
618 roll_next_with_ocw(Some(pool.clone()));
620 assert_eq!(pool.read().transactions.len(), 0);
621 assert!(matches!(<VerifierPallet as Verifier>::queued_score(), Some(_)));
622 })
623 }
624}