Skip to content

Commit

Permalink
Merge pull request #2416 from subspace/operator-storage-fee
Browse files Browse the repository at this point in the history
Introduce the bundle storage fund
  • Loading branch information
NingLin-P authored Feb 5, 2024
2 parents 7333974 + 6a35f51 commit 1f117fa
Show file tree
Hide file tree
Showing 16 changed files with 1,097 additions and 203 deletions.
34 changes: 23 additions & 11 deletions crates/pallet-domains/src/block_tree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use sp_domains::merkle_tree::MerkleTree;
use sp_domains::{DomainId, ExecutionReceipt, OperatorId};
use sp_runtime::traits::{BlockNumberProvider, CheckedSub, One, Saturating, Zero};
use sp_std::cmp::Ordering;
use sp_std::collections::btree_map::BTreeMap;
use sp_std::vec::Vec;

/// Block tree specific errors
Expand Down Expand Up @@ -271,6 +272,8 @@ pub(crate) struct ConfirmedDomainBlockInfo<DomainNumber, Balance> {
pub operator_ids: Vec<OperatorId>,
pub rewards: Balance,
pub invalid_bundle_authors: Vec<OperatorId>,
pub total_storage_fee: Balance,
pub paid_bundle_storage_fees: BTreeMap<OperatorId, u32>,
}

pub(crate) type ProcessExecutionReceiptResult<T> =
Expand Down Expand Up @@ -316,7 +319,8 @@ pub(crate) fn process_execution_receipt<T: Config>(
execution_receipt.domain_block_hash,
));

// Collect the invalid bundle author
// Collect the paid bundle storage fees and the invalid bundle author
let mut paid_bundle_storage_fees = BTreeMap::new();
let mut invalid_bundle_authors = Vec::new();
let bundle_digests = ExecutionInbox::<T>::get((
domain_id,
Expand All @@ -329,6 +333,11 @@ pub(crate) fn process_execution_receipt<T: Config>(
// the `ER::bundles` have the same length of `ExecutionInbox`
if execution_receipt.inboxed_bundles[index].is_invalid() {
invalid_bundle_authors.push(bundle_author);
} else {
paid_bundle_storage_fees
.entry(bundle_author)
.and_modify(|s| *s += bd.size)
.or_insert(bd.size);
}
}
}
Expand All @@ -345,9 +354,10 @@ pub(crate) fn process_execution_receipt<T: Config>(
return Ok(Some(ConfirmedDomainBlockInfo {
domain_block_number: to_prune,
operator_ids,
// TODO: also distribute the `storage_fee`
rewards: execution_receipt.block_fees.domain_execution_fee,
invalid_bundle_authors,
total_storage_fee: execution_receipt.block_fees.consensus_storage_fee,
paid_bundle_storage_fees,
}));
}
}
Expand Down Expand Up @@ -432,7 +442,7 @@ mod tests {
fn test_genesis_receipt() {
let mut ext = new_test_ext_with_extensions();
ext.execute_with(|| {
let domain_id = register_genesis_domain(0u64, vec![0u64]);
let domain_id = register_genesis_domain(0u128, vec![0u64]);

// The genesis receipt should be added to the block tree
let block_tree_node_at_0 = BlockTree::<Test>::get(domain_id, 0).unwrap();
Expand Down Expand Up @@ -463,7 +473,7 @@ mod tests {

#[test]
fn test_new_head_receipt() {
let creator = 0u64;
let creator = 0u128;
let operator_id = 1u64;
let block_tree_pruning_depth = <Test as Config>::BlockTreePruningDepth::get();

Expand Down Expand Up @@ -517,6 +527,7 @@ mod tests {
vec![BundleDigest {
header_hash: bundle_header_hash,
extrinsics_root: bundle_extrinsics_root,
size: 0,
}]
);
assert!(InboxedBundleAuthor::<Test>::contains_key(
Expand Down Expand Up @@ -577,7 +588,7 @@ mod tests {

#[test]
fn test_confirm_current_head_receipt() {
let creator = 0u64;
let creator = 0u128;
let operator_id1 = 1u64;
let operator_id2 = 2u64;
let mut ext = new_test_ext_with_extensions();
Expand Down Expand Up @@ -643,7 +654,7 @@ mod tests {

#[test]
fn test_non_head_receipt() {
let creator = 0u64;
let creator = 0u128;
let operator_id1 = 1u64;
let operator_id2 = 2u64;
let mut ext = new_test_ext_with_extensions();
Expand Down Expand Up @@ -688,7 +699,7 @@ mod tests {

#[test]
fn test_previous_head_receipt() {
let creator = 0u64;
let creator = 0u128;
let operator_id1 = 1u64;
let operator_id2 = 2u64;
let mut ext = new_test_ext_with_extensions();
Expand Down Expand Up @@ -729,7 +740,7 @@ mod tests {

#[test]
fn test_new_branch_receipt() {
let creator = 0u64;
let creator = 0u128;
let operator_id1 = 1u64;
let operator_id2 = 2u64;
let mut ext = new_test_ext_with_extensions();
Expand Down Expand Up @@ -776,7 +787,7 @@ mod tests {

#[test]
fn test_invalid_receipt() {
let creator = 0u64;
let creator = 0u128;
let operator_id = 1u64;
let mut ext = new_test_ext_with_extensions();
ext.execute_with(|| {
Expand All @@ -801,6 +812,7 @@ mod tests {
.map(|b| BundleDigest {
header_hash: H256::random(),
extrinsics_root: b.extrinsics_root,
size: 0,
})
.collect::<Vec<_>>(),
);
Expand Down Expand Up @@ -863,7 +875,7 @@ mod tests {

#[test]
fn test_invalid_trace_root_receipt() {
let creator = 0u64;
let creator = 0u128;
let operator_id1 = 1u64;
let operator_id2 = 2u64;
let mut ext = new_test_ext_with_extensions();
Expand Down Expand Up @@ -925,7 +937,7 @@ mod tests {

#[test]
fn test_collect_invalid_bundle_author() {
let creator = 0u64;
let creator = 0u128;
let challenge_period = BlockTreePruningDepth::get() as u64;
let operator_set: Vec<_> = (1..15).collect();
let mut ext = new_test_ext_with_extensions();
Expand Down
225 changes: 225 additions & 0 deletions crates/pallet-domains/src/bundle_storage_fund.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
//! Bundle storage fund

use crate::staking::NewDeposit;
use crate::{BalanceOf, Config, Event, HoldIdentifier, Operators, Pallet};
use codec::{Decode, Encode};
use frame_support::traits::fungible::{Inspect, Mutate, MutateHold};
use frame_support::traits::tokens::{Fortitude, Precision, Preservation};
use frame_support::traits::Get;
use frame_support::PalletError;
use scale_info::TypeInfo;
use sp_domains::OperatorId;
use sp_runtime::traits::{AccountIdConversion, CheckedSub, Zero};
use sp_runtime::Perbill;
use sp_std::collections::btree_map::BTreeMap;
use subspace_runtime_primitives::StorageFee;

/// The proportion of staking fund reserved for the bundle storage fee
pub const STORAGE_FEE_RESERVE: Perbill = Perbill::from_percent(20);

/// Bundle storage fund specific errors
#[derive(TypeInfo, Encode, Decode, PalletError, Debug, PartialEq)]
pub enum Error {
BundleStorageFeePayment,
BalanceUnderflow,
MintBalance,
FailToDeposit,
WithdrawAndHold,
BalanceTransfer,
}

/// The type of system account being created.
#[derive(Encode, Decode)]
pub enum AccountType {
StorageFund,
}

#[derive(TypeInfo, Debug, Encode, Decode, Clone, PartialEq, Eq, Default)]
pub struct StorageFundRedeemPrice<T: Config>((BalanceOf<T>, BalanceOf<T>));

impl<T: Config> StorageFundRedeemPrice<T> {
pub(crate) fn new(total_balance: BalanceOf<T>, total_deposit: BalanceOf<T>) -> Self {
StorageFundRedeemPrice((total_balance, total_deposit))
}

/// Return the amount of balance can be redeemed by the given `deposit`, it is calculated
/// by `storage_fund_total_balance * deposit / total_deposit`.
///
/// If the inflow of the storage fund (i.e. refund of the storage fee) is larger than its
/// outflow (i.e. payment of the storage fee), the return value will larger than `deposit`
/// otherwise smaller.
pub(crate) fn redeem(&self, deposit: BalanceOf<T>) -> BalanceOf<T> {
let (total_balance, total_deposit) = self.0;
if total_balance == total_deposit {
deposit
} else {
Perbill::from_rational(deposit, total_deposit).mul_floor(total_balance)
}
}
}

/// Return the bundle storage fund account of the given operator.
pub fn storage_fund_account<T: Config>(id: OperatorId) -> T::AccountId {
T::PalletId::get().into_sub_account_truncating((AccountType::StorageFund, id))
}

/// Charge the bundle storage fee from the operator's bundle storage fund
pub fn charge_bundle_storage_fee<T: Config>(
operator_id: OperatorId,
bundle_size: u32,
) -> Result<(), Error> {
if bundle_size.is_zero() {
return Ok(());
}

let storage_fund_acc = storage_fund_account::<T>(operator_id);
let storage_fee = T::StorageFee::transaction_byte_fee() * bundle_size.into();

T::Currency::burn_from(
&storage_fund_acc,
storage_fee,
Precision::Exact,
Fortitude::Polite,
)
.map_err(|_| Error::BundleStorageFeePayment)?;

// Note the storage fee, it will go to the consensus block author
T::StorageFee::note_storage_fees(storage_fee);

Ok(())
}

/// Refund the paid bundle storage fee of a particular domain block back to the operator, the amount to
/// refund to a particular operator is determined by the total storage fee collected from the domain user
/// and the percentage of bundle storage that the operator have submitted for the domain block.
#[allow(dead_code)]
pub fn refund_storage_fee<T: Config>(
total_storage_fee: BalanceOf<T>,
paid_bundle_storage_fees: BTreeMap<OperatorId, u32>,
) -> Result<(), Error> {
if total_storage_fee.is_zero() {
return Ok(());
}

let total_paid_storage = paid_bundle_storage_fees.values().sum::<u32>();
let mut remaining_fee = total_storage_fee;
for (operator_id, paid_storage) in paid_bundle_storage_fees {
// If the operator is deregistered and unlocked or slashed and finalized, the refund bundle storage
// fee will go to the treasury
if Operators::<T>::get(operator_id).is_none() || paid_storage.is_zero() {
continue;
}

let refund_amount = {
let paid_storage_percentage = Perbill::from_rational(paid_storage, total_paid_storage);
paid_storage_percentage.mul_floor(total_storage_fee)
};
let storage_fund_acc = storage_fund_account::<T>(operator_id);
T::Currency::mint_into(&storage_fund_acc, refund_amount).map_err(|_| Error::MintBalance)?;

remaining_fee = remaining_fee
.checked_sub(&refund_amount)
.ok_or(Error::BalanceUnderflow)?;
}

// Drop any dust and deregistered/slashed operator's bundle storage fee to the treasury
if !remaining_fee.is_zero() {
T::Currency::mint_into(&T::TreasuryAccount::get(), remaining_fee)
.map_err(|_| Error::MintBalance)?;
}

Ok(())
}

/// Split the new deposit into 2 parts: the staking deposit and the the storage fee deposit,
/// add the storage fee deposit to the bundle storage fund.
pub fn deposit_reserve_for_storage_fund<T: Config>(
operator_id: OperatorId,
source: &T::AccountId,
deposit_amount: BalanceOf<T>,
) -> Result<NewDeposit<BalanceOf<T>>, Error> {
let storage_fund_acc = storage_fund_account::<T>(operator_id);

let storage_fee_reserve = STORAGE_FEE_RESERVE.mul_floor(deposit_amount);

T::Currency::transfer(
source,
&storage_fund_acc,
storage_fee_reserve,
Preservation::Preserve,
)
.map_err(|_| Error::FailToDeposit)?;

Pallet::<T>::deposit_event(Event::StorageFeeDeposited {
operator_id,
nominator_id: source.clone(),
amount: storage_fee_reserve,
});

let staking = deposit_amount
.checked_sub(&storage_fee_reserve)
.ok_or(Error::BalanceUnderflow)?;

Ok(NewDeposit {
staking,
storage_fee_deposit: storage_fee_reserve,
})
}

/// Transfer the given `withdraw_amount` of balance from the bundle storage fund to the
/// given `dest_account` and hold on the `dest_account`
pub fn withdraw_and_hold<T: Config>(
operator_id: OperatorId,
dest_account: &T::AccountId,
withdraw_amount: BalanceOf<T>,
) -> Result<BalanceOf<T>, Error> {
if withdraw_amount.is_zero() {
return Ok(Zero::zero());
}

let storage_fund_acc = storage_fund_account::<T>(operator_id);
let storage_fund_hold_id = T::HoldIdentifier::storage_fund_withdrawal(operator_id);
T::Currency::transfer_and_hold(
&storage_fund_hold_id,
&storage_fund_acc,
dest_account,
withdraw_amount,
Precision::Exact,
Preservation::Expendable,
Fortitude::Force,
)
.map_err(|_| Error::WithdrawAndHold)
}

/// Return the total balance of the bundle storage fund the given `operator_id`
pub fn total_balance<T: Config>(operator_id: OperatorId) -> BalanceOf<T> {
let storage_fund_acc = storage_fund_account::<T>(operator_id);
T::Currency::reducible_balance(
&storage_fund_acc,
Preservation::Expendable,
Fortitude::Polite,
)
}

/// Return the bundle storage fund redeem price
pub fn storage_fund_redeem_price<T: Config>(
operator_id: OperatorId,
operator_total_deposit: BalanceOf<T>,
) -> StorageFundRedeemPrice<T> {
let total_balance = total_balance::<T>(operator_id);
StorageFundRedeemPrice::<T>::new(total_balance, operator_total_deposit)
}

/// Transfer all of the balance of the bundle storage fund to the treasury
pub fn transfer_all_to_treasury<T: Config>(operator_id: OperatorId) -> Result<(), Error> {
let storage_fund_acc = storage_fund_account::<T>(operator_id);
let total_balance = total_balance::<T>(operator_id);
T::Currency::transfer(
&storage_fund_acc,
&T::TreasuryAccount::get(),
total_balance,
Preservation::Expendable,
)
.map_err(|_| Error::BalanceTransfer)?;
Ok(())
}
2 changes: 1 addition & 1 deletion crates/pallet-domains/src/domain_registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ mod tests {

#[test]
fn test_domain_instantiation() {
let creator = 1u64;
let creator = 1u128;
let created_at = 0u64;
// Construct an invalid domain config initially
let mut domain_config = DomainConfig {
Expand Down
Loading

0 comments on commit 1f117fa

Please sign in to comment.