use crate::{
helpers, Call, Config, CurrentPhase, DesiredTargets, ElectionCompute, Error, FeasibilityError,
Pallet, QueuedSolution, RawSolution, ReadySolution, Round, RoundSnapshot, Snapshot,
SolutionAccuracyOf, SolutionOf, SolutionOrSnapshotSize, Weight,
};
use alloc::{boxed::Box, vec::Vec};
use codec::Encode;
use frame_election_provider_support::{NposSolution, NposSolver, PerThing128, VoteWeight};
use frame_support::{
dispatch::DispatchResult,
ensure,
traits::{DefensiveResult, Get},
BoundedVec,
};
use frame_system::{
offchain::{CreateInherent, SubmitTransaction},
pallet_prelude::BlockNumberFor,
};
use scale_info::TypeInfo;
use sp_npos_elections::{
assignment_ratio_to_staked_normalized, assignment_staked_to_ratio_normalized, ElectionResult,
ElectionScore, EvaluateSupport,
};
use sp_runtime::{
offchain::storage::{MutateStorageError, StorageValueRef},
DispatchError, SaturatedConversion,
};
pub(crate) const OFFCHAIN_LAST_BLOCK: &[u8] = b"parity/multi-phase-unsigned-election";
pub(crate) const OFFCHAIN_LOCK: &[u8] = b"parity/multi-phase-unsigned-election/lock";
pub(crate) const OFFCHAIN_CACHED_CALL: &[u8] = b"parity/multi-phase-unsigned-election/call";
pub type VoterOf<T> = frame_election_provider_support::VoterOf<<T as Config>::DataProvider>;
pub type MinerVoterOf<T> = frame_election_provider_support::Voter<
<T as MinerConfig>::AccountId,
<T as MinerConfig>::MaxVotesPerVoter,
>;
pub type Assignment<T> =
sp_npos_elections::Assignment<<T as frame_system::Config>::AccountId, SolutionAccuracyOf<T>>;
pub type IndexAssignmentOf<T> = frame_election_provider_support::IndexAssignmentOf<SolutionOf<T>>;
pub type SolverErrorOf<T> = <<T as Config>::Solver as NposSolver>::Error;
#[derive(frame_support::DebugNoBound, frame_support::PartialEqNoBound)]
pub enum MinerError {
NposElections(sp_npos_elections::Error),
SnapshotUnAvailable,
PoolSubmissionFailed,
PreDispatchChecksFailed(DispatchError),
Feasibility(FeasibilityError),
Lock(&'static str),
NoStoredSolution,
SolutionCallInvalid,
FailedToStoreSolution,
NoMoreVoters,
Solver,
}
impl From<sp_npos_elections::Error> for MinerError {
fn from(e: sp_npos_elections::Error) -> Self {
MinerError::NposElections(e)
}
}
impl From<FeasibilityError> for MinerError {
fn from(e: FeasibilityError) -> Self {
MinerError::Feasibility(e)
}
}
#[derive(Debug, Clone)]
pub struct TrimmingStatus {
weight: usize,
length: usize,
}
impl TrimmingStatus {
pub fn is_trimmed(&self) -> bool {
self.weight > 0 || self.length > 0
}
pub fn trimmed_weight(&self) -> usize {
self.weight
}
pub fn trimmed_length(&self) -> usize {
self.length
}
}
fn save_solution<T: Config>(call: &Call<T>) -> Result<(), MinerError> {
log!(debug, "saving a call to the offchain storage.");
let storage = StorageValueRef::persistent(OFFCHAIN_CACHED_CALL);
match storage.mutate::<_, (), _>(|_| Ok(call.clone())) {
Ok(_) => Ok(()),
Err(MutateStorageError::ConcurrentModification(_)) =>
Err(MinerError::FailedToStoreSolution),
Err(MutateStorageError::ValueFunctionFailed(_)) => {
Err(MinerError::FailedToStoreSolution)
},
}
}
fn restore_solution<T: Config>() -> Result<Call<T>, MinerError> {
StorageValueRef::persistent(OFFCHAIN_CACHED_CALL)
.get()
.ok()
.flatten()
.ok_or(MinerError::NoStoredSolution)
}
pub(super) fn kill_ocw_solution<T: Config>() {
log!(debug, "clearing offchain call cache storage.");
let mut storage = StorageValueRef::persistent(OFFCHAIN_CACHED_CALL);
storage.clear();
}
fn clear_offchain_repeat_frequency() {
let mut last_block = StorageValueRef::persistent(OFFCHAIN_LAST_BLOCK);
last_block.clear();
}
#[cfg(test)]
fn ocw_solution_exists<T: Config>() -> bool {
matches!(StorageValueRef::persistent(OFFCHAIN_CACHED_CALL).get::<Call<T>>(), Ok(Some(_)))
}
impl<T: Config + CreateInherent<Call<T>>> Pallet<T> {
pub fn mine_solution() -> Result<
(RawSolution<SolutionOf<T::MinerConfig>>, SolutionOrSnapshotSize, TrimmingStatus),
MinerError,
> {
let RoundSnapshot { voters, targets } =
Snapshot::<T>::get().ok_or(MinerError::SnapshotUnAvailable)?;
let desired_targets = DesiredTargets::<T>::get().ok_or(MinerError::SnapshotUnAvailable)?;
let (solution, score, size, is_trimmed) =
Miner::<T::MinerConfig>::mine_solution_with_snapshot::<T::Solver>(
voters,
targets,
desired_targets,
)?;
let round = Round::<T>::get();
Ok((RawSolution { solution, score, round }, size, is_trimmed))
}
pub fn restore_or_compute_then_maybe_submit() -> Result<(), MinerError> {
log!(debug, "miner attempting to restore or compute an unsigned solution.");
let call = restore_solution::<T>()
.and_then(|call| {
if let Call::submit_unsigned { raw_solution, .. } = &call {
Self::basic_checks(raw_solution, "restored")?;
Ok(call)
} else {
Err(MinerError::SolutionCallInvalid)
}
})
.or_else::<MinerError, _>(|error| {
log!(debug, "restoring solution failed due to {:?}", error);
match error {
MinerError::NoStoredSolution => {
log!(trace, "mining a new solution.");
let call = Self::mine_checked_call()?;
save_solution(&call)?;
Ok(call)
},
MinerError::Feasibility(_) => {
log!(trace, "wiping infeasible solution.");
kill_ocw_solution::<T>();
clear_offchain_repeat_frequency();
Err(error)
},
_ => {
Err(error)
},
}
})?;
Self::submit_call(call)
}
pub fn mine_check_save_submit() -> Result<(), MinerError> {
log!(debug, "miner attempting to compute an unsigned solution.");
let call = Self::mine_checked_call()?;
save_solution(&call)?;
Self::submit_call(call)
}
pub fn mine_checked_call() -> Result<Call<T>, MinerError> {
let (raw_solution, witness, _) = Self::mine_and_check()?;
let score = raw_solution.score;
let call: Call<T> = Call::submit_unsigned { raw_solution: Box::new(raw_solution), witness };
log!(
debug,
"mined a solution with score {:?} and size {}",
score,
call.using_encoded(|b| b.len())
);
Ok(call)
}
fn submit_call(call: Call<T>) -> Result<(), MinerError> {
log!(debug, "miner submitting a solution as an unsigned transaction");
let xt = T::create_inherent(call.into());
SubmitTransaction::<T, Call<T>>::submit_transaction(xt)
.map_err(|_| MinerError::PoolSubmissionFailed)
}
pub fn basic_checks(
raw_solution: &RawSolution<SolutionOf<T::MinerConfig>>,
solution_type: &str,
) -> Result<(), MinerError> {
Self::unsigned_pre_dispatch_checks(raw_solution).map_err(|err| {
log!(debug, "pre-dispatch checks failed for {} solution: {:?}", solution_type, err);
MinerError::PreDispatchChecksFailed(err)
})?;
Self::feasibility_check(raw_solution.clone(), ElectionCompute::Unsigned).map_err(
|err| {
log!(debug, "feasibility check failed for {} solution: {:?}", solution_type, err);
err
},
)?;
Ok(())
}
pub fn mine_and_check() -> Result<
(RawSolution<SolutionOf<T::MinerConfig>>, SolutionOrSnapshotSize, TrimmingStatus),
MinerError,
> {
let (raw_solution, witness, is_trimmed) = Self::mine_solution()?;
Self::basic_checks(&raw_solution, "mined")?;
Ok((raw_solution, witness, is_trimmed))
}
pub fn ensure_offchain_repeat_frequency(now: BlockNumberFor<T>) -> Result<(), MinerError> {
let threshold = T::OffchainRepeat::get();
let last_block = StorageValueRef::persistent(OFFCHAIN_LAST_BLOCK);
let mutate_stat = last_block.mutate::<_, &'static str, _>(
|maybe_head: Result<Option<BlockNumberFor<T>>, _>| {
match maybe_head {
Ok(Some(head)) if now < head => Err("fork."),
Ok(Some(head)) if now >= head && now <= head + threshold =>
Err("recently executed."),
Ok(Some(head)) if now > head + threshold => {
Ok(now)
},
_ => {
Ok(now)
},
}
},
);
match mutate_stat {
Ok(_) => Ok(()),
Err(MutateStorageError::ConcurrentModification(_)) =>
Err(MinerError::Lock("failed to write to offchain db (concurrent modification).")),
Err(MutateStorageError::ValueFunctionFailed(why)) => Err(MinerError::Lock(why)),
}
}
pub fn unsigned_pre_dispatch_checks(
raw_solution: &RawSolution<SolutionOf<T::MinerConfig>>,
) -> DispatchResult {
ensure!(
CurrentPhase::<T>::get().is_unsigned_open(),
Error::<T>::PreDispatchEarlySubmission
);
ensure!(Round::<T>::get() == raw_solution.round, Error::<T>::OcwCallWrongEra);
ensure!(
DesiredTargets::<T>::get().unwrap_or_default() ==
raw_solution.solution.unique_targets().len() as u32,
Error::<T>::PreDispatchWrongWinnerCount,
);
ensure!(
QueuedSolution::<T>::get()
.map_or(true, |q: ReadySolution<_, _>| raw_solution.score > q.score),
Error::<T>::PreDispatchWeakSubmission,
);
Ok(())
}
}
pub trait MinerConfig {
type AccountId: Ord + Clone + codec::Codec + core::fmt::Debug;
type Solution: codec::Codec
+ Default
+ PartialEq
+ Eq
+ Clone
+ core::fmt::Debug
+ Ord
+ NposSolution
+ TypeInfo;
type MaxVotesPerVoter;
type MaxLength: Get<u32>;
type MaxWeight: Get<Weight>;
type MaxWinners: Get<u32>;
fn solution_weight(voters: u32, targets: u32, active_voters: u32, degree: u32) -> Weight;
}
pub struct Miner<T: MinerConfig>(core::marker::PhantomData<T>);
impl<T: MinerConfig> Miner<T> {
pub fn mine_solution_with_snapshot<S>(
voters: Vec<(T::AccountId, VoteWeight, BoundedVec<T::AccountId, T::MaxVotesPerVoter>)>,
targets: Vec<T::AccountId>,
desired_targets: u32,
) -> Result<(SolutionOf<T>, ElectionScore, SolutionOrSnapshotSize, TrimmingStatus), MinerError>
where
S: NposSolver<AccountId = T::AccountId>,
{
S::solve(desired_targets as usize, targets.clone(), voters.clone())
.map_err(|e| {
log_no_system!(error, "solver error: {:?}", e);
MinerError::Solver
})
.and_then(|e| {
Self::prepare_election_result_with_snapshot::<S::Accuracy>(
e,
voters,
targets,
desired_targets,
)
})
}
pub fn prepare_election_result_with_snapshot<Accuracy: PerThing128>(
election_result: ElectionResult<T::AccountId, Accuracy>,
voters: Vec<(T::AccountId, VoteWeight, BoundedVec<T::AccountId, T::MaxVotesPerVoter>)>,
targets: Vec<T::AccountId>,
desired_targets: u32,
) -> Result<(SolutionOf<T>, ElectionScore, SolutionOrSnapshotSize, TrimmingStatus), MinerError>
{
let cache = helpers::generate_voter_cache::<T>(&voters);
let voter_index = helpers::voter_index_fn::<T>(&cache);
let target_index = helpers::target_index_fn::<T>(&targets);
let voter_at = helpers::voter_at_fn::<T>(&voters);
let target_at = helpers::target_at_fn::<T>(&targets);
let stake_of = helpers::stake_of_fn::<T>(&voters, &cache);
let encoded_size_of = |assignments: &[IndexAssignmentOf<T>]| {
SolutionOf::<T>::try_from(assignments).map(|s| s.encoded_size())
};
let ElectionResult { assignments, winners: _ } = election_result;
let sorted_assignments = {
let mut staked = assignment_ratio_to_staked_normalized(assignments, &stake_of)?;
sp_npos_elections::reduce(&mut staked);
staked.sort_by_key(
|sp_npos_elections::StakedAssignment::<T::AccountId> { who, .. }| {
let stake = cache
.get(who)
.map(|idx| {
let (_, stake, _) = voters[*idx];
stake
})
.unwrap_or_default();
core::cmp::Reverse(stake)
},
);
assignment_staked_to_ratio_normalized(staked)?
};
let mut index_assignments = sorted_assignments
.into_iter()
.map(|assignment| IndexAssignmentOf::<T>::new(&assignment, &voter_index, &target_index))
.collect::<Result<Vec<_>, _>>()?;
let size =
SolutionOrSnapshotSize { voters: voters.len() as u32, targets: targets.len() as u32 };
let weight_trimmed = Self::trim_assignments_weight(
desired_targets,
size,
T::MaxWeight::get(),
&mut index_assignments,
);
let length_trimmed = Self::trim_assignments_length(
T::MaxLength::get(),
&mut index_assignments,
&encoded_size_of,
)?;
let solution = SolutionOf::<T>::try_from(&index_assignments)?;
let score = solution.clone().score(stake_of, voter_at, target_at)?;
let is_trimmed = TrimmingStatus { weight: weight_trimmed, length: length_trimmed };
Ok((solution, score, size, is_trimmed))
}
pub fn trim_assignments_length(
max_allowed_length: u32,
assignments: &mut Vec<IndexAssignmentOf<T>>,
encoded_size_of: impl Fn(&[IndexAssignmentOf<T>]) -> Result<usize, sp_npos_elections::Error>,
) -> Result<usize, MinerError> {
let max_allowed_length: usize = max_allowed_length.saturated_into();
let mut high = assignments.len();
let mut low = 0;
if high == low {
return Ok(0)
}
while high - low > 1 {
let test = (high + low) / 2;
if encoded_size_of(&assignments[..test])? <= max_allowed_length {
low = test;
} else {
high = test;
}
}
let maximum_allowed_voters = if low < assignments.len() &&
encoded_size_of(&assignments[..low + 1])? <= max_allowed_length
{
low + 1
} else {
low
};
debug_assert!(
encoded_size_of(&assignments[..maximum_allowed_voters]).unwrap() <= max_allowed_length
);
debug_assert!(if maximum_allowed_voters < assignments.len() {
encoded_size_of(&assignments[..maximum_allowed_voters + 1]).unwrap() >
max_allowed_length
} else {
true
});
let remove = assignments.len().saturating_sub(maximum_allowed_voters);
log_no_system!(
debug,
"from {} assignments, truncating to {} for length, removing {}",
assignments.len(),
maximum_allowed_voters,
remove
);
assignments.truncate(maximum_allowed_voters);
Ok(remove)
}
pub fn trim_assignments_weight(
desired_targets: u32,
size: SolutionOrSnapshotSize,
max_weight: Weight,
assignments: &mut Vec<IndexAssignmentOf<T>>,
) -> usize {
let maximum_allowed_voters =
Self::maximum_voter_for_weight(desired_targets, size, max_weight);
let removing: usize =
assignments.len().saturating_sub(maximum_allowed_voters.saturated_into());
log_no_system!(
debug,
"from {} assignments, truncating to {} for weight, removing {}",
assignments.len(),
maximum_allowed_voters,
removing,
);
assignments.truncate(maximum_allowed_voters as usize);
removing
}
pub fn maximum_voter_for_weight(
desired_winners: u32,
size: SolutionOrSnapshotSize,
max_weight: Weight,
) -> u32 {
if size.voters < 1 {
return size.voters
}
let max_voters = size.voters.max(1);
let mut voters = max_voters;
let weight_with = |active_voters: u32| -> Weight {
T::solution_weight(size.voters, size.targets, active_voters, desired_winners)
};
let next_voters = |current_weight: Weight, voters: u32, step: u32| -> Result<u32, ()> {
if current_weight.all_lt(max_weight) {
let next_voters = voters.checked_add(step);
match next_voters {
Some(voters) if voters < max_voters => Ok(voters),
_ => Err(()),
}
} else if current_weight.any_gt(max_weight) {
voters.checked_sub(step).ok_or(())
} else {
Ok(voters)
}
};
let mut step = voters / 2;
let mut current_weight = weight_with(voters);
while step > 0 {
match next_voters(current_weight, voters, step) {
Ok(next) if next != voters => {
voters = next;
},
Err(()) => break,
Ok(next) => return next,
}
step /= 2;
current_weight = weight_with(voters);
}
while voters < max_voters && weight_with(voters + 1).all_lt(max_weight) {
voters += 1;
}
while voters.checked_sub(1).is_some() && weight_with(voters).any_gt(max_weight) {
voters -= 1;
}
let final_decision = voters.min(size.voters);
debug_assert!(
weight_with(final_decision).all_lte(max_weight),
"weight_with({}) <= {}",
final_decision,
max_weight,
);
final_decision
}
pub fn feasibility_check(
raw_solution: RawSolution<SolutionOf<T>>,
compute: ElectionCompute,
desired_targets: u32,
snapshot: RoundSnapshot<T::AccountId, MinerVoterOf<T>>,
current_round: u32,
minimum_untrusted_score: Option<ElectionScore>,
) -> Result<ReadySolution<T::AccountId, T::MaxWinners>, FeasibilityError> {
let RawSolution { solution, score, round } = raw_solution;
let RoundSnapshot { voters: snapshot_voters, targets: snapshot_targets } = snapshot;
ensure!(current_round == round, FeasibilityError::InvalidRound);
let winners = solution.unique_targets();
ensure!(winners.len() as u32 == desired_targets, FeasibilityError::WrongWinnerCount);
ensure!(desired_targets <= T::MaxWinners::get(), FeasibilityError::TooManyDesiredTargets);
let submitted_score = raw_solution.score;
ensure!(
minimum_untrusted_score.map_or(true, |min_score| {
submitted_score.strict_threshold_better(min_score, sp_runtime::Perbill::zero())
}),
FeasibilityError::UntrustedScoreTooLow
);
let cache = helpers::generate_voter_cache::<T>(&snapshot_voters);
let voter_at = helpers::voter_at_fn::<T>(&snapshot_voters);
let target_at = helpers::target_at_fn::<T>(&snapshot_targets);
let voter_index = helpers::voter_index_fn_usize::<T>(&cache);
let assignments = solution
.into_assignment(voter_at, target_at)
.map_err::<FeasibilityError, _>(Into::into)?;
let _ = assignments.iter().try_for_each(|assignment| {
let snapshot_index =
voter_index(&assignment.who).ok_or(FeasibilityError::InvalidVoter)?;
let (_voter, _stake, targets) =
snapshot_voters.get(snapshot_index).ok_or(FeasibilityError::InvalidVoter)?;
if assignment.distribution.iter().any(|(d, _)| !targets.contains(d)) {
return Err(FeasibilityError::InvalidVote)
}
Ok(())
})?;
let stake_of = helpers::stake_of_fn::<T>(&snapshot_voters, &cache);
let staked_assignments = assignment_ratio_to_staked_normalized(assignments, stake_of)
.map_err::<FeasibilityError, _>(Into::into)?;
let supports = sp_npos_elections::to_supports(&staked_assignments);
let known_score = supports.evaluate();
ensure!(known_score == score, FeasibilityError::InvalidScore);
let supports = supports
.try_into()
.defensive_map_err(|_| FeasibilityError::BoundedConversionFailed)?;
Ok(ReadySolution { supports, compute, score })
}
}
#[cfg(test)]
mod max_weight {
#![allow(unused_variables)]
use super::*;
use crate::mock::{MockWeightInfo, Runtime};
#[test]
fn find_max_voter_binary_search_works() {
let w = SolutionOrSnapshotSize { voters: 10, targets: 0 };
MockWeightInfo::set(crate::mock::MockedWeightInfo::Complex);
assert_eq!(
Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(0, u64::MAX)),
0
);
assert_eq!(
Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(1, u64::MAX)),
0
);
assert_eq!(
Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(999, u64::MAX)),
0
);
assert_eq!(
Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(1000, u64::MAX)),
1
);
assert_eq!(
Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(1001, u64::MAX)),
1
);
assert_eq!(
Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(1990, u64::MAX)),
1
);
assert_eq!(
Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(1999, u64::MAX)),
1
);
assert_eq!(
Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(2000, u64::MAX)),
2
);
assert_eq!(
Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(2001, u64::MAX)),
2
);
assert_eq!(
Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(2010, u64::MAX)),
2
);
assert_eq!(
Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(2990, u64::MAX)),
2
);
assert_eq!(
Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(2999, u64::MAX)),
2
);
assert_eq!(
Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(3000, u64::MAX)),
3
);
assert_eq!(
Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(3333, u64::MAX)),
3
);
assert_eq!(
Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(5500, u64::MAX)),
5
);
assert_eq!(
Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(7777, u64::MAX)),
7
);
assert_eq!(
Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(9999, u64::MAX)),
9
);
assert_eq!(
Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(10_000, u64::MAX)),
10
);
assert_eq!(
Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(10_999, u64::MAX)),
10
);
assert_eq!(
Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(11_000, u64::MAX)),
10
);
assert_eq!(
Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(22_000, u64::MAX)),
10
);
let w = SolutionOrSnapshotSize { voters: 1, targets: 0 };
assert_eq!(
Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(0, u64::MAX)),
0
);
assert_eq!(
Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(1, u64::MAX)),
0
);
assert_eq!(
Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(999, u64::MAX)),
0
);
assert_eq!(
Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(1000, u64::MAX)),
1
);
assert_eq!(
Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(1001, u64::MAX)),
1
);
assert_eq!(
Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(1990, u64::MAX)),
1
);
assert_eq!(
Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(1999, u64::MAX)),
1
);
assert_eq!(
Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(2000, u64::MAX)),
1
);
assert_eq!(
Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(2001, u64::MAX)),
1
);
assert_eq!(
Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(2010, u64::MAX)),
1
);
assert_eq!(
Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(3333, u64::MAX)),
1
);
let w = SolutionOrSnapshotSize { voters: 2, targets: 0 };
assert_eq!(
Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(0, u64::MAX)),
0
);
assert_eq!(
Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(1, u64::MAX)),
0
);
assert_eq!(
Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(999, u64::MAX)),
0
);
assert_eq!(
Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(1000, u64::MAX)),
1
);
assert_eq!(
Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(1001, u64::MAX)),
1
);
assert_eq!(
Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(1999, u64::MAX)),
1
);
assert_eq!(
Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(2000, u64::MAX)),
2
);
assert_eq!(
Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(2001, u64::MAX)),
2
);
assert_eq!(
Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(2010, u64::MAX)),
2
);
assert_eq!(
Miner::<Runtime>::maximum_voter_for_weight(0, w, Weight::from_parts(3333, u64::MAX)),
2
);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
mock::{
multi_phase_events, roll_to, roll_to_signed, roll_to_unsigned, roll_to_with_ocw,
trim_helpers, witness, BlockNumber, ExtBuilder, Extrinsic, MinerMaxWeight, MultiPhase,
Runtime, RuntimeCall, RuntimeOrigin, System, TestNposSolution, TrimHelpers,
UnsignedPhase,
},
Event, InvalidTransaction, Phase, QueuedSolution, TransactionSource,
TransactionValidityError,
};
use alloc::vec;
use codec::Decode;
use frame_election_provider_support::IndexAssignment;
use frame_support::{assert_noop, assert_ok, traits::OffchainWorker};
use sp_npos_elections::ElectionScore;
use sp_runtime::{
bounded_vec,
offchain::storage_lock::{BlockAndTime, StorageLock},
traits::{Dispatchable, ValidateUnsigned, Zero},
ModuleError, PerU16,
};
type Assignment = crate::unsigned::Assignment<Runtime>;
#[test]
fn validate_unsigned_retracts_wrong_phase() {
ExtBuilder::default().desired_targets(0).build_and_execute(|| {
let solution = RawSolution::<TestNposSolution> {
score: ElectionScore { minimal_stake: 5, ..Default::default() },
..Default::default()
};
let call = Call::submit_unsigned {
raw_solution: Box::new(solution.clone()),
witness: witness(),
};
assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Off);
assert!(matches!(
<MultiPhase as ValidateUnsigned>::validate_unsigned(
TransactionSource::Local,
&call
)
.unwrap_err(),
TransactionValidityError::Invalid(InvalidTransaction::Custom(0))
));
assert!(matches!(
<MultiPhase as ValidateUnsigned>::pre_dispatch(&call).unwrap_err(),
TransactionValidityError::Invalid(InvalidTransaction::Custom(0))
));
roll_to_signed();
assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Signed);
assert!(matches!(
<MultiPhase as ValidateUnsigned>::validate_unsigned(
TransactionSource::Local,
&call
)
.unwrap_err(),
TransactionValidityError::Invalid(InvalidTransaction::Custom(0))
));
assert!(matches!(
<MultiPhase as ValidateUnsigned>::pre_dispatch(&call).unwrap_err(),
TransactionValidityError::Invalid(InvalidTransaction::Custom(0))
));
roll_to_unsigned();
assert!(CurrentPhase::<Runtime>::get().is_unsigned());
assert!(<MultiPhase as ValidateUnsigned>::validate_unsigned(
TransactionSource::Local,
&call
)
.is_ok());
assert!(<MultiPhase as ValidateUnsigned>::pre_dispatch(&call).is_ok());
MultiPhase::phase_transition(Phase::Unsigned((false, 25)));
assert!(CurrentPhase::<Runtime>::get().is_unsigned());
assert!(matches!(
<MultiPhase as ValidateUnsigned>::validate_unsigned(
TransactionSource::Local,
&call
)
.unwrap_err(),
TransactionValidityError::Invalid(InvalidTransaction::Custom(0))
));
assert!(matches!(
<MultiPhase as ValidateUnsigned>::pre_dispatch(&call).unwrap_err(),
TransactionValidityError::Invalid(InvalidTransaction::Custom(0))
));
})
}
#[test]
fn validate_unsigned_retracts_low_score() {
ExtBuilder::default().desired_targets(0).build_and_execute(|| {
roll_to_unsigned();
assert!(CurrentPhase::<Runtime>::get().is_unsigned());
let solution = RawSolution::<TestNposSolution> {
score: ElectionScore { minimal_stake: 5, ..Default::default() },
..Default::default()
};
let call = Call::submit_unsigned {
raw_solution: Box::new(solution.clone()),
witness: witness(),
};
assert!(<MultiPhase as ValidateUnsigned>::validate_unsigned(
TransactionSource::Local,
&call
)
.is_ok());
assert!(<MultiPhase as ValidateUnsigned>::pre_dispatch(&call).is_ok());
let ready = ReadySolution {
score: ElectionScore { minimal_stake: 10, ..Default::default() },
..Default::default()
};
QueuedSolution::<Runtime>::put(ready);
assert!(matches!(
<MultiPhase as ValidateUnsigned>::validate_unsigned(
TransactionSource::Local,
&call
)
.unwrap_err(),
TransactionValidityError::Invalid(InvalidTransaction::Custom(2))
));
assert!(matches!(
<MultiPhase as ValidateUnsigned>::pre_dispatch(&call).unwrap_err(),
TransactionValidityError::Invalid(InvalidTransaction::Custom(2))
));
})
}
#[test]
fn validate_unsigned_retracts_incorrect_winner_count() {
ExtBuilder::default().desired_targets(1).build_and_execute(|| {
roll_to_unsigned();
assert!(CurrentPhase::<Runtime>::get().is_unsigned());
let raw = RawSolution::<TestNposSolution> {
score: ElectionScore { minimal_stake: 5, ..Default::default() },
..Default::default()
};
let call =
Call::submit_unsigned { raw_solution: Box::new(raw.clone()), witness: witness() };
assert_eq!(raw.solution.unique_targets().len(), 0);
assert!(matches!(
<MultiPhase as ValidateUnsigned>::validate_unsigned(
TransactionSource::Local,
&call
)
.unwrap_err(),
TransactionValidityError::Invalid(InvalidTransaction::Custom(1))
));
})
}
#[test]
fn priority_is_set() {
ExtBuilder::default()
.miner_tx_priority(20)
.desired_targets(0)
.build_and_execute(|| {
roll_to_unsigned();
assert!(CurrentPhase::<Runtime>::get().is_unsigned());
let solution = RawSolution::<TestNposSolution> {
score: ElectionScore { minimal_stake: 5, ..Default::default() },
..Default::default()
};
let call = Call::submit_unsigned {
raw_solution: Box::new(solution.clone()),
witness: witness(),
};
assert_eq!(
<MultiPhase as ValidateUnsigned>::validate_unsigned(
TransactionSource::Local,
&call
)
.unwrap()
.priority,
25
);
})
}
#[test]
#[should_panic(expected = "Invalid unsigned submission must produce invalid block and \
deprive validator from their authoring reward.: \
Module(ModuleError { index: 2, error: [1, 0, 0, 0], message: \
Some(\"PreDispatchWrongWinnerCount\") })")]
fn unfeasible_solution_panics() {
ExtBuilder::default().build_and_execute(|| {
roll_to_unsigned();
assert!(CurrentPhase::<Runtime>::get().is_unsigned());
let solution = RawSolution::<TestNposSolution> {
score: ElectionScore { minimal_stake: 5, ..Default::default() },
..Default::default()
};
let call = Call::submit_unsigned {
raw_solution: Box::new(solution.clone()),
witness: witness(),
};
let runtime_call: RuntimeCall = call.into();
let _ = runtime_call.dispatch(RuntimeOrigin::none());
})
}
#[test]
#[should_panic(expected = "Invalid unsigned submission must produce invalid block and \
deprive validator from their authoring reward.")]
fn wrong_witness_panics() {
ExtBuilder::default().build_and_execute(|| {
roll_to_unsigned();
assert!(CurrentPhase::<Runtime>::get().is_unsigned());
let solution = RawSolution::<TestNposSolution> {
score: ElectionScore { minimal_stake: 5, ..Default::default() },
..Default::default()
};
let mut correct_witness = witness();
correct_witness.voters += 1;
correct_witness.targets -= 1;
let call = Call::submit_unsigned {
raw_solution: Box::new(solution.clone()),
witness: correct_witness,
};
let runtime_call: RuntimeCall = call.into();
let _ = runtime_call.dispatch(RuntimeOrigin::none());
})
}
#[test]
fn miner_works() {
ExtBuilder::default().build_and_execute(|| {
roll_to_unsigned();
assert!(CurrentPhase::<Runtime>::get().is_unsigned());
assert!(Snapshot::<Runtime>::get().is_some());
assert_eq!(DesiredTargets::<Runtime>::get().unwrap(), 2);
let (solution, witness, _) = MultiPhase::mine_solution().unwrap();
assert!(QueuedSolution::<Runtime>::get().is_none());
assert_ok!(MultiPhase::submit_unsigned(
RuntimeOrigin::none(),
Box::new(solution),
witness
));
assert!(QueuedSolution::<Runtime>::get().is_some());
assert_eq!(
multi_phase_events(),
vec![
Event::PhaseTransitioned { from: Phase::Off, to: Phase::Signed, round: 1 },
Event::PhaseTransitioned {
from: Phase::Signed,
to: Phase::Unsigned((true, 25)),
round: 1
},
Event::SolutionStored {
compute: ElectionCompute::Unsigned,
origin: None,
prev_ejected: false
}
]
);
})
}
#[test]
fn miner_trims_weight() {
ExtBuilder::default()
.miner_weight(Weight::from_parts(100, u64::MAX))
.mock_weight_info(crate::mock::MockedWeightInfo::Basic)
.build_and_execute(|| {
roll_to_unsigned();
assert!(CurrentPhase::<Runtime>::get().is_unsigned());
let (raw, witness, t) = MultiPhase::mine_solution().unwrap();
let solution_weight = <Runtime as MinerConfig>::solution_weight(
witness.voters,
witness.targets,
raw.solution.voter_count() as u32,
raw.solution.unique_targets().len() as u32,
);
assert_eq!(solution_weight, Weight::from_parts(35, 0));
assert_eq!(raw.solution.voter_count(), 5);
assert_eq!(t.trimmed_weight(), 0);
<MinerMaxWeight>::set(Weight::from_parts(25, u64::MAX));
let (raw, witness, t) = MultiPhase::mine_solution().unwrap();
let solution_weight = <Runtime as MinerConfig>::solution_weight(
witness.voters,
witness.targets,
raw.solution.voter_count() as u32,
raw.solution.unique_targets().len() as u32,
);
assert_eq!(solution_weight, Weight::from_parts(25, 0));
assert_eq!(raw.solution.voter_count(), 3);
assert_eq!(t.trimmed_weight(), 2);
})
}
#[test]
fn miner_will_not_submit_if_not_enough_winners() {
let (mut ext, _) = ExtBuilder::default().desired_targets(8).build_offchainify(0);
ext.execute_with(|| {
roll_to_unsigned();
assert!(CurrentPhase::<Runtime>::get().is_unsigned());
let (mut solution, _, _) = MultiPhase::mine_solution().unwrap();
solution.solution.votes1[0].1 = 4;
assert_eq!(
MultiPhase::basic_checks(&solution, "mined").unwrap_err(),
MinerError::PreDispatchChecksFailed(DispatchError::Module(ModuleError {
index: 2,
error: [1, 0, 0, 0],
message: Some("PreDispatchWrongWinnerCount"),
})),
);
})
}
#[test]
fn unsigned_per_dispatch_checks_can_only_submit_threshold_better() {
ExtBuilder::default()
.desired_targets(1)
.add_voter(7, 2, bounded_vec![10])
.add_voter(8, 5, bounded_vec![10])
.add_voter(9, 1, bounded_vec![10])
.build_and_execute(|| {
roll_to_unsigned();
assert!(CurrentPhase::<Runtime>::get().is_unsigned());
assert_eq!(DesiredTargets::<Runtime>::get().unwrap(), 1);
let result = ElectionResult {
winners: vec![(10, 12)],
assignments: vec![
Assignment { who: 10, distribution: vec![(10, PerU16::one())] },
Assignment {
who: 7,
distribution: vec![(10, PerU16::one())],
},
],
};
let RoundSnapshot { voters, targets } = Snapshot::<Runtime>::get().unwrap();
let desired_targets = DesiredTargets::<Runtime>::get().unwrap();
let (raw, score, witness, _) =
Miner::<Runtime>::prepare_election_result_with_snapshot(
result,
voters.clone(),
targets.clone(),
desired_targets,
)
.unwrap();
let solution = RawSolution { solution: raw, score, round: Round::<Runtime>::get() };
assert_ok!(MultiPhase::unsigned_pre_dispatch_checks(&solution));
assert_ok!(MultiPhase::submit_unsigned(
RuntimeOrigin::none(),
Box::new(solution),
witness
));
assert_eq!(QueuedSolution::<Runtime>::get().unwrap().score.minimal_stake, 12);
let result = ElectionResult {
winners: vec![(10, 10)],
assignments: vec![Assignment {
who: 10,
distribution: vec![(10, PerU16::one())],
}],
};
let (raw, score, _, _) = Miner::<Runtime>::prepare_election_result_with_snapshot(
result,
voters.clone(),
targets.clone(),
desired_targets,
)
.unwrap();
let solution = RawSolution { solution: raw, score, round: Round::<Runtime>::get() };
assert_eq!(solution.score.minimal_stake, 10);
assert_noop!(
MultiPhase::unsigned_pre_dispatch_checks(&solution),
Error::<Runtime>::PreDispatchWeakSubmission,
);
let result = ElectionResult {
winners: vec![(10, 12)],
assignments: vec![
Assignment { who: 10, distribution: vec![(10, PerU16::one())] },
Assignment {
who: 7,
distribution: vec![(10, PerU16::one())],
},
],
};
let (raw, score, _, _) = Miner::<Runtime>::prepare_election_result_with_snapshot(
result,
voters.clone(),
targets.clone(),
desired_targets,
)
.unwrap();
let solution = RawSolution { solution: raw, score, round: Round::<Runtime>::get() };
assert_eq!(solution.score.minimal_stake, 12);
assert_noop!(
MultiPhase::unsigned_pre_dispatch_checks(&solution),
Error::<Runtime>::PreDispatchWeakSubmission,
);
let result = ElectionResult {
winners: vec![(10, 12)],
assignments: vec![
Assignment { who: 10, distribution: vec![(10, PerU16::one())] },
Assignment { who: 7, distribution: vec![(10, PerU16::one())] },
Assignment { who: 9, distribution: vec![(10, PerU16::one())] },
],
};
let (raw, score, witness, _) =
Miner::<Runtime>::prepare_election_result_with_snapshot(
result,
voters.clone(),
targets.clone(),
desired_targets,
)
.unwrap();
let solution = RawSolution { solution: raw, score, round: Round::<Runtime>::get() };
assert_eq!(solution.score.minimal_stake, 13);
assert_ok!(MultiPhase::unsigned_pre_dispatch_checks(&solution));
assert_ok!(MultiPhase::submit_unsigned(
RuntimeOrigin::none(),
Box::new(solution),
witness
));
let result = ElectionResult {
winners: vec![(10, 12)],
assignments: vec![
Assignment { who: 10, distribution: vec![(10, PerU16::one())] },
Assignment { who: 7, distribution: vec![(10, PerU16::one())] },
Assignment {
who: 8,
distribution: vec![(10, PerU16::one())],
},
],
};
let (raw, score, witness, _) =
Miner::<Runtime>::prepare_election_result_with_snapshot(
result,
voters.clone(),
targets.clone(),
desired_targets,
)
.unwrap();
let solution = RawSolution { solution: raw, score, round: Round::<Runtime>::get() };
assert_eq!(solution.score.minimal_stake, 17);
assert_ok!(MultiPhase::unsigned_pre_dispatch_checks(&solution));
assert_ok!(MultiPhase::submit_unsigned(
RuntimeOrigin::none(),
Box::new(solution),
witness
));
})
}
#[test]
fn ocw_lock_prevents_frequent_execution() {
let (mut ext, _) = ExtBuilder::default().build_offchainify(0);
ext.execute_with(|| {
let offchain_repeat = <Runtime as Config>::OffchainRepeat::get();
roll_to_unsigned();
assert!(CurrentPhase::<Runtime>::get().is_unsigned());
assert!(MultiPhase::ensure_offchain_repeat_frequency(25).is_ok());
assert_noop!(
MultiPhase::ensure_offchain_repeat_frequency(26),
MinerError::Lock("recently executed.")
);
assert!(
MultiPhase::ensure_offchain_repeat_frequency((26 + offchain_repeat).into()).is_ok()
);
assert!(MultiPhase::ensure_offchain_repeat_frequency(
(26 + offchain_repeat - 3).into()
)
.is_err());
assert!(MultiPhase::ensure_offchain_repeat_frequency(
(26 + offchain_repeat - 2).into()
)
.is_err());
assert!(MultiPhase::ensure_offchain_repeat_frequency(
(26 + offchain_repeat - 1).into()
)
.is_err());
})
}
#[test]
fn ocw_lock_released_after_successful_execution() {
let (mut ext, pool) = ExtBuilder::default().build_offchainify(0);
ext.execute_with(|| {
let guard = StorageValueRef::persistent(&OFFCHAIN_LOCK);
let last_block = StorageValueRef::persistent(OFFCHAIN_LAST_BLOCK);
roll_to_unsigned();
assert!(CurrentPhase::<Runtime>::get().is_unsigned());
assert!(guard.get::<bool>().unwrap().is_none());
MultiPhase::offchain_worker(25);
assert_eq!(pool.read().transactions.len(), 1);
assert!(guard.get::<bool>().unwrap().is_none());
assert_eq!(last_block.get::<BlockNumber>().unwrap(), Some(25));
});
}
#[test]
fn ocw_lock_prevents_overlapping_execution() {
let (mut ext, pool) = ExtBuilder::default().build_offchainify(0);
ext.execute_with(|| {
roll_to_unsigned();
assert!(CurrentPhase::<Runtime>::get().is_unsigned());
let mut lock = StorageLock::<BlockAndTime<System>>::with_block_deadline(
OFFCHAIN_LOCK,
UnsignedPhase::get().saturated_into(),
);
let guard = lock.lock();
MultiPhase::offchain_worker(25);
assert_eq!(pool.read().transactions.len(), 0);
MultiPhase::offchain_worker(26);
assert_eq!(pool.read().transactions.len(), 0);
drop(guard);
MultiPhase::offchain_worker(25);
assert_eq!(pool.read().transactions.len(), 1);
});
}
#[test]
fn ocw_only_runs_when_unsigned_open_now() {
let (mut ext, pool) = ExtBuilder::default().build_offchainify(0);
ext.execute_with(|| {
roll_to_unsigned();
assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Unsigned((true, 25)));
let mut storage = StorageValueRef::persistent(&OFFCHAIN_LAST_BLOCK);
MultiPhase::offchain_worker(24);
assert!(pool.read().transactions.len().is_zero());
storage.clear();
MultiPhase::offchain_worker(25);
assert_eq!(pool.read().transactions.len(), 1);
pool.try_write().unwrap().transactions.clear();
MultiPhase::offchain_worker(26);
assert!(pool.read().transactions.len().is_zero());
})
}
#[test]
fn ocw_clears_cache_on_unsigned_phase_open() {
let (mut ext, pool) = ExtBuilder::default().build_offchainify(0);
ext.execute_with(|| {
const BLOCK: u64 = 25;
let block_plus = |delta: u64| BLOCK + delta;
let offchain_repeat = <Runtime as Config>::OffchainRepeat::get();
roll_to(BLOCK);
assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Unsigned((true, BLOCK)));
assert!(
!ocw_solution_exists::<Runtime>(),
"no solution should be present before we mine one",
);
MultiPhase::offchain_worker(BLOCK);
assert!(
ocw_solution_exists::<Runtime>(),
"a solution must be cached after running the worker",
);
let tx_cache_1 = pool.read().transactions[0].clone();
pool.try_write().unwrap().transactions.clear();
let _ = MultiPhase::do_elect();
MultiPhase::offchain_worker(block_plus(1));
assert!(ocw_solution_exists::<Runtime>(), "elections does not clear the ocw cache");
MultiPhase::offchain_worker(block_plus(offchain_repeat + 1));
let tx_cache_2 = pool.read().transactions[0].clone();
pool.try_write().unwrap().transactions.clear();
assert_eq!(tx_cache_1, tx_cache_2);
let current_block = block_plus(offchain_repeat * 2 + 2);
MultiPhase::phase_transition(Phase::Unsigned((true, current_block)));
MultiPhase::offchain_worker(current_block);
let tx_cache_3 = pool.read().transactions[0].clone();
assert_eq!(tx_cache_1, tx_cache_3);
assert_eq!(
multi_phase_events(),
vec![
Event::PhaseTransitioned { from: Phase::Off, to: Phase::Signed, round: 1 },
Event::PhaseTransitioned {
from: Phase::Signed,
to: Phase::Unsigned((true, 25)),
round: 1
},
Event::ElectionFinalized {
compute: ElectionCompute::Fallback,
score: ElectionScore {
minimal_stake: 0,
sum_stake: 0,
sum_stake_squared: 0
}
},
Event::PhaseTransitioned {
from: Phase::Unsigned((true, 25)),
to: Phase::Unsigned((true, 37)),
round: 1
},
]
);
})
}
#[test]
fn ocw_resubmits_after_offchain_repeat() {
let (mut ext, pool) = ExtBuilder::default().build_offchainify(0);
ext.execute_with(|| {
const BLOCK: u64 = 25;
let block_plus = |delta: i32| ((BLOCK as i32) + delta) as u64;
let offchain_repeat = <Runtime as Config>::OffchainRepeat::get();
roll_to(BLOCK);
assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Unsigned((true, BLOCK)));
let mut storage = StorageValueRef::persistent(&OFFCHAIN_LAST_BLOCK);
MultiPhase::offchain_worker(block_plus(-1));
assert!(pool.read().transactions.len().is_zero());
storage.clear();
MultiPhase::offchain_worker(BLOCK);
assert_eq!(pool.read().transactions.len(), 1);
let tx_cache = pool.read().transactions[0].clone();
pool.try_write().unwrap().transactions.clear();
MultiPhase::offchain_worker(block_plus(1 + offchain_repeat as i32));
assert_eq!(pool.read().transactions.len(), 1);
let tx = &pool.read().transactions[0];
assert_eq!(&tx_cache, tx);
})
}
#[test]
fn ocw_regenerates_and_resubmits_after_offchain_repeat() {
let (mut ext, pool) = ExtBuilder::default().build_offchainify(0);
ext.execute_with(|| {
const BLOCK: u64 = 25;
let block_plus = |delta: i32| ((BLOCK as i32) + delta) as u64;
let offchain_repeat = <Runtime as Config>::OffchainRepeat::get();
roll_to(BLOCK);
assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Unsigned((true, BLOCK)));
let mut storage = StorageValueRef::persistent(&OFFCHAIN_LAST_BLOCK);
MultiPhase::offchain_worker(block_plus(-1));
assert!(pool.read().transactions.len().is_zero());
storage.clear();
MultiPhase::offchain_worker(BLOCK);
assert_eq!(pool.read().transactions.len(), 1);
let tx_cache = pool.read().transactions[0].clone();
pool.try_write().unwrap().transactions.clear();
let mut call_cache = StorageValueRef::persistent(&OFFCHAIN_CACHED_CALL);
assert!(matches!(call_cache.get::<Call<Runtime>>(), Ok(Some(_call))));
call_cache.clear();
MultiPhase::offchain_worker(block_plus(1 + offchain_repeat as i32));
assert_eq!(pool.read().transactions.len(), 1);
let tx = &pool.read().transactions[0];
assert_eq!(&tx_cache, tx);
})
}
#[test]
fn ocw_can_submit_to_pool() {
let (mut ext, pool) = ExtBuilder::default().build_offchainify(0);
ext.execute_with(|| {
roll_to_with_ocw(25);
assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Unsigned((true, 25)));
let encoded = pool.read().transactions[0].clone();
let extrinsic: Extrinsic = codec::Decode::decode(&mut &*encoded).unwrap();
let call = extrinsic.function;
assert!(matches!(call, RuntimeCall::MultiPhase(Call::submit_unsigned { .. })));
})
}
#[test]
fn ocw_solution_must_have_correct_round() {
let (mut ext, pool) = ExtBuilder::default().build_offchainify(0);
ext.execute_with(|| {
roll_to_with_ocw(25);
assert_eq!(CurrentPhase::<Runtime>::get(), Phase::Unsigned((true, 25)));
crate::Round::<Runtime>::mutate(|round| *round += 1);
let encoded = pool.read().transactions[0].clone();
let extrinsic = Extrinsic::decode(&mut &*encoded).unwrap();
let call = match extrinsic.function {
RuntimeCall::MultiPhase(call @ Call::submit_unsigned { .. }) => call,
_ => panic!("bad call: unexpected submission"),
};
let pre_dispatch_check_error =
TransactionValidityError::Invalid(InvalidTransaction::Custom(7));
assert_eq!(
<MultiPhase as ValidateUnsigned>::validate_unsigned(
TransactionSource::Local,
&call,
)
.unwrap_err(),
pre_dispatch_check_error,
);
assert_eq!(
<MultiPhase as ValidateUnsigned>::pre_dispatch(&call).unwrap_err(),
pre_dispatch_check_error,
);
})
}
#[test]
fn trim_assignments_length_does_not_modify_when_short_enough() {
ExtBuilder::default().build_and_execute(|| {
roll_to_unsigned();
let TrimHelpers { mut assignments, encoded_size_of, .. } = trim_helpers();
let solution = SolutionOf::<Runtime>::try_from(assignments.as_slice()).unwrap();
let encoded_len = solution.encoded_size() as u32;
let solution_clone = solution.clone();
let trimmed_len = Miner::<Runtime>::trim_assignments_length(
encoded_len,
&mut assignments,
encoded_size_of,
)
.unwrap();
let solution = SolutionOf::<Runtime>::try_from(assignments.as_slice()).unwrap();
assert_eq!(solution, solution_clone);
assert_eq!(trimmed_len, 0);
});
}
#[test]
fn trim_assignments_length_modifies_when_too_long() {
ExtBuilder::default().build().execute_with(|| {
roll_to_unsigned();
let TrimHelpers { mut assignments, encoded_size_of, .. } = trim_helpers();
let solution = SolutionOf::<Runtime>::try_from(assignments.as_slice()).unwrap();
let encoded_len = solution.encoded_size();
let solution_clone = solution.clone();
let trimmed_len = Miner::<Runtime>::trim_assignments_length(
encoded_len as u32 - 1,
&mut assignments,
encoded_size_of,
)
.unwrap();
let solution = SolutionOf::<Runtime>::try_from(assignments.as_slice()).unwrap();
assert_ne!(solution, solution_clone);
assert!(solution.encoded_size() < encoded_len);
assert_eq!(trimmed_len, 1);
});
}
#[test]
fn trim_assignments_length_trims_lowest_stake() {
ExtBuilder::default().build().execute_with(|| {
roll_to_unsigned();
let TrimHelpers { voters, mut assignments, encoded_size_of, voter_index } =
trim_helpers();
let solution = SolutionOf::<Runtime>::try_from(assignments.as_slice()).unwrap();
let encoded_len = solution.encoded_size() as u32;
let count = assignments.len();
let min_stake_voter = voters
.iter()
.map(|(id, weight, _)| (weight, id))
.min()
.and_then(|(_, id)| voter_index(id))
.unwrap();
Miner::<Runtime>::trim_assignments_length(
encoded_len - 1,
&mut assignments,
encoded_size_of,
)
.unwrap();
assert_eq!(assignments.len(), count - 1, "we must have removed exactly one assignment");
assert!(
assignments.iter().all(|IndexAssignment { who, .. }| *who != min_stake_voter),
"min_stake_voter must no longer be in the set of voters",
);
});
}
#[test]
fn trim_assignments_length_wont_panic() {
ExtBuilder::default().build_and_execute(|| {
let encoded_size_of = Box::new(|assignments: &[IndexAssignmentOf<Runtime>]| {
SolutionOf::<Runtime>::try_from(assignments).map(|solution| solution.encoded_size())
});
let mut assignments = vec![];
let min_solution_size = encoded_size_of(&assignments).unwrap();
assert_eq!(min_solution_size, SolutionOf::<Runtime>::LIMIT);
Miner::<Runtime>::trim_assignments_length(0, &mut assignments, encoded_size_of.clone())
.unwrap();
Miner::<Runtime>::trim_assignments_length(1, &mut assignments, encoded_size_of.clone())
.unwrap();
Miner::<Runtime>::trim_assignments_length(
min_solution_size as u32,
&mut assignments,
encoded_size_of,
)
.unwrap();
});
ExtBuilder::default().build_and_execute(|| {
roll_to_unsigned();
let TrimHelpers { mut assignments, encoded_size_of, .. } = trim_helpers();
assert!(assignments.len() > 0);
let min_solution_size = SolutionOf::<Runtime>::LIMIT as u32;
Miner::<Runtime>::trim_assignments_length(
min_solution_size,
&mut assignments,
encoded_size_of,
)
.unwrap();
assert_eq!(assignments.len(), 0);
});
}
#[test]
fn mine_solution_solutions_always_within_acceptable_length() {
ExtBuilder::default().build_and_execute(|| {
roll_to_unsigned();
let solution = MultiPhase::mine_solution().unwrap();
let max_length = <Runtime as MinerConfig>::MaxLength::get();
let solution_size = solution.0.solution.encoded_size();
assert!(solution_size <= max_length as usize);
<Runtime as MinerConfig>::MaxLength::set(solution_size as u32 - 1);
let solution = MultiPhase::mine_solution().unwrap();
let max_length = <Runtime as MinerConfig>::MaxLength::get();
let solution_size = solution.0.solution.encoded_size();
assert!(solution_size <= max_length as usize);
});
}
}