Skip to content

Commit

Permalink
Frame: Consideration trait generic over Footprint and indicates z…
Browse files Browse the repository at this point in the history
…ero cost (#4596)

`Consideration` trait generic over `Footprint` and indicates zero cost
for a give footprint.

`Consideration` trait is generic over `Footprint` (currently defined
over the type with the same name). This makes it possible to setup a
custom footprint (e.g. current number of proposals in the storage).

`Consideration::new` and `Consideration::update` return an
`Option<Self>` instead `Self`, this make it possible to indicate a no
cost for a specific footprint (e.g. if current number of proposals in
the storage < max_proposal_count / 2 then no cost).

These cases need to be handled for
#3151
  • Loading branch information
muharem authored Jun 22, 2024
1 parent c4b3c1c commit 812dbff
Show file tree
Hide file tree
Showing 7 changed files with 299 additions and 60 deletions.
18 changes: 18 additions & 0 deletions prdoc/pr_4596.prdoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
title: "Frame: `Consideration` trait generic over `Footprint` and handles zero cost"

doc:
- audience: Runtime Dev
description: |
`Consideration` trait generic over `Footprint` and can handle zero cost for a give footprint.

`Consideration` trait is generic over `Footprint` (currently defined over the type with the same name). This makes it possible to setup a custom footprint (e.g. current number of proposals in the storage).

`Consideration::new` and `Consideration::update` return an `Option<Self>` instead `Self`, this make it possible to define no cost for a specific footprint (e.g. current number of proposals in the storage < max_proposal_count / 2).

crates:
- name: frame-support
bump: major
- name: pallet-preimage
bump: major
- name: pallet-balances
bump: patch
177 changes: 171 additions & 6 deletions substrate/frame/balances/src/tests/fungible_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,20 @@
//! Tests regarding the functionality of the `fungible` trait set implementations.

use super::*;
use frame_support::traits::tokens::{
Fortitude::{Force, Polite},
Precision::{BestEffort, Exact},
Preservation::{Expendable, Preserve, Protect},
Restriction::Free,
use frame_support::traits::{
tokens::{
Fortitude::{Force, Polite},
Precision::{BestEffort, Exact},
Preservation::{Expendable, Preserve, Protect},
Restriction::Free,
},
Consideration, Footprint, LinearStoragePrice,
};
use fungible::{Inspect, InspectFreeze, InspectHold, Mutate, MutateFreeze, MutateHold, Unbalanced};
use fungible::{
FreezeConsideration, HoldConsideration, Inspect, InspectFreeze, InspectHold,
LoneFreezeConsideration, LoneHoldConsideration, Mutate, MutateFreeze, MutateHold, Unbalanced,
};
use sp_core::ConstU64;

#[test]
fn inspect_trait_reducible_balance_basic_works() {
Expand Down Expand Up @@ -493,3 +500,161 @@ fn withdraw_precision_exact_works() {
);
});
}

#[test]
fn freeze_consideration_works() {
ExtBuilder::default()
.existential_deposit(1)
.monied(true)
.build_and_execute_with(|| {
type Consideration = FreezeConsideration<
u64,
Balances,
FooReason,
LinearStoragePrice<ConstU64<0>, ConstU64<1>, u64>,
Footprint,
>;

let who = 4;
// freeze amount taken somewhere outside of our (Consideration) scope.
let extend_freeze = 15;
assert_eq!(Balances::balance_frozen(&TestId::Foo, &who), 0);

let ticket = Consideration::new(&who, Footprint::from_parts(10, 1)).unwrap().unwrap();
assert_eq!(Balances::balance_frozen(&TestId::Foo, &who), 10);

let ticket = ticket.update(&who, Footprint::from_parts(4, 1)).unwrap().unwrap();
assert_eq!(Balances::balance_frozen(&TestId::Foo, &who), 4);

assert_ok!(Balances::increase_frozen(&TestId::Foo, &who, extend_freeze));
assert_eq!(Balances::balance_frozen(&TestId::Foo, &who), 4 + extend_freeze);

let ticket = ticket.update(&who, Footprint::from_parts(8, 1)).unwrap().unwrap();
assert_eq!(Balances::balance_frozen(&TestId::Foo, &who), 8 + extend_freeze);

assert_eq!(ticket.update(&who, Footprint::from_parts(0, 0)).unwrap(), None);
assert_eq!(Balances::balance_frozen(&TestId::Foo, &who), 0 + extend_freeze);

let ticket = Consideration::new(&who, Footprint::from_parts(10, 1)).unwrap().unwrap();
assert_eq!(Balances::balance_frozen(&TestId::Foo, &who), 10 + extend_freeze);

let _ = ticket.drop(&who).unwrap();
assert_eq!(Balances::balance_frozen(&TestId::Foo, &who), 0 + extend_freeze);
});
}

#[test]
fn hold_consideration_works() {
ExtBuilder::default()
.existential_deposit(1)
.monied(true)
.build_and_execute_with(|| {
type Consideration = HoldConsideration<
u64,
Balances,
FooReason,
LinearStoragePrice<ConstU64<0>, ConstU64<1>, u64>,
Footprint,
>;

let who = 4;
// hold amount taken somewhere outside of our (Consideration) scope.
let extend_hold = 15;
assert_eq!(Balances::balance_on_hold(&TestId::Foo, &who), 0);

let ticket = Consideration::new(&who, Footprint::from_parts(10, 1)).unwrap().unwrap();
assert_eq!(Balances::balance_on_hold(&TestId::Foo, &who), 10);

let ticket = ticket.update(&who, Footprint::from_parts(4, 1)).unwrap().unwrap();
assert_eq!(Balances::balance_on_hold(&TestId::Foo, &who), 4);

assert_ok!(Balances::hold(&TestId::Foo, &who, extend_hold));
assert_eq!(Balances::balance_on_hold(&TestId::Foo, &who), 4 + extend_hold);

let ticket = ticket.update(&who, Footprint::from_parts(8, 1)).unwrap().unwrap();
assert_eq!(Balances::balance_on_hold(&TestId::Foo, &who), 8 + extend_hold);

assert_eq!(ticket.update(&who, Footprint::from_parts(0, 0)).unwrap(), None);
assert_eq!(Balances::balance_on_hold(&TestId::Foo, &who), 0 + extend_hold);

let ticket = Consideration::new(&who, Footprint::from_parts(10, 1)).unwrap().unwrap();
assert_eq!(Balances::balance_on_hold(&TestId::Foo, &who), 10 + extend_hold);

let _ = ticket.drop(&who).unwrap();
assert_eq!(Balances::balance_on_hold(&TestId::Foo, &who), 0 + extend_hold);
});
}

#[test]
fn lone_freeze_consideration_works() {
ExtBuilder::default()
.existential_deposit(1)
.monied(true)
.build_and_execute_with(|| {
type Consideration = LoneFreezeConsideration<
u64,
Balances,
FooReason,
LinearStoragePrice<ConstU64<0>, ConstU64<1>, u64>,
Footprint,
>;

let who = 4;
assert_eq!(Balances::balance_frozen(&TestId::Foo, &who), 0);

let ticket = Consideration::new(&who, Footprint::from_parts(10, 1)).unwrap().unwrap();
assert_eq!(Balances::balance_frozen(&TestId::Foo, &who), 10);

assert_ok!(Balances::increase_frozen(&TestId::Foo, &who, 5));
assert_eq!(Balances::balance_frozen(&TestId::Foo, &who), 15);

let ticket = ticket.update(&who, Footprint::from_parts(4, 1)).unwrap().unwrap();
assert_eq!(Balances::balance_frozen(&TestId::Foo, &who), 4);

assert_eq!(ticket.update(&who, Footprint::from_parts(0, 0)).unwrap(), None);
assert_eq!(Balances::balance_frozen(&TestId::Foo, &who), 0);

let ticket = Consideration::new(&who, Footprint::from_parts(10, 1)).unwrap().unwrap();
assert_eq!(Balances::balance_frozen(&TestId::Foo, &who), 10);

let _ = ticket.drop(&who).unwrap();
assert_eq!(Balances::balance_frozen(&TestId::Foo, &who), 0);
});
}

#[test]
fn lone_hold_consideration_works() {
ExtBuilder::default()
.existential_deposit(1)
.monied(true)
.build_and_execute_with(|| {
type Consideration = LoneHoldConsideration<
u64,
Balances,
FooReason,
LinearStoragePrice<ConstU64<0>, ConstU64<1>, u64>,
Footprint,
>;

let who = 4;
assert_eq!(Balances::balance_on_hold(&TestId::Foo, &who), 0);

let ticket = Consideration::new(&who, Footprint::from_parts(10, 1)).unwrap().unwrap();
assert_eq!(Balances::balance_on_hold(&TestId::Foo, &who), 10);

assert_ok!(Balances::hold(&TestId::Foo, &who, 5));
assert_eq!(Balances::balance_on_hold(&TestId::Foo, &who), 15);

let ticket = ticket.update(&who, Footprint::from_parts(4, 1)).unwrap().unwrap();
assert_eq!(Balances::balance_on_hold(&TestId::Foo, &who), 4);

assert_eq!(ticket.update(&who, Footprint::from_parts(0, 0)).unwrap(), None);
assert_eq!(Balances::balance_on_hold(&TestId::Foo, &who), 0);

let ticket = Consideration::new(&who, Footprint::from_parts(10, 1)).unwrap().unwrap();
assert_eq!(Balances::balance_on_hold(&TestId::Foo, &who), 10);

let _ = ticket.drop(&who).unwrap();
assert_eq!(Balances::balance_on_hold(&TestId::Foo, &who), 0);
});
}
4 changes: 4 additions & 0 deletions substrate/frame/balances/src/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,10 @@ impl pallet_transaction_payment::Config for Test {
type FeeMultiplierUpdate = ();
}

parameter_types! {
pub FooReason: TestId = TestId::Foo;
}

#[derive_impl(pallet_balances::config_preludes::TestDefaultConfig)]
impl Config for Test {
type DustRemoval = DustTrap;
Expand Down
2 changes: 1 addition & 1 deletion substrate/frame/preimage/src/benchmarking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ benchmarks! {
T::ManagerOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?,
hash
) verify {
let ticket = TicketOf::<T>::new(&noter, Footprint { count: 1, size: MAX_SIZE as u64 }).unwrap();
let ticket = TicketOf::<T>::new(&noter, Footprint { count: 1, size: MAX_SIZE as u64 }).unwrap().unwrap();
let s = RequestStatus::Requested { maybe_ticket: Some((noter, ticket)), count: 1, maybe_len: Some(MAX_SIZE) };
assert_eq!(RequestStatusFor::<T>::get(&hash), Some(s));
}
Expand Down
19 changes: 11 additions & 8 deletions substrate/frame/preimage/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,9 @@ pub mod pallet {
type ManagerOrigin: EnsureOrigin<Self::RuntimeOrigin>;

/// A means of providing some cost while data is stored on-chain.
type Consideration: Consideration<Self::AccountId>;
///
/// Should never return a `None`, implying no cost for a non-empty preimage.
type Consideration: Consideration<Self::AccountId, Footprint>;
}

#[pallet::pallet]
Expand Down Expand Up @@ -158,6 +160,8 @@ pub mod pallet {
TooMany,
/// Too few hashes were requested to be upgraded (i.e. zero).
TooFew,
/// No ticket with a cost was returned by [`Config::Consideration`] to store the preimage.
NoCost,
}

/// A reason for this pallet placing a hold on funds.
Expand Down Expand Up @@ -268,10 +272,10 @@ impl<T: Config> Pallet<T> {
// unreserve deposit
T::Currency::unreserve(&who, amount);
// take consideration
let Ok(ticket) =
let Ok(Some(ticket)) =
T::Consideration::new(&who, Footprint::from_parts(1, len as usize))
.defensive_proof("Unexpected inability to take deposit after unreserved")
else {
defensive!("None ticket or inability to take deposit after unreserved");
return true
};
RequestStatus::Unrequested { ticket: (who, ticket), len }
Expand All @@ -282,12 +286,10 @@ impl<T: Config> Pallet<T> {
T::Currency::unreserve(&who, deposit);
// take consideration
if let Some(len) = maybe_len {
let Ok(ticket) =
let Ok(Some(ticket)) =
T::Consideration::new(&who, Footprint::from_parts(1, len as usize))
.defensive_proof(
"Unexpected inability to take deposit after unreserved",
)
else {
defensive!("None ticket or inability to take deposit after unreserved");
return true
};
Some((who, ticket))
Expand Down Expand Up @@ -347,7 +349,8 @@ impl<T: Config> Pallet<T> {
RequestStatus::Requested { maybe_ticket: None, count: 1, maybe_len: Some(len) },
(None, Some(depositor)) => {
let ticket =
T::Consideration::new(depositor, Footprint::from_parts(1, len as usize))?;
T::Consideration::new(depositor, Footprint::from_parts(1, len as usize))?
.ok_or(Error::<T>::NoCost)?;
RequestStatus::Unrequested { ticket: (depositor.clone(), ticket), len }
},
};
Expand Down
26 changes: 15 additions & 11 deletions substrate/frame/support/src/traits/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ where
}

/// Some sort of cost taken from account temporarily in order to offset the cost to the chain of
/// holding some data [`Footprint`] in state.
/// holding some data `Footprint` (e.g. [`Footprint`]) in state.
///
/// The cost may be increased, reduced or dropped entirely as the footprint changes.
///
Expand All @@ -206,16 +206,20 @@ where
/// treated as one*. Don't type to duplicate it, and remember to drop it when you're done with
/// it.
#[must_use]
pub trait Consideration<AccountId>: Member + FullCodec + TypeInfo + MaxEncodedLen {
pub trait Consideration<AccountId, Footprint>:
Member + FullCodec + TypeInfo + MaxEncodedLen
{
/// Create a ticket for the `new` footprint attributable to `who`. This ticket *must* ultimately
/// be consumed through `update` or `drop` once the footprint changes or is removed.
fn new(who: &AccountId, new: Footprint) -> Result<Self, DispatchError>;
/// be consumed through `update` or `drop` once the footprint changes or is removed. `None`
/// implies no cost for a given footprint.
fn new(who: &AccountId, new: Footprint) -> Result<Option<Self>, DispatchError>;

/// Optionally consume an old ticket and alter the footprint, enforcing the new cost to `who`
/// and returning the new ticket (or an error if there was an issue).
/// and returning the new ticket (or an error if there was an issue). `None` implies no cost for
/// a given footprint.
///
/// For creating tickets and dropping them, you can use the simpler `new` and `drop` instead.
fn update(self, who: &AccountId, new: Footprint) -> Result<Self, DispatchError>;
fn update(self, who: &AccountId, new: Footprint) -> Result<Option<Self>, DispatchError>;

/// Consume a ticket for some `old` footprint attributable to `who` which should now been freed.
fn drop(self, who: &AccountId) -> Result<(), DispatchError>;
Expand All @@ -230,12 +234,12 @@ pub trait Consideration<AccountId>: Member + FullCodec + TypeInfo + MaxEncodedLe
}
}

impl<A> Consideration<A> for () {
fn new(_: &A, _: Footprint) -> Result<Self, DispatchError> {
Ok(())
impl<A, F> Consideration<A, F> for () {
fn new(_: &A, _: F) -> Result<Option<Self>, DispatchError> {
Ok(Some(()))
}
fn update(self, _: &A, _: Footprint) -> Result<(), DispatchError> {
Ok(())
fn update(self, _: &A, _: F) -> Result<Option<Self>, DispatchError> {
Ok(Some(()))
}
fn drop(self, _: &A) -> Result<(), DispatchError> {
Ok(())
Expand Down
Loading

0 comments on commit 812dbff

Please sign in to comment.