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

Configurable call fee refund for signed submissions #11002

Merged
merged 31 commits into from
Apr 20, 2022
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
7054355
Refund call fee for all non-invalid signed submissions
emostov Mar 9, 2022
ee3553d
Clean up
emostov Mar 9, 2022
d25fa7b
Fix benchmarks
emostov Mar 10, 2022
c4fc0bd
Remove reward from struct
emostov Mar 10, 2022
fbe67f2
WIP SignedMaxRefunds
emostov Mar 10, 2022
9e0490d
Apply suggestions from code review
kianenigma Mar 12, 2022
301902d
Add test for ejected call_fee refunds
emostov Mar 14, 2022
6ff702a
Try merge remote
emostov Mar 14, 2022
13b88ba
Add test for number of calls refunded
emostov Mar 14, 2022
dce1792
Account for read op in mutate
emostov Mar 14, 2022
7f71199
Apply suggestions from code review
emostov Mar 14, 2022
72f1c37
Merge remote-tracking branch 'origin' into zeke-refund-miner-fee
emostov Mar 14, 2022
cc5da5e
Add to node runtime
emostov Mar 14, 2022
77e5384
Merge branch 'zeke-refund-miner-fee' of https://github.com/paritytech…
emostov Mar 14, 2022
0f20ee8
Don't refund ejected solutions
emostov Mar 15, 2022
9cb84d5
Merge remote-tracking branch 'origin' into zeke-refund-miner-fee
emostov Mar 15, 2022
d291d56
Update frame/election-provider-multi-phase/src/lib.rs
emostov Mar 18, 2022
e60b055
Inegrity test SignedMaxRefunds
emostov Mar 18, 2022
bbcbf50
Merge branch 'zeke-refund-miner-fee' of https://github.com/paritytech…
emostov Mar 18, 2022
f9eb103
Try merge master
emostov Mar 18, 2022
4867e2f
Use reward handle to refund call fee
emostov Mar 18, 2022
9f8a136
Fix node runtime build
emostov Apr 7, 2022
3c24e65
Try merge origin master
emostov Apr 7, 2022
07d8c9d
Drain in order of submission
emostov Apr 8, 2022
8f3bd5b
Merge remote-tracking branch 'origin' into zeke-refund-miner-fee
emostov Apr 8, 2022
1cf7847
Update frame/election-provider-multi-phase/src/signed.rs
emostov Apr 11, 2022
9dd0974
save
emostov Apr 13, 2022
b89fa57
Merge remote-tracking branch 'origin' into zeke-refund-miner-fee
emostov Apr 15, 2022
9482b3f
Update frame/election-provider-multi-phase/src/signed.rs
emostov Apr 16, 2022
1976d4e
Update frame/election-provider-multi-phase/src/signed.rs
niklasad1 Apr 17, 2022
af66618
Merge branch 'master' into zeke-refund-miner-fee
shawntabrizi Apr 20, 2022
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
1 change: 1 addition & 0 deletions bin/node/runtime/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -672,6 +672,7 @@ impl pallet_election_provider_multi_phase::Config for Runtime {
type SignedRewardBase = SignedRewardBase;
type SignedDepositBase = SignedDepositBase;
type SignedDepositByte = SignedDepositByte;
type SignedMaxRefunds = SignedMaxRefunds;
type SignedDepositWeight = ();
type SignedMaxWeight = MinerMaxWeight;
type SlashHandler = (); // burn slashes
Expand Down
18 changes: 14 additions & 4 deletions frame/election-provider-multi-phase/src/benchmarking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -225,14 +225,24 @@ frame_benchmarking::benchmarks! {
compute: Default::default()
};
let deposit: BalanceOf<T> = 10u32.into();
let reward: BalanceOf<T> = 20u32.into();

let reward: BalanceOf<T> = T::SignedRewardBase::get();
let call_fee: BalanceOf<T> = 30u32.into();

assert_ok!(T::Currency::reserve(&receiver, deposit));
assert_eq!(T::Currency::free_balance(&receiver), initial_balance - 10u32.into());
}: {
<MultiPhase<T>>::finalize_signed_phase_accept_solution(ready, &receiver, deposit, reward)
<MultiPhase<T>>::finalize_signed_phase_accept_solution(
ready,
&receiver,
deposit,
call_fee
)
} verify {
assert_eq!(T::Currency::free_balance(&receiver), initial_balance + 20u32.into());
assert_eq!(
T::Currency::free_balance(&receiver),
initial_balance + reward + call_fee
);
assert_eq!(T::Currency::reserved_balance(&receiver), 0u32.into());
}

Expand Down Expand Up @@ -333,7 +343,7 @@ frame_benchmarking::benchmarks! {
raw_solution,
who: account("submitters", i, SEED),
deposit: Default::default(),
reward: Default::default(),
call_fee: Default::default(),
};
signed_submissions.insert(signed_submission);
}
Expand Down
24 changes: 18 additions & 6 deletions frame/election-provider-multi-phase/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ use frame_support::{
use frame_system::{ensure_none, offchain::SendTransactionTypes};
use scale_info::TypeInfo;
use sp_arithmetic::{
traits::{Bounded, CheckedAdd, Saturating, Zero},
traits::{Bounded, CheckedAdd, Zero},
UpperOf,
};
use sp_npos_elections::{
Expand Down Expand Up @@ -623,6 +623,10 @@ pub mod pallet {
#[pallet::constant]
type SignedMaxWeight: Get<Weight>;

/// The maximum amount of unchecked solutions to refund the call fee for.
#[pallet::constant]
type SignedMaxRefunds: Get<u32>;

/// Base reward for a signed solution
#[pallet::constant]
type SignedRewardBase: Get<BalanceOf<Self>>;
Expand Down Expand Up @@ -843,6 +847,11 @@ pub mod pallet {
<T::DataProvider as ElectionDataProvider>::MaxVotesPerVoter::get(),
<SolutionOf<T> as NposSolution>::LIMIT as u32,
);

// While it won't cause any failures, setting `SignedMaxRefunds` gt
// `SignedMaxSubmissions` is a red flag that the developer does not understand how to
// configure this pallet.
assert!(T::SignedMaxSubmissions::get() >= T::SignedMaxRefunds::get());
}
}

Expand Down Expand Up @@ -983,14 +992,17 @@ pub mod pallet {

// create the submission
let deposit = Self::deposit_for(&raw_solution, size);
let reward = {
let call_fee = {
let call = Call::submit { raw_solution: raw_solution.clone() };
let call_fee = T::EstimateCallFee::estimate_call_fee(&call, None.into());
T::SignedRewardBase::get().saturating_add(call_fee)
emostov marked this conversation as resolved.
Show resolved Hide resolved
T::EstimateCallFee::estimate_call_fee(&call, None.into())
};

let submission =
SignedSubmission { who: who.clone(), deposit, raw_solution: *raw_solution, reward };
let submission = SignedSubmission {
who: who.clone(),
deposit,
raw_solution: *raw_solution,
call_fee,
kianenigma marked this conversation as resolved.
Show resolved Hide resolved
};

// insert the submission if the queue has space or it's better than the weakest
// eject the weakest if the queue was full
Expand Down
2 changes: 2 additions & 0 deletions frame/election-provider-multi-phase/src/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,7 @@ parameter_types! {
pub static SignedPhase: BlockNumber = 10;
pub static UnsignedPhase: BlockNumber = 5;
pub static SignedMaxSubmissions: u32 = 5;
pub static SignedMaxRefunds: u32 = 1;
pub static SignedDepositBase: Balance = 5;
pub static SignedDepositByte: Balance = 0;
pub static SignedDepositWeight: Balance = 0;
Expand Down Expand Up @@ -424,6 +425,7 @@ impl crate::Config for Runtime {
type SignedDepositWeight = ();
type SignedMaxWeight = SignedMaxWeight;
type SignedMaxSubmissions = SignedMaxSubmissions;
type SignedMaxRefunds = SignedMaxRefunds;
type SlashHandler = ();
type RewardHandler = ();
type DataProvider = StakingMock;
Expand Down
123 changes: 98 additions & 25 deletions frame/election-provider-multi-phase/src/signed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ pub struct SignedSubmission<AccountId, Balance: HasCompact, Solution> {
pub deposit: Balance,
/// The raw solution itself.
pub raw_solution: RawSolution<Solution>,
/// The reward that should potentially be paid for this solution, if accepted.
pub reward: Balance,
// The estimated fee `who` paid to submit the solution.
pub call_fee: Balance,
kianenigma marked this conversation as resolved.
Show resolved Hide resolved
}

impl<AccountId, Balance, Solution> Ord for SignedSubmission<AccountId, Balance, Solution>
Expand Down Expand Up @@ -235,20 +235,34 @@ impl<T: Config> SignedSubmissions<T> {
}

/// Empty the set of signed submissions, returning an iterator of signed submissions in
/// arbitrary order.
/// order of submission.
///
/// Note that if the iterator is dropped without consuming all elements, not all may be removed
/// from the underlying `SignedSubmissionsMap`, putting the storages into an invalid state.
///
/// Note that, like `put`, this function consumes `Self` and modifies storage.
fn drain(mut self) -> impl Iterator<Item = SignedSubmissionOf<T>> {
fn drain_submitted_order(mut self) -> impl Iterator<Item = SignedSubmissionOf<T>> {
let mut keys = SignedSubmissionsMap::<T>::iter_keys()
.filter(|k| {
if self.deletion_overlay.contains(k) {
// Remove submissions that should be deleted.
SignedSubmissionsMap::<T>::remove(k);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think SignedSubmissionsMap::<T>::remove(k) (side-effects) in filter is a bit unexpected...

Personally I would prefer filter_map or something even if the actual iterator isn't affected by it.

false
} else {
true
}
})
.chain(self.insertion_overlay.keys().map(|n| *n))
emostov marked this conversation as resolved.
Show resolved Hide resolved
.collect::<Vec<_>>();
keys.sort();

SignedSubmissionIndices::<T>::kill();
SignedSubmissionNextIndex::<T>::kill();
let insertion_overlay = sp_std::mem::take(&mut self.insertion_overlay);
SignedSubmissionsMap::<T>::drain()
.filter(move |(k, _v)| !self.deletion_overlay.contains(k))
.map(|(_k, v)| v)
.chain(insertion_overlay.into_iter().map(|(_k, v)| v))
let _ = sp_std::mem::take(&mut self.deletion_overlay);
emostov marked this conversation as resolved.
Show resolved Hide resolved
emostov marked this conversation as resolved.
Show resolved Hide resolved

keys.into_iter().filter_map(move |index| {
SignedSubmissionsMap::<T>::take(index).or_else(|| self.insertion_overlay.remove(&index))
})
}

/// Decode the length of the signed submissions without actually reading the entire struct into
Expand Down Expand Up @@ -362,7 +376,7 @@ impl<T: Config> Pallet<T> {
Self::snapshot_metadata().unwrap_or_default();

while let Some(best) = all_submissions.pop_last() {
let SignedSubmission { raw_solution, who, deposit, reward } = best;
let SignedSubmission { raw_solution, who, deposit, call_fee } = best;
let active_voters = raw_solution.solution.voter_count() as u32;
let feasibility_weight = {
// defensive only: at the end of signed phase, snapshot will exits.
Expand All @@ -377,7 +391,7 @@ impl<T: Config> Pallet<T> {
ready_solution,
&who,
deposit,
reward,
call_fee,
);
found_solution = true;

Expand All @@ -396,10 +410,23 @@ impl<T: Config> Pallet<T> {
// Any unprocessed solution is pointless to even consider. Feasible or malicious,
// they didn't end up being used. Unreserve the bonds.
let discarded = all_submissions.len();
for SignedSubmission { who, deposit, .. } in all_submissions.drain() {
let mut refund_count = 0;
let max_refunds = T::SignedMaxRefunds::get();

for SignedSubmission { who, deposit, call_fee, .. } in
all_submissions.drain_submitted_order()
{
if refund_count < max_refunds {
// Refund fee
let positive_imbalance = T::Currency::deposit_creating(&who, call_fee);
T::RewardHandler::on_unbalanced(positive_imbalance);
refund_count += 1;
}

// Unreserve deposit
let _remaining = T::Currency::unreserve(&who, deposit);
weight = weight.saturating_add(T::DbWeight::get().writes(1));
debug_assert!(_remaining.is_zero());
weight = weight.saturating_add(T::DbWeight::get().reads_writes(1, 2));
kianenigma marked this conversation as resolved.
Show resolved Hide resolved
}

debug_assert!(!SignedSubmissionIndices::<T>::exists());
Expand All @@ -424,20 +451,22 @@ impl<T: Config> Pallet<T> {
ready_solution: ReadySolution<T::AccountId>,
who: &T::AccountId,
deposit: BalanceOf<T>,
reward: BalanceOf<T>,
call_fee: BalanceOf<T>,
) {
// write this ready solution.
<QueuedSolution<T>>::put(ready_solution);

let reward = T::SignedRewardBase::get();
// emit reward event
Self::deposit_event(crate::Event::Rewarded { account: who.clone(), value: reward });

// unreserve deposit.
// Unreserve deposit.
let _remaining = T::Currency::unreserve(who, deposit);
debug_assert!(_remaining.is_zero());

// Reward.
let positive_imbalance = T::Currency::deposit_creating(who, reward);
// Reward and refund the call fee.
let positive_imbalance =
T::Currency::deposit_creating(who, reward.saturating_add(call_fee));
T::RewardHandler::on_unbalanced(positive_imbalance);
}

Expand Down Expand Up @@ -496,8 +525,8 @@ mod tests {
use super::*;
use crate::{
mock::{
balances, raw_solution, roll_to, ExtBuilder, MultiPhase, Origin, Runtime,
SignedMaxSubmissions, SignedMaxWeight,
balances, raw_solution, roll_to, Balances, ExtBuilder, MultiPhase, Origin, Runtime,
SignedMaxRefunds, SignedMaxSubmissions, SignedMaxWeight,
},
Error, Phase,
};
Expand Down Expand Up @@ -599,8 +628,8 @@ mod tests {

// 99 is rewarded.
assert_eq!(balances(&99), (100 + 7 + 8, 0));
// 999 gets everything back.
assert_eq!(balances(&999), (100, 0));
// 999 gets everything back, including the call fee.
assert_eq!(balances(&999), (100 + 8, 0));
})
}

Expand Down Expand Up @@ -632,19 +661,60 @@ mod tests {
})
}

#[test]
fn call_fee_refund_is_limited_by_signed_max_refunds() {
ExtBuilder::default().build_and_execute(|| {
roll_to(15);
assert!(MultiPhase::current_phase().is_signed());
assert_eq!(SignedMaxRefunds::get(), 1);
assert!(SignedMaxSubmissions::get() > 2);

for s in 0..SignedMaxSubmissions::get() {
let account = 99 + s as u64;
Balances::make_free_balance_be(&account, 100);
// score is always decreasing
let mut solution = raw_solution();
solution.score.minimal_stake -= s as u128;

assert_ok!(MultiPhase::submit(Origin::signed(account), Box::new(solution)));
assert_eq!(balances(&account), (95, 5));
}

assert!(MultiPhase::finalize_signed_phase());

for s in 0..SignedMaxSubmissions::get() {
let account = 99 + s as u64;
// lower accounts have higher scores
if s == 0 {
// winning solution always gets call fee + reward
assert_eq!(balances(&account), (100 + 8 + 7, 0))
} else if s == 1 {
// 1 runner up gets their call fee refunded
assert_eq!(balances(&account), (100 + 8, 0))
} else {
// all other solutions don't get a call fee refund
assert_eq!(balances(&account), (100, 0));
}
}
});
}

#[test]
fn weakest_is_removed_if_better_provided() {
ExtBuilder::default().build_and_execute(|| {
roll_to(15);
assert!(MultiPhase::current_phase().is_signed());

for s in 0..SignedMaxSubmissions::get() {
let account = 99 + s as u64;
Balances::make_free_balance_be(&account, 100);
// score is always getting better
let solution = RawSolution {
score: ElectionScore { minimal_stake: (5 + s).into(), ..Default::default() },
..Default::default()
};
assert_ok!(MultiPhase::submit(Origin::signed(99), Box::new(solution)));
assert_ok!(MultiPhase::submit(Origin::signed(account), Box::new(solution)));
assert_eq!(balances(&account), (95, 5));
}

assert_eq!(
Expand All @@ -660,7 +730,7 @@ mod tests {
score: ElectionScore { minimal_stake: 20, ..Default::default() },
..Default::default()
};
assert_ok!(MultiPhase::submit(Origin::signed(99), Box::new(solution)));
assert_ok!(MultiPhase::submit(Origin::signed(999), Box::new(solution)));

// the one with score 5 was rejected, the new one inserted.
assert_eq!(
Expand All @@ -670,6 +740,9 @@ mod tests {
.collect::<Vec<_>>(),
vec![6, 7, 8, 9, 20]
);

// the submitter of the ejected solution does *not* get a call fee refund
assert_eq!(balances(&(99 + 0)), (100, 0));
})
}

Expand Down Expand Up @@ -825,8 +898,8 @@ mod tests {
assert_eq!(balances(&99), (100 + 7 + 8, 0));
// 999 is slashed.
assert_eq!(balances(&999), (95, 0));
// 9999 gets everything back.
assert_eq!(balances(&9999), (100, 0));
// 9999 gets everything back, including the call fee.
assert_eq!(balances(&9999), (100 + 8, 0));
})
}

Expand Down