Skip to content
tests.rs 146 KiB
Newer Older

				run_to_block(10);
				assert_session_era!(1, 0);
				assert_eq!(Staking::era_election_status(), ElectionStatus::Closed);
				assert!(Staking::snapshot_nominators().is_none());
				assert!(Staking::snapshot_validators().is_none());

				run_to_block(36);
				assert_session_era!(3, 0);

				// fist era has session 0, which has 0 blocks length, so we have in total 40 blocks
				// in the era.
				run_to_block(37);
				assert_session_era!(3, 0);
				assert_eq!(Staking::era_election_status(), ElectionStatus::Open(37));
				assert!(Staking::snapshot_nominators().is_some());
				assert!(Staking::snapshot_validators().is_some());

				run_to_block(38);
				assert_eq!(Staking::era_election_status(), ElectionStatus::Open(37));

				run_to_block(39);
				assert_eq!(Staking::era_election_status(), ElectionStatus::Open(37));

				run_to_block(40);
				assert_session_era!(4, 0);
				assert_eq!(Staking::era_election_status(), ElectionStatus::Closed);
				assert!(Staking::snapshot_nominators().is_none());
				assert!(Staking::snapshot_validators().is_none());

				run_to_block(86);
				assert_session_era!(8, 1);
				assert_eq!(Staking::era_election_status(), ElectionStatus::Closed);
				assert!(Staking::snapshot_nominators().is_none());
				assert!(Staking::snapshot_validators().is_none());

				// second era onwards has 50 blocks per era.
				run_to_block(87);
				assert_eq!(Staking::era_election_status(), ElectionStatus::Open(87));
				assert!(Staking::snapshot_nominators().is_some());
				assert!(Staking::snapshot_validators().is_some());

				run_to_block(90);
				assert_session_era!(9, 1);
				assert_eq!(Staking::era_election_status(), ElectionStatus::Closed);
				assert!(Staking::snapshot_nominators().is_none());
				assert!(Staking::snapshot_validators().is_none());
			})
	}

	fn offchain_window_is_triggered_when_forcing() {
		ExtBuilder::default()
			.session_per_era(5)
			.election_lookahead(3)
			.build()
			.execute_with(|| {
				run_to_block(12);
				ForceEra::put(Forcing::ForceNew);
				run_to_block(13);
				assert_eq!(Staking::era_election_status(), ElectionStatus::Closed);

				run_to_block(17); // instead of 47
				assert_eq!(Staking::era_election_status(), ElectionStatus::Open(17));

				run_to_block(20);
				assert_eq!(Staking::era_election_status(), ElectionStatus::Closed);
			})
	}

	#[test]
	fn offchain_window_is_triggered_when_force_always() {
		ExtBuilder::default()
			.session_per_era(5)
			.election_lookahead(3)
			.build()
			.execute_with(|| {
				ForceEra::put(Forcing::ForceAlways);
				run_to_block(16);
				assert_eq!(Staking::era_election_status(), ElectionStatus::Closed);

				run_to_block(17); // instead of 37
				assert_eq!(Staking::era_election_status(), ElectionStatus::Open(17));

				run_to_block(20);
				assert_eq!(Staking::era_election_status(), ElectionStatus::Closed);

				run_to_block(26);
				assert_eq!(Staking::era_election_status(), ElectionStatus::Closed);

				run_to_block(27); // next one again
				assert_eq!(Staking::era_election_status(), ElectionStatus::Open(27));
			})
	}

	#[test]
	fn offchain_window_closes_when_forcenone() {
		ExtBuilder::default()
			.session_per_era(5)
			.election_lookahead(3)
			.build()
			.execute_with(|| {
				ForceEra::put(Forcing::ForceNone);

				run_to_block(36);
				assert_session_era!(3, 0);
				assert_eq!(Staking::era_election_status(), ElectionStatus::Closed);

				// opens
				run_to_block(37);
				assert_eq!(Staking::era_election_status(), ElectionStatus::Open(37));
				assert!(Staking::is_current_session_final());
				assert!(Staking::snapshot_validators().is_some());

				// closes normally
				run_to_block(40);
				assert_eq!(Staking::era_election_status(), ElectionStatus::Closed);
				assert!(!Staking::is_current_session_final());
				assert!(Staking::snapshot_validators().is_none());
				assert_session_era!(4, 0);

				run_to_block(47);
				assert_eq!(Staking::era_election_status(), ElectionStatus::Closed);
				assert_session_era!(4, 0);

				run_to_block(57);
				assert_eq!(Staking::era_election_status(), ElectionStatus::Closed);
				assert_session_era!(5, 0);

				run_to_block(67);
				assert_eq!(Staking::era_election_status(), ElectionStatus::Closed);

				// Will not open again as scheduled
				run_to_block(87);
				assert_eq!(Staking::era_election_status(), ElectionStatus::Closed);
				assert_session_era!(8, 0);

				run_to_block(90);
				assert_eq!(Staking::era_election_status(), ElectionStatus::Closed);
				assert_session_era!(9, 0);
	fn offchain_window_on_chain_fallback_works() {
		ExtBuilder::default().build_and_execute(|| {
			start_session(1);
			start_session(2);
			assert_eq!(Staking::era_election_status(), ElectionStatus::Closed);
			// some election must have happened by now.
			assert_eq!(
				System::events()
					.into_iter()
					.map(|r| r.event)
					.filter_map(|e| {
						if let MetaEvent::staking(inner) = e {
							Some(inner)
						} else {
							None
						}
					})
					.last()
					.unwrap(),
				RawEvent::StakingElection(ElectionCompute::OnChain),
			);
		})
	}

	#[test]
	fn offchain_wont_work_if_snapshot_fails() {
		ExtBuilder::default()
			.offchain_election_ext()
			.build()
			.execute_with(|| {
				run_to_block(12);
				assert!(Staking::snapshot_validators().is_some());
				assert_eq!(Staking::era_election_status(), ElectionStatus::Open(12));

				// validate more than the limit
				let limit: NominatorIndex = ValidatorIndex::max_value() as NominatorIndex + 1;
				let ctrl = 1_000_000;
				for i in 0..limit {
					bond_validator((1000 + i).into(), (1000 + i + ctrl).into(), 100);
				}

				// window stays closed since no snapshot was taken.
				run_to_block(27);
				assert!(Staking::snapshot_validators().is_none());
				assert_eq!(Staking::era_election_status(), ElectionStatus::Closed);
			})
	}

	#[test]
	fn staking_is_locked_when_election_window_open() {
		ExtBuilder::default()
			.offchain_election_ext()
			.election_lookahead(3)
			.build()
			.execute_with(|| {
				run_to_block(12);
				assert!(Staking::snapshot_validators().is_some());
				assert_eq!(Staking::era_election_status(), ElectionStatus::Open(12));

				// chill et. al. are now not allowed.
				assert_noop!(
					Staking::chill(Origin::signed(10)),
					Error::<Test>::CallNotAllowed,
				);
			})
	}

	#[test]
	fn signed_result_can_be_submitted() {
		// should check that we have a new validator set normally, event says that it comes from
		// offchain.
		ExtBuilder::default()
			.offchain_election_ext()
			.build()
			.execute_with(|| {
				run_to_block(12);
				assert_eq!(Staking::era_election_status(), ElectionStatus::Open(12));
				assert!(Staking::snapshot_validators().is_some());

Kian Paimani's avatar
Kian Paimani committed
				let (compact, winners, score) = prepare_submission_with(true, true, 2, |_| {});
				assert_ok!(submit_solution(
					Origin::signed(10),
					winners,
					compact,
					score,
				));

				let queued_result = Staking::queued_elected().unwrap();
				assert_eq!(queued_result.compute, ElectionCompute::Signed);
				assert_eq!(
					System::events()
						.into_iter()
						.map(|r| r.event)
						.filter_map(|e| {
							if let MetaEvent::staking(inner) = e {
								Some(inner)
							} else {
								None
							}
						})
						.last()
						.unwrap(),
					RawEvent::SolutionStored(ElectionCompute::Signed),
				);

				run_to_block(15);
				assert_eq!(Staking::era_election_status(), ElectionStatus::Closed);

				assert_eq!(
					System::events()
						.into_iter()
						.map(|r| r.event)
						.filter_map(|e| {
							if let MetaEvent::staking(inner) = e {
								Some(inner)
							} else {
								None
							}
						})
						.last()
						.unwrap(),
					RawEvent::StakingElection(ElectionCompute::Signed),
				);
			})
	}

	#[test]
	fn signed_result_can_be_submitted_later() {
		// same as `signed_result_can_be_submitted` but at a later block.
		ExtBuilder::default()
			.offchain_election_ext()
			.build()
			.execute_with(|| {
				run_to_block(14);
				assert_eq!(Staking::era_election_status(), ElectionStatus::Open(12));

Kian Paimani's avatar
Kian Paimani committed
				let (compact, winners, score) = prepare_submission_with(true, true, 2, |_| {});
				assert_ok!(submit_solution(Origin::signed(10), winners, compact, score));

				let queued_result = Staking::queued_elected().unwrap();
				assert_eq!(queued_result.compute, ElectionCompute::Signed);

				run_to_block(15);
				assert_eq!(Staking::era_election_status(), ElectionStatus::Closed);

				assert_eq!(
					System::events()
						.into_iter()
						.map(|r| r.event)
						.filter_map(|e| {
							if let MetaEvent::staking(inner) = e {
								Some(inner)
							} else {
								None
							}
						})
						.last()
						.unwrap(),
					RawEvent::StakingElection(ElectionCompute::Signed),
				);
			})
	}

	#[test]
	fn early_solution_submission_is_rejected() {
		// should check that we have a new validator set normally, event says that it comes from
		// offchain.
		ExtBuilder::default()
			.offchain_election_ext()
			.build()
			.execute_with(|| {
				run_to_block(11);
				// submission is not yet allowed
				assert_eq!(Staking::era_election_status(), ElectionStatus::Closed);

				// create all the indices just to build the solution.
				Staking::create_stakers_snapshot();
Kian Paimani's avatar
Kian Paimani committed
				let (compact, winners, score) = prepare_submission_with(true, true, 2, |_| {});
				Staking::kill_stakers_snapshot();

				assert_err_with_weight!(
					Staking::submit_election_solution(
						Origin::signed(10),
						winners.clone(),
						compact.clone(),
						ElectionSize::default(),
					Error::<Test>::OffchainElectionEarlySubmission,
					Some(<Test as frame_system::Config>::DbWeight::get().reads(1)),
				);
			})
	}

	#[test]
	fn weak_solution_is_rejected() {
		// A solution which is weaker than what we currently have on-chain is rejected.
		ExtBuilder::default()
			.offchain_election_ext()
			.has_stakers(false)
			.validator_count(4)
			.build()
			.execute_with(|| {
				build_offchain_election_test_ext();
				run_to_block(12);

				// a good solution
Kian Paimani's avatar
Kian Paimani committed
				let (compact, winners, score) = prepare_submission_with(true, true, 2, |_| {});
				assert_ok!(submit_solution(
					Origin::signed(10),
					winners,
					compact,
					score,
				));

				// a bad solution
				let (compact, winners, score) = horrible_npos_solution(false);
				assert_err_with_weight!(
					submit_solution(
						Origin::signed(10),
						winners.clone(),
						compact.clone(),
					Error::<Test>::OffchainElectionWeakSubmission,
					Some(<Test as frame_system::Config>::DbWeight::get().reads(3))
				);
			})
	}

	#[test]
	fn better_solution_is_accepted() {
		// A solution which is better than what we currently have on-chain is accepted.
		ExtBuilder::default()
			.offchain_election_ext()
			.validator_count(4)
			.has_stakers(false)
			.build()
			.execute_with(|| {
				build_offchain_election_test_ext();
				run_to_block(12);

				// a meeeeh solution
				let (compact, winners, score) = horrible_npos_solution(false);
				assert_ok!(submit_solution(
					Origin::signed(10),
					winners,
					compact,
					score,
				));

				// a better solution
Kian Paimani's avatar
Kian Paimani committed
				let (compact, winners, score) = prepare_submission_with(true, true, 2, |_| {});
				assert_ok!(submit_solution(
					Origin::signed(10),
					winners,
					compact,
					score,
				));
			})
	}

	#[test]
	fn offchain_worker_runs_when_window_open() {
		// at the end of the first finalized block with ElectionStatus::open(_), it should execute.
		let mut ext = ExtBuilder::default()
			.offchain_election_ext()
			.validator_count(2)
			.build();
		let state = offchainify(&mut ext, 0);
		ext.execute_with(|| {
			run_to_block(12);

			// local key 11 is in the elected set.
			assert_eq_uvec!(Session::validators(), vec![11, 21]);
			assert_eq!(state.read().transactions.len(), 0);
			Staking::offchain_worker(12);
			assert_eq!(state.read().transactions.len(), 1);

			let encoded = state.read().transactions[0].clone();
			let extrinsic: Extrinsic = Decode::decode(&mut &*encoded).unwrap();

			let call = extrinsic.call;
			let inner = match call {
				mock::Call::Staking(inner) => inner,
			};

			assert_eq!(
				<Staking as sp_runtime::traits::ValidateUnsigned>::validate_unsigned(
					TransactionSource::Local,
					&inner,
				),
				TransactionValidity::Ok(ValidTransaction {
					priority: UnsignedPriority::get() + 1125, // the proposed slot stake.
					requires: vec![],
					provides: vec![("StakingOffchain", current_era()).encode()],
					longevity: 3,
					propagate: false,
				})
			)
		})
	}

	fn offchain_worker_runs_with_balancing() {
		// Offchain worker balances based on the number provided by randomness. See the difference
		// in the priority, which comes from the computed score.
		let mut ext = ExtBuilder::default()
			.offchain_election_ext()
			.validator_count(2)
			.max_offchain_iterations(2)
			.build();
		let state = offchainify(&mut ext, 2);
		ext.execute_with(|| {
			run_to_block(12);

			// local key 11 is in the elected set.
			assert_eq_uvec!(Session::validators(), vec![11, 21]);
			assert_eq!(state.read().transactions.len(), 0);
			Staking::offchain_worker(12);
			assert_eq!(state.read().transactions.len(), 1);

			let encoded = state.read().transactions[0].clone();
			let extrinsic: Extrinsic = Decode::decode(&mut &*encoded).unwrap();

			let call = extrinsic.call;
			let inner = match call {
				mock::Call::Staking(inner) => inner,
			};

			assert_eq!(
				<Staking as sp_runtime::traits::ValidateUnsigned>::validate_unsigned(
					TransactionSource::Local,
					&inner,
				),
				TransactionValidity::Ok(ValidTransaction {
					// the proposed slot stake, with balance_solution.
					priority: UnsignedPriority::get() + 1250,
					requires: vec![],
					provides: vec![("StakingOffchain", active_era()).encode()],
					longevity: 3,
					propagate: false,
				})
			)
		})
	}

	#[test]
	fn mediocre_submission_from_authority_is_early_rejected() {
		let mut ext = ExtBuilder::default()
			.offchain_election_ext()
			.validator_count(4)
			.build();
		let state = offchainify(&mut ext, 0);
		ext.execute_with(|| {
			run_to_block(12);
			// put a good solution on-chain
Kian Paimani's avatar
Kian Paimani committed
			let (compact, winners, score) = prepare_submission_with(true, true, 2, |_| {});
			assert_ok!(submit_solution(
				Origin::signed(10),
				winners,
				compact,
				score,
			),);

			// now run the offchain worker in the same chain state.
			Staking::offchain_worker(12);
			assert_eq!(state.read().transactions.len(), 1);

			let encoded = state.read().transactions[0].clone();
			let extrinsic: Extrinsic = Decode::decode(&mut &*encoded).unwrap();

			let call = extrinsic.call;
			let inner = match call {
				mock::Call::Staking(inner) => inner,
			};

			// pass this call to ValidateUnsigned
			assert_eq!(
				<Staking as sp_runtime::traits::ValidateUnsigned>::validate_unsigned(
					TransactionSource::Local,
					&inner,
				),
				TransactionValidity::Err(
					InvalidTransaction::Custom(<Error<Test>>::OffchainElectionWeakSubmission.as_u8()).into(),
	fn invalid_election_correct_number_of_winners() {
		ExtBuilder::default()
			.offchain_election_ext()
			.validator_count(4)
			.has_stakers(false)
			.build()
			.execute_with(|| {
				build_offchain_election_test_ext();
				run_to_block(12);

				ValidatorCount::put(3);
Kian Paimani's avatar
Kian Paimani committed
				let (compact, winners, score) = prepare_submission_with(true, true, 2, |_| {});
				ValidatorCount::put(4);

				assert_eq!(winners.len(), 3);

				assert_noop!(
					submit_solution(
						Origin::signed(10),
						winners,
						compact,
						score,
					),
					Error::<Test>::OffchainElectionBogusWinnerCount,
	fn invalid_election_solution_size() {
		ExtBuilder::default()
			.offchain_election_ext()
			.build()
			.execute_with(|| {
				run_to_block(12);

Kian Paimani's avatar
Kian Paimani committed
				let (compact, winners, score) = prepare_submission_with(true, true, 2, |_| {});
				assert_noop!(
					Staking::submit_election_solution(
						Origin::signed(10),
						winners,
						compact,
						score,
						ElectionSize::default(),
					Error::<Test>::OffchainElectionBogusElectionSize,
	fn invalid_election_correct_number_of_winners_1() {
		// if we have too little validators, then the number of candidates is the bound.
		ExtBuilder::default()
			.offchain_election_ext()
			.validator_count(8) // we simply cannot elect 8
			.has_stakers(false)
			.build()
			.execute_with(|| {
				build_offchain_election_test_ext();
				run_to_block(12);

				ValidatorCount::put(3);
Kian Paimani's avatar
Kian Paimani committed
				let (compact, winners, score) = prepare_submission_with(true, true, 2, |_| {});
				ValidatorCount::put(4);

				assert_eq!(winners.len(), 3);

				assert_noop!(
						Origin::signed(10),
						winners,
						compact,
						score,
					),
					Error::<Test>::OffchainElectionBogusWinnerCount,
	fn invalid_election_correct_number_of_winners_2() {
		// if we have too little validators, then the number of candidates is the bound.
		ExtBuilder::default()
			.offchain_election_ext()
			.validator_count(8) // we simply cannot elect 8
			.has_stakers(false)
			.build()
			.execute_with(|| {
				build_offchain_election_test_ext();
				run_to_block(12);

Kian Paimani's avatar
Kian Paimani committed
				let (compact, winners, score) = prepare_submission_with(true, true, 2, |_| {});

				assert_eq!(winners.len(), 4);

				// all good. We chose 4 and it works.
				assert_ok!(submit_solution(
					Origin::signed(10),
					winners,
					compact,
					score,
				),);
			})
	}

	#[test]
	fn invalid_election_out_of_bound_nominator_index() {
		// A nominator index which is simply invalid
		ExtBuilder::default()
			.offchain_election_ext()
			.validator_count(4)
			.has_stakers(false)
			.build()
			.execute_with(|| {
				build_offchain_election_test_ext();
				run_to_block(12);

				assert_eq!(Staking::snapshot_nominators().unwrap().len(), 5 + 4);
				assert_eq!(Staking::snapshot_validators().unwrap().len(), 4);
Kian Paimani's avatar
Kian Paimani committed
				let (mut compact, winners, score) = prepare_submission_with(true, true, 2, |_| {});

				// index 9 doesn't exist.
				compact.votes1.push((9, 2));

				// The error type sadly cannot be more specific now.
				assert_noop!(
						Origin::signed(10),
						winners,
						compact,
						score,
					),
					Error::<Test>::OffchainElectionBogusCompact,
	fn invalid_election_out_of_bound_validator_index() {
		// A validator index which is out of bound
		ExtBuilder::default()
			.offchain_election_ext()
			.validator_count(2)
			.has_stakers(false)
			.build()
			.execute_with(|| {
				build_offchain_election_test_ext();
				run_to_block(12);

				assert_eq!(Staking::snapshot_nominators().unwrap().len(), 5 + 4);
				assert_eq!(Staking::snapshot_validators().unwrap().len(), 4);
Kian Paimani's avatar
Kian Paimani committed
				let (mut compact, winners, score) = prepare_submission_with(true, true, 2, |_| {});

				// index 4 doesn't exist.
				compact.votes1.iter_mut().for_each(|(_, vidx)| if *vidx == 1 { *vidx = 4 });

				// The error type sadly cannot be more specific now.
				assert_noop!(
						Origin::signed(10),
						winners,
						compact,
						score,
					),
					Error::<Test>::OffchainElectionBogusCompact,
	fn invalid_election_out_of_bound_winner_index() {
		// A winner index which is simply invalid
		ExtBuilder::default()
			.offchain_election_ext()
			.validator_count(4)
			.has_stakers(false)
			.build()
			.execute_with(|| {
				build_offchain_election_test_ext();
				run_to_block(12);

				assert_eq!(Staking::snapshot_nominators().unwrap().len(), 5 + 4);
				assert_eq!(Staking::snapshot_validators().unwrap().len(), 4);
Kian Paimani's avatar
Kian Paimani committed
				let (compact, _, score) = prepare_submission_with(true, true, 2, |_| {});

				// index 4 doesn't exist.
				let winners = vec![0, 1, 2, 4];

				assert_noop!(
						Origin::signed(10),
						winners,
						compact,
						score,
					),
					Error::<Test>::OffchainElectionBogusWinner,
	fn invalid_election_non_winner_validator_index() {
		// An edge that points to a correct validator index who is NOT a winner. This is very
		// similar to the test that raises `OffchainElectionBogusNomination`.
		ExtBuilder::default()
			.offchain_election_ext()
			.validator_count(2) // we select only 2.
			.has_stakers(false)
			.build()
			.execute_with(|| {
				build_offchain_election_test_ext();
				run_to_block(12);

				assert_eq!(Staking::snapshot_nominators().unwrap().len(), 5 + 4);
				assert_eq!(Staking::snapshot_validators().unwrap().len(), 4);
Kian Paimani's avatar
Kian Paimani committed
				let (compact, winners, score) = prepare_submission_with(false, true, 2, |a| {
					// swap all 11 and 41s in the distribution with non-winners. Note that it is
					// important that the count of winners and the count of unique targets remain
					// valid.
					a.iter_mut().for_each(| StakedAssignment { who, distribution } |
						distribution.iter_mut().for_each(|(t, _)| {
							if *t == 41 { *t = 31 } else { *t = 21 }
							// if it is self vote, correct that.
							if *who == 41 { *who = 31 }
							if *who == 11 { *who = 21 }
						})
					);
				});

				assert_noop!(
					submit_solution(
						Origin::signed(10),
						winners,
						compact,
						score,
					),
					Error::<Test>::OffchainElectionBogusNomination,
				);
			})
	}

	#[test]
	fn offchain_election_unique_target_count_is_checked() {
		// Number of unique targets and and winners.len must match.
		ExtBuilder::default()
			.offchain_election_ext()
			.validator_count(2) // we select only 2.
			.has_stakers(false)
			.build()
			.execute_with(|| {
				build_offchain_election_test_ext();
				run_to_block(12);

				assert_eq!(Staking::snapshot_nominators().unwrap().len(), 5 + 4);
				assert_eq!(Staking::snapshot_validators().unwrap().len(), 4);

Kian Paimani's avatar
Kian Paimani committed
				let (compact, winners, score) = prepare_submission_with(false, true, 2, |a| {
					a.iter_mut()
						.find(|x| x.who == 5)
						// just add any new target.
						.map(|x| {
							// old value.
							assert_eq!(x.distribution, vec![(41, 100)]);
							// new value.
							x.distribution = vec![(21, 50), (41, 50)]
						});
						Origin::signed(10),
						winners,
						compact,
						score,
					),
					Error::<Test>::OffchainElectionBogusWinnerCount,
	fn invalid_election_wrong_self_vote() {
		// A self vote for someone else.
		ExtBuilder::default()
			.offchain_election_ext()
			.validator_count(4)
			.has_stakers(false)
			.build()
			.execute_with(|| {
				build_offchain_election_test_ext();
				run_to_block(12);

Kian Paimani's avatar
Kian Paimani committed
				let (compact, winners, score) = prepare_submission_with(true, true, 2, |a| {
					// mutate a self vote to target someone else. That someone else is still among the
					// winners
					a.iter_mut().find(|x| x.who == 11).map(|x| {
						x.distribution
							.iter_mut()
							.find(|y| y.0 == 11)
							.map(|y| y.0 = 21)
					});
				});

				assert_noop!(
						Origin::signed(10),
						winners,
						compact,
						score,
					),
					Error::<Test>::OffchainElectionBogusSelfVote,
	fn invalid_election_wrong_self_vote_2() {
		// A self validator voting for someone else next to self vote.
		ExtBuilder::default()
			.offchain_election_ext()
			.validator_count(4)
			.has_stakers(false)
			.build()
			.execute_with(|| {
				build_offchain_election_test_ext();
				run_to_block(12);

Kian Paimani's avatar
Kian Paimani committed
				let (compact, winners, score) = prepare_submission_with(true, true, 2, |a| {
					// Remove the self vote.
					a.retain(|x| x.who != 11);
					// add is as a new double vote
					a.push(StakedAssignment {
						who: 11,
						distribution: vec![(11, 50), (21, 50)],
					});
				});

				// This raises score issue.
				assert_noop!(
						Origin::signed(10),
						winners,
						compact,
						score,
					),
					Error::<Test>::OffchainElectionBogusSelfVote,
	fn invalid_election_over_stake() {
		// Someone's edge ratios sums to more than 100%.
		ExtBuilder::default()
			.offchain_election_ext()
			.validator_count(4)
			.has_stakers(false)
			.build()
			.execute_with(|| {
				build_offchain_election_test_ext();
				run_to_block(12);

				// Note: we don't reduce here to be able to tweak votes3. votes3 will vanish if you
				// reduce.
Kian Paimani's avatar
Kian Paimani committed
				let (mut compact, winners, score) = prepare_submission_with(true, false, 0, |_| {});

				if let Some(c) = compact.votes3.iter_mut().find(|x| x.0 == 0) {
					// by default it should have been (0, [(2, 33%), (1, 33%)], 0)
					// now the sum is above 100%
					c.1 = [(2, percent(66)), (1, percent(66))];
				}

				assert_noop!(
						Origin::signed(10),
						winners,
						compact,
						score,
					),
					Error::<Test>::OffchainElectionBogusCompact,
	fn invalid_election_under_stake() {
		// at the time of this writing, we cannot under stake someone. The compact assignment works
		// in a way that some of the stakes are presented by the submitter, and the last one is read
		// from chain by subtracting the rest from total. Hence, the sum is always correct.
		// This test is only here as a demonstration.
	}

	#[test]
	fn invalid_election_invalid_target_stealing() {
		// A valid voter who voted for someone who is a candidate, and is a correct winner, but is
		// actually NOT nominated by this nominator.
		ExtBuilder::default()
			.offchain_election_ext()
			.validator_count(4)
			.has_stakers(false)
			.build()
			.execute_with(|| {
				build_offchain_election_test_ext();
				run_to_block(12);

Kian Paimani's avatar
Kian Paimani committed
				let (compact, winners, score) = prepare_submission_with(true, false, 0, |a| {
					// 3 only voted for 20 and 40. We add a fake vote to 30. The stake sum is still
					// correctly 100.
					a.iter_mut()
						.find(|x| x.who == 3)
						.map(|x| x.distribution = vec![(21, 50), (41, 30), (31, 20)]);
				});

				assert_noop!(
						Origin::signed(10),
						winners,
						compact,
						score,
					),
					Error::<Test>::OffchainElectionBogusNomination,
				);
			})
	}

	#[test]
	fn nomination_slash_filter_is_checked() {
		// If a nominator has voted for someone who has been recently slashed, that particular
		// nomination should be disabled for the upcoming election. A solution must respect this
		// rule.
		ExtBuilder::default()
			.offchain_election_ext()
			.validator_count(4)
			.has_stakers(false)
			.build()
			.execute_with(|| {
				build_offchain_election_test_ext();

				// finalize the round with fallback. This is needed since all nominator submission
				// are in era zero and we want this one to pass with no problems.
				run_to_block(15);

				// go to the next session to trigger mock::start_era and bump the active era
				run_to_block(20);

				// slash 10. This must happen outside of the election window.
				let offender_expo = Staking::eras_stakers(Staking::active_era().unwrap().index, 11);
				on_offence_now(
					&[OffenceDetails {
						offender: (11, offender_expo.clone()),
						reporters: vec![],
					}],
					&[Perbill::from_percent(50)],
				);

				// validate 10 again for the next round. But this guy will not have the votes that