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 10 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
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 @@ -226,14 +226,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 @@ -335,7 +345,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::{CheckedAdd, Saturating, Zero},
traits::{CheckedAdd, Zero},
UpperOf,
};
use sp_npos_elections::{
Expand Down Expand Up @@ -628,6 +628,11 @@ pub mod pallet {
#[pallet::constant]
type SignedMaxWeight: Get<Weight>;

/// The maximum amount of unchecked solutions to refund the call fee for. If `None`, all
/// solutions, including ejected solutions, will get a refund.
emostov marked this conversation as resolved.
Show resolved Hide resolved
#[pallet::constant]
type SignedMaxRefunds: Get<Option<u32>>;
kianenigma marked this conversation as resolved.
Show resolved Hide resolved
emostov marked this conversation as resolved.
Show resolved Hide resolved

/// Base reward for a signed solution
#[pallet::constant]
type SignedRewardBase: Get<BalanceOf<Self>>;
Expand Down Expand Up @@ -987,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 All @@ -1013,6 +1021,10 @@ pub mod pallet {
let ejected_a_solution = maybe_removed.is_some();
// if we had to remove the weakest solution, unreserve its deposit
if let Some(removed) = maybe_removed {
if T::SignedMaxRefunds::get().is_none() {
// There are no limits on how many solutions get their call fee refunded
let _ = T::Currency::deposit_creating(&removed.who, removed.call_fee);
}
emostov marked this conversation as resolved.
Show resolved Hide resolved
let _remainder = T::Currency::unreserve(&removed.who, removed.deposit);
debug_assert!(_remainder.is_zero());
}
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 @@ -252,6 +252,7 @@ parameter_types! {
pub static SignedPhase: BlockNumber = 10;
pub static UnsignedPhase: BlockNumber = 5;
pub static SignedMaxSubmissions: u32 = 5;
pub static SignedMaxRefunds: Option<u32> = Some(1);
pub static SignedDepositBase: Balance = 5;
pub static SignedDepositByte: Balance = 0;
pub static SignedDepositWeight: Balance = 0;
Expand Down Expand Up @@ -404,6 +405,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
114 changes: 96 additions & 18 deletions frame/election-provider-multi-phase/src/signed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,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 @@ -361,7 +361,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 @@ -376,7 +376,7 @@ impl<T: Config> Pallet<T> {
ready_solution,
&who,
deposit,
reward,
call_fee,
);
found_solution = true;

Expand All @@ -395,10 +395,19 @@ 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 maybe_max_refunds = T::SignedMaxRefunds::get();
for SignedSubmission { who, deposit, call_fee, .. } in all_submissions.drain() {
if maybe_max_refunds.map_or(true, |max| refund_count < max) {
// Refund fee
let _ = T::Currency::deposit_creating(&who, call_fee);
emostov marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -423,20 +432,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
emostov marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -495,8 +506,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 @@ -598,8 +609,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 @@ -631,19 +642,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(), Some(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 better
emostov marked this conversation as resolved.
Show resolved Hide resolved
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 there call fee refunded
emostov marked this conversation as resolved.
Show resolved Hide resolved
assert_eq!(balances(&account), (100 + 8, 0))
} else {
// all other solutions don't get a call fee refunds
emostov marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -659,7 +711,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 @@ -669,6 +721,32 @@ 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));

// set the max refunds to None
SignedMaxRefunds::set(None);
assert!(<Runtime as Config>::SignedMaxRefunds::get().is_none());

// submit another solution to force an ejection
let solution = RawSolution {
score: ElectionScore { minimal_stake: 21, ..Default::default() },
..Default::default()
};
assert_ok!(MultiPhase::submit(Origin::signed(999), Box::new(solution)));

// the one with score 6 was rejected, the new one inserted.
assert_eq!(
MultiPhase::signed_submissions()
.iter()
.map(|s| s.raw_solution.score.minimal_stake)
.collect::<Vec<_>>(),
vec![7, 8, 9, 20, 21]
);

// the submitter of the ejected solution gets a call fee refund
assert_eq!(balances(&(99 + 1)), (100 + 8, 0));
})
}

Expand Down Expand Up @@ -824,8 +902,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