Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pallet services-payment #77

Merged
merged 22 commits into from
May 25, 2023
Merged
Show file tree
Hide file tree
Changes from 13 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
37 changes: 37 additions & 0 deletions pallets/services-payment/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
[package]
name = "pallet-services-payment"
authors = []
description = "Services payment pallet"
edition = "2021"
publish = false
version = "0.1.0"

[package.metadata.docs.rs]
targets = [ "x86_64-unknown-linux-gnu" ]

[dependencies]
frame-support = { workspace = true }
frame-system = { workspace = true }
log = { workspace = true }
parity-scale-codec = { workspace = true, features = [ "derive", "max-encoded-len" ] }
scale-info = { workspace = true }
serde = { workspace = true, optional = true, features = [ "derive" ] }

cumulus-primitives-core = { workspace = true }

[dev-dependencies]
sp-runtime = { workspace = true }
sp-core = { workspace = true }
sp-io = { workspace = true }
pallet-balances = { workspace = true }

[features]
default = [ "std" ]
std = [
"cumulus-primitives-core/std",
"frame-support/std",
"frame-system/std",
"pallet-balances/std",
"scale-info/std",
]
try-runtime = [ "frame-support/try-runtime" ]
181 changes: 181 additions & 0 deletions pallets/services-payment/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
//! # Services Payment pallet
//!
//! This pallet allows for block creation services to be paid for by a
//! containerChain.

#![cfg_attr(not(feature = "std"), no_std)]

use {
cumulus_primitives_core::ParaId,
frame_support::{pallet_prelude::*, sp_runtime::{Saturating, traits::Zero}, traits::Currency},
frame_system::pallet_prelude::*,
};

#[cfg(test)]
mod mock;

#[cfg(test)]
mod test;

pub use pallet::*;

#[frame_support::pallet]
pub mod pallet {
use super::*;

#[pallet::config]
pub trait Config: frame_system::Config {
/// The overarching event type.
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
/// Handler for fees
type OnChargeForBlockCredit: OnChargeForBlockCredit<Self>;
/// Currency type for fee payment
type Currency: Currency<Self::AccountId>;
/// Provider of a block cost which can adjust from block to block
type ProvideBlockProductionCost: ProvideBlockProductionCost<Self>;
/// The maximum number of credits that can be accumulated
type MaxCreditsStored: Get<Self::BlockNumber>;
}

#[pallet::error]
pub enum Error<T> {
TooManyCredits,
InsufficientFundsToPurchaseCredits,
InsufficientCredits,
}

#[pallet::pallet]
pub struct Pallet<T>(PhantomData<T>);

#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
CreditsPurchased {
para_id: ParaId,
payer: T::AccountId,
fee: BalanceOf<T>,
credits_purchased: T::BlockNumber,
credits_remaining: T::BlockNumber,
},
CreditBurned {
para_id: ParaId,
credits_remaining: T::BlockNumber,
},
}

#[pallet::storage]
#[pallet::getter(fn collator_commission)]
pub type BlockProductionCredits<T: Config> = StorageMap<
_,
Blake2_128Concat,
ParaId,
T::BlockNumber,
OptionQuery,
>;

#[pallet::call]
impl<T: Config> Pallet<T>
where
BalanceOf<T>: From<BlockNumberFor<T>>,
{
#[pallet::call_index(0)]
#[pallet::weight(0)] // TODO
pub fn purchase_credits(
Copy link
Contributor Author

Choose a reason for hiding this comment

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

It might be desirable to be able to provide a maximum fee the caller is willing to pay per credit here...

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah I think it might be useful. Can you add that?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added in 3a91bcf as an Optional limit. LMK what you think.

origin: OriginFor<T>,
para_id: ParaId,
credits: T::BlockNumber
) -> DispatchResultWithPostInfo {
let account = ensure_signed(origin)?;

let existing_credits = BlockProductionCredits::<T>::get(para_id).unwrap_or(T::BlockNumber::zero());
let updated_credits = existing_credits.saturating_add(credits);
ensure!(
Copy link
Contributor

Choose a reason for hiding this comment

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

We can maybe buy as many credit as we can to fill the credit buffer here instead of erroring.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added in 188e2d2.

One detail to note: if there are 0 purchases possible (even if 0 were requested) the extrinsic succeeds and a CreditsPurchased event will be emitted with credits_purchased == 0.

This case is a bit ambiguous to me, but this seemed like the most graceful thing to do. We could bloat the event with one more field for clarity: credits_requested.

updated_credits <= T::MaxCreditsStored::get(),
Error::<T>::TooManyCredits,
);

// get the current per-credit cost of a block
let (block_cost, _weight) = T::ProvideBlockProductionCost::block_cost(&para_id);
let total_fee = block_cost.saturating_mul(credits.into());

T::OnChargeForBlockCredit::charge_credits(&account, &para_id, credits, total_fee)?;

BlockProductionCredits::<T>::insert(para_id, updated_credits);

Self::deposit_event(Event::<T>::CreditsPurchased {
para_id,
payer: account,
fee: total_fee,
credits_purchased: credits,
credits_remaining: updated_credits,
});

Ok(().into())
}
}

impl<T: Config> Pallet<T> {
// TODO: make this a regular call? weight?
/// Burn a credit for the given para. Deducts one credit if possible, errors otherwise.
pub fn burn_credit_for_para(para_id: &ParaId) -> DispatchResultWithPostInfo {
let existing_credits = BlockProductionCredits::<T>::get(para_id).unwrap_or(T::BlockNumber::zero());

ensure!(
existing_credits >= 1u32.into(),
Error::<T>::InsufficientCredits,
);

let updated_credits = existing_credits.saturating_sub(1u32.into());
BlockProductionCredits::<T>::insert(para_id, updated_credits);

Self::deposit_event(Event::<T>::CreditBurned {
para_id: *para_id,
credits_remaining: updated_credits,
});

Ok(().into())
}
}

#[pallet::genesis_config]
pub struct GenesisConfig<T: Config> {
pub initial_credits: Vec<(ParaId, T::BlockNumber)>,
}

#[cfg(feature = "std")]
impl<T: Config> Default for GenesisConfig<T> {
fn default() -> Self {
Self {
initial_credits: Default::default(),
}
}
}

#[pallet::genesis_build]
impl<T: Config> GenesisBuild<T> for GenesisConfig<T> {
fn build(&self) {
todo!();
Copy link
Contributor

Choose a reason for hiding this comment

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

Won't this panic on genesis? If we don't want to implement it just make it a noop.

Suggested change
todo!();
// TODO: implement

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks, fixed in a4b0d36

}
}
}

/// Balance used by this pallet
pub type BalanceOf<T> =
<<T as Config>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance;

/// Handler for fee charging. This will be invoked when fees need to be deducted from the fee
/// account for a given paraId.
pub trait OnChargeForBlockCredit<T: Config> {
fn charge_credits(
payer: &T::AccountId,
para_id: &ParaId,
credits: T::BlockNumber,
fee: BalanceOf<T>,
) -> Result<(), Error<T>>;
}

/// Returns the cost for a given block credit at the current time. This can be a complex operation,
/// so it also returns the weight it consumes. (TODO: or just rely on benchmarking)
pub trait ProvideBlockProductionCost<T: Config> {
fn block_cost(para_id: &ParaId) -> (BalanceOf<T>, Weight);
}
164 changes: 164 additions & 0 deletions pallets/services-payment/src/mock.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
use frame_support::traits::{Currency, WithdrawReasons};

use {
crate::{self as payment_services_pallet, OnChargeForBlockCredit, ProvideBlockProductionCost},
cumulus_primitives_core::ParaId,
frame_support::{
parameter_types,
pallet_prelude::*,
traits::{
ConstU32, ConstU64, Everything,
tokens::ExistenceRequirement,
},
},
sp_core::H256,
sp_runtime::{
testing::Header,
traits::{BlakeTwo256, IdentityLookup},
},
};

type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic<Test>;
type Block = frame_system::mocking::MockBlock<Test>;
type AccountId = u64;
type Balance = u128;

frame_support::construct_runtime!(
pub enum Test where
Block = Block,
NodeBlock = Block,
UncheckedExtrinsic = UncheckedExtrinsic,
{
System: frame_system::{Pallet, Call, Config, Storage, Event<T>},
Balances: pallet_balances::{Pallet, Call, Storage, Config<T>, Event<T>},
PaymentServices: payment_services_pallet::{Pallet, Call, Config<T>, Storage, Event<T>}
}
);

impl frame_system::Config for Test {
type BaseCallFilter = Everything;
type BlockWeights = ();
type BlockLength = ();
type DbWeight = ();
type RuntimeOrigin = RuntimeOrigin;
type RuntimeCall = RuntimeCall;
type Index = u64;
type BlockNumber = u64;
type Hash = H256;
type Hashing = BlakeTwo256;
type AccountId = AccountId;
type Lookup = IdentityLookup<Self::AccountId>;
type Header = Header;
type RuntimeEvent = RuntimeEvent;
type BlockHashCount = ConstU64<250>;
type Version = ();
type PalletInfo = PalletInfo;
type AccountData = pallet_balances::AccountData<Balance>;
type OnNewAccount = ();
type OnKilledAccount = ();
type SystemWeightInfo = ();
type SS58Prefix = ();
type OnSetCode = ();
type MaxConsumers = ConstU32<16>;
}

parameter_types! {
pub const ExistentialDeposit: u128 = 1;
}

impl pallet_balances::Config for Test {
type MaxReserves = ();
type ReserveIdentifier = [u8; 4];
type MaxLocks = ();
type Balance = Balance;
type RuntimeEvent = RuntimeEvent;
type DustRemoval = ();
type ExistentialDeposit = ExistentialDeposit;
type AccountStore = System;
type WeightInfo = ();
}

parameter_types! {
pub const MaxCreditsStored: u64 = 5;
}

impl payment_services_pallet::Config for Test {
type RuntimeEvent = RuntimeEvent;
type OnChargeForBlockCredit = ChargeForBlockCredit<Test>;
type Currency = Balances;
type ProvideBlockProductionCost = BlockProductionCost<Test>;
type MaxCreditsStored = MaxCreditsStored;
}

pub struct ChargeForBlockCredit<Test>(PhantomData<Test>);
impl OnChargeForBlockCredit<Test> for ChargeForBlockCredit<Test> {
fn charge_credits(
payer: &u64,
_para_id: &ParaId,
_credits: u64,
fee: u128,
) -> Result<(), payment_services_pallet::Error<Test>> {
use frame_support::traits::tokens::imbalance::Imbalance;

let result = Balances::withdraw(
&*payer,
fee,
WithdrawReasons::FEE,
ExistenceRequirement::AllowDeath
);
let imbalance = result.map_err(|_| payment_services_pallet::Error::InsufficientFundsToPurchaseCredits)?;

if imbalance.peek() != fee {
panic!("withdrawn balance incorrect");
}

Ok(())
}
}

pub struct BlockProductionCost<Test>(PhantomData<Test>);
impl ProvideBlockProductionCost<Test> for BlockProductionCost<Test> {
fn block_cost(_para_id: &ParaId) -> (u128, Weight) {
(100, Weight::zero())
}
}

#[derive(Default)]
pub struct ExtBuilder {
balances: Vec<(AccountId, Balance)>,
}

impl ExtBuilder {
pub fn with_balances(mut self, balances: Vec<(AccountId, Balance)>) -> Self {
self.balances = balances;
self
}

pub fn build(self) -> sp_io::TestExternalities {
let mut t = frame_system::GenesisConfig::default()
.build_storage::<Test>()
.unwrap();

pallet_balances::GenesisConfig::<Test> {
balances: self.balances,
}
.assimilate_storage(&mut t)
.unwrap();

t.into()
}
}

pub(crate) fn events() -> Vec<payment_services_pallet::Event<Test>> {
System::events()
.into_iter()
.map(|r| r.event)
.filter_map(|e| {
if let RuntimeEvent::PaymentServices(inner) = e {
Some(inner)
} else {
None
}
})
.collect::<Vec<_>>()
}
Loading