Skip to content
This repository has been archived by the owner on Nov 15, 2023. It is now read-only.

Companion for EPM duplicate submissions #6115

Merged
merged 15 commits into from
Oct 19, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
360 changes: 180 additions & 180 deletions Cargo.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions runtime/kusama/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1478,6 +1478,7 @@ pub type Executive = frame_executive::Executive<
pallet_multisig::migrations::v1::MigrateToV1<Runtime>,
// "Properly migrate weights to v2" <https://github.com/paritytech/polkadot/pull/6091>
parachains_configuration::migration::v3::MigrateToV3<Runtime>,
pallet_election_provider_multi_phase::migrations::v1::MigrateToV1<Runtime>,
),
>;
/// The payload being signed in the transactions.
Expand Down
1 change: 1 addition & 0 deletions runtime/polkadot/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1571,6 +1571,7 @@ pub type Executive = frame_executive::Executive<
pallet_multisig::migrations::v1::MigrateToV1<Runtime>,
// "Properly migrate weights to v2" <https://github.com/paritytech/polkadot/pull/6091>
parachains_configuration::migration::v3::MigrateToV3<Runtime>,
pallet_election_provider_multi_phase::migrations::v1::MigrateToV1<Runtime>,
),
>;

Expand Down
1 change: 1 addition & 0 deletions runtime/westend/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1222,6 +1222,7 @@ pub type Executive = frame_executive::Executive<
pallet_multisig::migrations::v1::MigrateToV1<Runtime>,
// "Properly migrate weights to v2" <https://github.com/paritytech/polkadot/pull/6091>
parachains_configuration::migration::v3::MigrateToV3<Runtime>,
pallet_election_provider_multi_phase::migrations::v1::MigrateToV1<Runtime>,
),
>;
/// The payload being signed in transactions.
Expand Down
31 changes: 27 additions & 4 deletions utils/staking-miner/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,7 @@ enum Error<T: EPM::Config> {
AlreadySubmitted,
VersionMismatch,
StrategyNotSatisfied,
QueueFull,
Other(String),
}

Expand Down Expand Up @@ -425,17 +426,39 @@ fn mine_dpos<T: EPM::Config>(ext: &mut Ext) -> Result<(), Error<T>> {

pub(crate) async fn check_versions<T: frame_system::Config + EPM::Config>(
rpc: &SharedRpcClient,
print: bool,
) -> Result<(), Error<T>> {
let linked_version = T::Version::get();
let on_chain_version = rpc
.runtime_version(None)
.await
.expect("runtime version RPC should always work; qed");

log::debug!(target: LOG_TARGET, "linked version {:?}", linked_version);
log::debug!(target: LOG_TARGET, "on-chain version {:?}", on_chain_version);
let do_print = || {
log::info!(
target: LOG_TARGET,
"linked version {:?}",
(&linked_version.spec_name, &linked_version.spec_version)
);
log::info!(
target: LOG_TARGET,
"on-chain version {:?}",
(&on_chain_version.spec_name, &on_chain_version.spec_version)
);
};

if print {
do_print();
}

if linked_version != on_chain_version {
// we relax the checking here a bit, which should not cause any issues in production (a chain
// that messes up its spec name is highly unlikely), but it allows us to do easier testing.
kianenigma marked this conversation as resolved.
Show resolved Hide resolved
if linked_version.spec_name != on_chain_version.spec_name ||
linked_version.spec_version != on_chain_version.spec_version
{
if !print {
do_print();
}
log::error!(
target: LOG_TARGET,
"VERSION MISMATCH: any transaction will fail with bad-proof"
Expand Down Expand Up @@ -563,7 +586,7 @@ async fn main() {
log::info!(target: LOG_TARGET, "connected to chain {:?}", chain);

any_runtime_unit! {
check_versions::<Runtime>(&rpc).await
check_versions::<Runtime>(&rpc, true).await
};

let outcome = any_runtime! {
Expand Down
122 changes: 84 additions & 38 deletions utils/staking-miner/src/monitor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ where
.map_err::<Error<T>, _>(Into::into)?
.unwrap_or_default();

for (_score, idx) in indices {
for (_score, _bn, idx) in indices {
let key = StorageKey(EPM::SignedSubmissionsMap::<T>::hashed_key_for(idx));

if let Some(submission) = rpc
Expand All @@ -81,20 +81,36 @@ where
Ok(())
}

/// `true` if `our_score` should pass the onchain `best_score` with the given strategy.
pub(crate) fn score_passes_strategy(
our_score: sp_npos_elections::ElectionScore,
best_score: sp_npos_elections::ElectionScore,
strategy: SubmissionStrategy,
) -> bool {
match strategy {
SubmissionStrategy::Always => true,
SubmissionStrategy::IfLeading =>
our_score == best_score ||
our_score.strict_threshold_better(best_score, Perbill::zero()),
SubmissionStrategy::ClaimBetterThan(epsilon) =>
our_score.strict_threshold_better(best_score, epsilon),
SubmissionStrategy::ClaimNoWorseThan(epsilon) =>
!best_score.strict_threshold_better(our_score, epsilon),
}
}

/// Reads all current solutions and checks the scores according to the `SubmissionStrategy`.
async fn ensure_no_better_solution<T: EPM::Config, B: BlockT>(
async fn ensure_strategy_met<T: EPM::Config, B: BlockT>(
rpc: &SharedRpcClient,
at: Hash,
score: sp_npos_elections::ElectionScore,
strategy: SubmissionStrategy,
max_submissions: u32,
) -> Result<(), Error<T>> {
let epsilon = match strategy {
// don't care about current scores.
SubmissionStrategy::Always => return Ok(()),
SubmissionStrategy::IfLeading => Perbill::zero(),
SubmissionStrategy::ClaimBetterThan(epsilon) => epsilon,
};
// don't care about current scores.
if matches!(strategy, SubmissionStrategy::Always) {
return Ok(())
}

let indices_key = StorageKey(EPM::SignedSubmissionIndices::<T>::hashed_key().to_vec());

Expand All @@ -104,34 +120,16 @@ async fn ensure_no_better_solution<T: EPM::Config, B: BlockT>(
.map_err::<Error<T>, _>(Into::into)?
.unwrap_or_default();

let mut is_best_score = true;
let mut scores = 0;

log::debug!(target: LOG_TARGET, "submitted solutions on chain: {:?}", indices);

// BTreeMap is ordered, take last to get the max score.
for (curr_max_score, _) in indices.into_iter() {
if !score.strict_threshold_better(curr_max_score, epsilon) {
log::warn!(target: LOG_TARGET, "mined score is not better; skipping to submit");
is_best_score = false;
}

if score == curr_max_score {
log::warn!(
target: LOG_TARGET,
"mined score has the same score as already submitted score"
);
}

// Indices can't be bigger than u32::MAX so can't overflow.
scores += 1;
// we check the queue here as well. Could be checked elsewhere.
if indices.len() as u32 >= max_submissions {
return Err(Error::<T>::QueueFull)
}

if scores == max_submissions {
log::warn!(target: LOG_TARGET, "The submissions queue is full");
}
// default score is all zeros, any score is better than it.
let best_score = indices.last().map(|(score, _, _)| *score).unwrap_or_default();
niklasad1 marked this conversation as resolved.
Show resolved Hide resolved
log::debug!(target: LOG_TARGET, "best onchain score is {:?}", best_score);

if is_best_score {
if score_passes_strategy(score, best_score, strategy) {
Ok(())
} else {
Err(Error::StrategyNotSatisfied)
Expand Down Expand Up @@ -233,7 +231,7 @@ macro_rules! monitor_cmd_for { ($runtime:tt) => { paste::paste! {

// block on this because if this fails there is no way to recover from
// that error i.e, upgrade/downgrade required.
if let Err(err) = crate::check_versions::<Runtime>(&rpc).await {
if let Err(err) = crate::check_versions::<Runtime>(&rpc, false).await {
let _ = tx.send(err.into());
return;
}
Expand Down Expand Up @@ -314,9 +312,14 @@ macro_rules! monitor_cmd_for { ($runtime:tt) => { paste::paste! {
}
};

let ensure_no_better_fut = tokio::spawn(async move {
ensure_no_better_solution::<Runtime, Block>(&rpc1, latest_head, score, config.submission_strategy,
SignedMaxSubmissions::get()).await
let ensure_strategy_met_fut = tokio::spawn(async move {
ensure_strategy_met::<Runtime, Block>(
&rpc1,
latest_head,
score,
config.submission_strategy,
SignedMaxSubmissions::get()
).await
});

let ensure_signed_phase_fut = tokio::spawn(async move {
Expand All @@ -325,7 +328,7 @@ macro_rules! monitor_cmd_for { ($runtime:tt) => { paste::paste! {

// Run the calls in parallel and return once all has completed or any failed.
if let Err(err) = tokio::try_join!(
flatten(ensure_no_better_fut),
flatten(ensure_strategy_met_fut),
flatten(ensure_signed_phase_fut),
) {
log::debug!(target: LOG_TARGET, "Skipping to submit at block {}; {}", at.number, err);
Expand Down Expand Up @@ -420,3 +423,46 @@ macro_rules! monitor_cmd_for { ($runtime:tt) => { paste::paste! {
monitor_cmd_for!(polkadot);
monitor_cmd_for!(kusama);
monitor_cmd_for!(westend);

#[cfg(test)]
pub mod tests {
use super::*;

#[test]
fn score_passes_strategy_works() {
let s = |x| sp_npos_elections::ElectionScore { minimal_stake: x, ..Default::default() };
let two = Perbill::from_percent(2);

// anything passes Always
assert!(score_passes_strategy(s(0), s(0), SubmissionStrategy::Always));
assert!(score_passes_strategy(s(5), s(0), SubmissionStrategy::Always));
assert!(score_passes_strategy(s(5), s(10), SubmissionStrategy::Always));

// if leading
assert!(score_passes_strategy(s(0), s(0), SubmissionStrategy::IfLeading));
assert!(score_passes_strategy(s(1), s(0), SubmissionStrategy::IfLeading));
assert!(score_passes_strategy(s(2), s(0), SubmissionStrategy::IfLeading));
assert!(!score_passes_strategy(s(5), s(10), SubmissionStrategy::IfLeading));
assert!(!score_passes_strategy(s(9), s(10), SubmissionStrategy::IfLeading));
assert!(score_passes_strategy(s(10), s(10), SubmissionStrategy::IfLeading));

// if better by 2%
assert!(!score_passes_strategy(s(50), s(100), SubmissionStrategy::ClaimBetterThan(two)));
assert!(!score_passes_strategy(s(100), s(100), SubmissionStrategy::ClaimBetterThan(two)));
assert!(!score_passes_strategy(s(101), s(100), SubmissionStrategy::ClaimBetterThan(two)));
assert!(!score_passes_strategy(s(102), s(100), SubmissionStrategy::ClaimBetterThan(two)));
assert!(score_passes_strategy(s(103), s(100), SubmissionStrategy::ClaimBetterThan(two)));
assert!(score_passes_strategy(s(150), s(100), SubmissionStrategy::ClaimBetterThan(two)));

// if no less than 2% worse
assert!(!score_passes_strategy(s(50), s(100), SubmissionStrategy::ClaimNoWorseThan(two)));
assert!(!score_passes_strategy(s(97), s(100), SubmissionStrategy::ClaimNoWorseThan(two)));
assert!(score_passes_strategy(s(98), s(100), SubmissionStrategy::ClaimNoWorseThan(two)));
assert!(score_passes_strategy(s(99), s(100), SubmissionStrategy::ClaimNoWorseThan(two)));
assert!(score_passes_strategy(s(100), s(100), SubmissionStrategy::ClaimNoWorseThan(two)));
assert!(score_passes_strategy(s(101), s(100), SubmissionStrategy::ClaimNoWorseThan(two)));
assert!(score_passes_strategy(s(102), s(100), SubmissionStrategy::ClaimNoWorseThan(two)));
assert!(score_passes_strategy(s(103), s(100), SubmissionStrategy::ClaimNoWorseThan(two)));
assert!(score_passes_strategy(s(150), s(100), SubmissionStrategy::ClaimNoWorseThan(two)));
}
}
24 changes: 16 additions & 8 deletions utils/staking-miner/src/opts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,9 @@ pub(crate) struct MonitorConfig {
/// `--submission-strategy always`: always submit.
///
/// `--submission-strategy "percent-better <percent>"`: submit if the submission is `n` percent better.
#[arg(long, default_value = "if-leading")]
///
/// `--submission-strategy "no-worse-than <percent>"`: submit if submission is no more than `n` percent worse.
#[clap(long, default_value = "if-leading")]
pub submission_strategy: SubmissionStrategy,

/// Delay in number seconds to wait until starting mining a solution.
Expand Down Expand Up @@ -157,12 +159,14 @@ pub(crate) struct InfoOpts {
#[derive(Debug, Copy, Clone)]
#[cfg_attr(test, derive(PartialEq))]
pub enum SubmissionStrategy {
// Only submit if at the time, we are the best.
IfLeading,
// Always submit.
/// Always submit.
Always,
// Submit if we are leading, or if the solution that's leading is more that the given `Perbill`
// better than us. This helps detect obviously fake solutions and still combat them.
/// Only submit if at the time, we are the best (or equal to it).
IfLeading,
/// Submit if we are no worse than `Perbill` worse than the best.
ClaimNoWorseThan(Perbill),
/// Submit if we are leading, or if the solution that's leading is more that the given `Perbill`
/// better than us. This helps detect obviously fake solutions and still combat them.
ClaimBetterThan(Perbill),
}

Expand All @@ -185,6 +189,7 @@ pub(crate) enum Solver {
/// * --submission-strategy if-leading: only submit if leading
/// * --submission-strategy always: always submit
/// * --submission-strategy "percent-better <percent>": submit if submission is `n` percent better.
/// * --submission-strategy "no-worse-than<percent>": submit if submission is no more than `n` percent worse.
///
impl FromStr for SubmissionStrategy {
type Err = String;
Expand All @@ -196,8 +201,11 @@ impl FromStr for SubmissionStrategy {
Self::IfLeading
} else if s == "always" {
Self::Always
} else if s.starts_with("percent-better ") {
let percent: u32 = s[15..].parse().map_err(|e| format!("{:?}", e))?;
} else if let Some(percent) = s.strip_prefix("no-worse-than ") {
let percent: u32 = percent.parse().map_err(|e| format!("{:?}", e))?;
Self::ClaimNoWorseThan(Perbill::from_percent(percent))
} else if let Some(percent) = s.strip_prefix("percent-better ") {
let percent: u32 = percent.parse().map_err(|e| format!("{:?}", e))?;
Self::ClaimBetterThan(Perbill::from_percent(percent))
} else {
return Err(s.into())
Expand Down