diff --git a/Cargo.lock b/Cargo.lock index 8155e842ac..51802ecc09 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4376,7 +4376,6 @@ dependencies = [ "frame-system", "parity-scale-codec", "scale-info", - "sp-consensus-subspace", ] [[package]] @@ -4456,6 +4455,16 @@ dependencies = [ "sp-timestamp", ] +[[package]] +name = "pallet-transaction-fees" +version = "0.1.0" +dependencies = [ + "frame-support", + "frame-system", + "parity-scale-codec", + "scale-info", +] + [[package]] name = "pallet-transaction-payment" version = "4.0.0-dev" @@ -7990,6 +7999,7 @@ dependencies = [ "pallet-subspace", "pallet-sudo", "pallet-timestamp", + "pallet-transaction-fees", "pallet-transaction-payment", "pallet-transaction-payment-rpc-runtime-api", "pallet-utility", diff --git a/crates/pallet-rewards/Cargo.toml b/crates/pallet-rewards/Cargo.toml index 5327a2bca3..0efc20ccd8 100644 --- a/crates/pallet-rewards/Cargo.toml +++ b/crates/pallet-rewards/Cargo.toml @@ -6,7 +6,7 @@ edition = "2021" license = "Apache-2.0" homepage = "https://subspace.network" repository = "https://github.com/subspace/subspace" -description = "Subspace pallet for issuing rewards to block producers" +description = "Pallet for issuing rewards to block producers" readme = "README.md" include = [ "/src", @@ -22,7 +22,6 @@ codec = { package = "parity-scale-codec", version = "2.3.0", default-features = frame-support = { version = "4.0.0-dev", default-features = false, git = "https://github.com/paritytech/substrate", rev = "b6c1c1bcfa5d831bfd1f278064d7af757f9b38f5" } frame-system = { version = "4.0.0-dev", default-features = false, git = "https://github.com/paritytech/substrate", rev = "b6c1c1bcfa5d831bfd1f278064d7af757f9b38f5" } scale-info = { version = "1.0", default-features = false, features = ["derive"] } -sp-consensus-subspace = { version = "0.1.0", default-features = false, path = "../sp-consensus-subspace" } [features] default = ["std"] @@ -31,6 +30,5 @@ std = [ "frame-support/std", "frame-system/std", "scale-info/std", - "sp-consensus-subspace/std", ] try-runtime = ["frame-support/try-runtime"] diff --git a/crates/pallet-rewards/src/lib.rs b/crates/pallet-rewards/src/lib.rs index f7e24022eb..fe03ad5a78 100644 --- a/crates/pallet-rewards/src/lib.rs +++ b/crates/pallet-rewards/src/lib.rs @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -//! Subspace pallet for issuing rewards to block producers. +//! Pallet for issuing rewards to block producers. #![cfg_attr(not(feature = "std"), no_std)] #![forbid(unsafe_code)] @@ -21,10 +21,9 @@ mod default_weights; -use frame_support::traits::{Currency, Get}; +use frame_support::traits::{Currency, FindAuthor, Get}; use frame_support::weights::Weight; pub use pallet::*; -use sp_consensus_subspace::digests::CompatibleDigestItem; pub trait WeightInfo { fn on_initialize() -> Weight; @@ -34,7 +33,7 @@ pub trait WeightInfo { mod pallet { use super::WeightInfo; use frame_support::pallet_prelude::*; - use frame_support::traits::Currency; + use frame_support::traits::{Currency, FindAuthor}; use frame_system::pallet_prelude::*; type BalanceOf = @@ -51,6 +50,8 @@ mod pallet { #[pallet::constant] type BlockReward: Get>; + type FindAuthor: FindAuthor; + type WeightInfo: WeightInfo; } @@ -81,19 +82,20 @@ mod pallet { impl Pallet { fn do_initialize(_n: T::BlockNumber) { - if let Some(block_author) = frame_system::Pallet::::digest() - .logs - .iter() - .find_map(|s| s.as_subspace_pre_digest()) - .map(|pre_digest| pre_digest.solution.public_key) - { - let reward = T::BlockReward::get(); - T::Currency::deposit_creating(&block_author, reward); - - Self::deposit_event(Event::BlockReward { - block_author, - reward, - }); - } + let block_author = T::FindAuthor::find_author( + frame_system::Pallet::::digest() + .logs + .iter() + .filter_map(|d| d.as_pre_runtime()), + ) + .expect("Block author must always be present; qed"); + + let reward = T::BlockReward::get(); + T::Currency::deposit_creating(&block_author, reward); + + Self::deposit_event(Event::BlockReward { + block_author, + reward, + }); } } diff --git a/crates/pallet-subspace/src/lib.rs b/crates/pallet-subspace/src/lib.rs index 25026161fd..eac3212337 100644 --- a/crates/pallet-subspace/src/lib.rs +++ b/crates/pallet-subspace/src/lib.rs @@ -57,6 +57,7 @@ use sp_runtime::transaction_validity::{ use sp_runtime::{ generic::DigestItem, traits::{One, SaturatedConversion, Saturating, Zero}, + ConsensusEngineId, }; use sp_std::prelude::*; use subspace_core_primitives::{crypto, RootBlock, PIECE_SIZE, RANDOMNESS_LENGTH, SALT_SIZE}; @@ -200,11 +201,6 @@ mod pallet { #[pallet::constant] type RecordedHistorySegmentSize: Get; - /// Replication factor, defines minimum desired number of replicas of the blockchain to be - /// stored by the network. - #[pallet::constant] - type ReplicationFactor: Get; - /// Subspace requires some logic to be triggered on every block to query for whether an epoch /// has ended and to perform the transition to the next epoch. /// @@ -698,14 +694,15 @@ impl Pallet { // TODO: Temporary testnet hack, we don't update solution range for the first 15_000 blocks // in order to seed the blockchain with data quickly - #[cfg(all(feature = "no-early-solution-range-updates", not(test)))] - let solution_range = if block_number < 15_000_u32.into() { - previous_solution_range + let solution_range = if cfg!(all(feature = "no-early-solution-range-updates", not(test))) { + if block_number < 15_000_u32.into() { + previous_solution_range + } else { + (previous_solution_range as f64 * adjustment_factor).round() as u64 + } } else { (previous_solution_range as f64 * adjustment_factor).round() as u64 }; - #[cfg(not(all(feature = "no-early-solution-range-updates", not(test))))] - let solution_range = (previous_solution_range as f64 * adjustment_factor).round() as u64; SolutionRange::::put(solution_range); EraStartSlot::::put(current_slot); @@ -1110,6 +1107,23 @@ impl OnTimestampSet for Pallet { } } +impl frame_support::traits::FindAuthor for Pallet { + fn find_author<'a, I>(digests: I) -> Option + where + I: 'a + IntoIterator, + { + digests.into_iter().find_map(|(id, mut data)| { + if id == SUBSPACE_ENGINE_ID { + PreDigest::decode(&mut data) + .map(|pre_digest| pre_digest.solution.public_key) + .ok() + } else { + None + } + }) + } +} + impl frame_support::traits::Lateness for Pallet { fn lateness(&self) -> T::BlockNumber { Self::lateness() diff --git a/crates/pallet-subspace/src/mock.rs b/crates/pallet-subspace/src/mock.rs index e83f5d5d21..444ebcc614 100644 --- a/crates/pallet-subspace/src/mock.rs +++ b/crates/pallet-subspace/src/mock.rs @@ -165,7 +165,6 @@ impl Config for Test { type ConfirmationDepthK = ConfirmationDepthK; type RecordSize = RecordSize; type RecordedHistorySegmentSize = RecordedHistorySegmentSize; - type ReplicationFactor = ReplicationFactor; type EpochChangeTrigger = NormalEpochChange; type EraChangeTrigger = NormalEraChange; type EonChangeTrigger = NormalEonChange; diff --git a/crates/pallet-transaction-fees/Cargo.toml b/crates/pallet-transaction-fees/Cargo.toml new file mode 100644 index 0000000000..e2003bd19a --- /dev/null +++ b/crates/pallet-transaction-fees/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "pallet-transaction-fees" +version = "0.1.0" +authors = ["Nazar Mokrynskyi "] +edition = "2021" +license = "Apache-2.0" +homepage = "https://subspace.network" +repository = "https://github.com/subspace/subspace" +description = "Pallet for charging and re-distributing transaction fees" +readme = "README.md" +include = [ + "/src", + "/Cargo.toml", + "/README.md", +] + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +codec = { package = "parity-scale-codec", version = "2.3.0", default-features = false, features = ["derive"] } +frame-support = { version = "4.0.0-dev", default-features = false, git = "https://github.com/paritytech/substrate", rev = "b6c1c1bcfa5d831bfd1f278064d7af757f9b38f5" } +frame-system = { version = "4.0.0-dev", default-features = false, git = "https://github.com/paritytech/substrate", rev = "b6c1c1bcfa5d831bfd1f278064d7af757f9b38f5" } +scale-info = { version = "1.0", default-features = false, features = ["derive"] } + +[features] +default = ["std"] +std = [ + "codec/std", + "frame-support/std", + "frame-system/std", + "scale-info/std", +] +try-runtime = ["frame-support/try-runtime"] diff --git a/crates/pallet-transaction-fees/README.md b/crates/pallet-transaction-fees/README.md new file mode 100644 index 0000000000..cca2c2b954 --- /dev/null +++ b/crates/pallet-transaction-fees/README.md @@ -0,0 +1,5 @@ +# Pallet Transaction Fees + +Pallet for charging and re-distributing transaction fees. + +License: Apache-2.0 diff --git a/crates/pallet-transaction-fees/src/default_weights.rs b/crates/pallet-transaction-fees/src/default_weights.rs new file mode 100644 index 0000000000..813e030629 --- /dev/null +++ b/crates/pallet-transaction-fees/src/default_weights.rs @@ -0,0 +1,26 @@ +// Copyright (C) 2021 Subspace Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Default weights for the Rewards Pallet +//! This file was not auto-generated. + +use frame_support::weights::Weight; + +impl crate::WeightInfo for () { + fn on_initialize() -> Weight { + // TODO: Correct value + 1 + } +} diff --git a/crates/pallet-transaction-fees/src/lib.rs b/crates/pallet-transaction-fees/src/lib.rs new file mode 100644 index 0000000000..4fda394dd1 --- /dev/null +++ b/crates/pallet-transaction-fees/src/lib.rs @@ -0,0 +1,294 @@ +// Copyright (C) 2021 Subspace Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Pallet for charging and re-distributing transaction fees. + +#![cfg_attr(not(feature = "std"), no_std)] +#![forbid(unsafe_code)] +#![warn(rust_2018_idioms, missing_debug_implementations)] + +mod default_weights; + +use codec::{Codec, Decode, Encode}; +use frame_support::sp_runtime::traits::Zero; +use frame_support::traits::{Currency, FindAuthor, Get}; +use frame_support::weights::Weight; +pub use pallet::*; +use scale_info::TypeInfo; + +type BalanceOf = + <::Currency as Currency<::AccountId>>::Balance; + +pub trait WeightInfo { + fn on_initialize() -> Weight; +} + +#[derive(Encode, Decode, TypeInfo)] +struct CollectedFees { + storage: Balance, + compute: Balance, + // TODO: Split tips for storage and compute proportionally? + tips: Balance, +} + +#[frame_support::pallet] +mod pallet { + use super::{BalanceOf, CollectedFees, WeightInfo}; + use frame_support::pallet_prelude::*; + use frame_support::traits::{Currency, FindAuthor}; + use frame_system::pallet_prelude::*; + + #[pallet::config] + pub trait Config: frame_system::Config { + /// `pallet-transaction-fees` events + type Event: From> + IsType<::Event>; + + /// Minimum desired number of replicas of the blockchain to be stored by the network, + /// impacts storage fees. + #[pallet::constant] + type MinReplicationFactor: Get; + + /// How much (ratio) of storage fees escrow should be given to farmer each block as a + /// reward. + #[pallet::constant] + type StorageFeesEscrowBlockReward: Get<(u64, u64)>; + + /// How much (ratio) of storage fees collected in a block should be put into storage fees + /// escrow (with remaining issued to farmer immediately). + #[pallet::constant] + type StorageFeesEscrowBlockTax: Get<(u64, u64)>; + + /// How many credits there is in circulation. + #[pallet::constant] + type CreditSupply: Get>; + + /// How much space there is on the network. + #[pallet::constant] + type TotalSpacePledged: Get; + + /// How big is the history of the blockchain in archived state (thus includes erasure + /// coding, but not replication). + #[pallet::constant] + type BlockchainHistorySize: Get; + + type Currency: Currency; + + type FindAuthor: FindAuthor; + + type WeightInfo: WeightInfo; + } + + /// Escrow of storage fees, a portion of it is released to the block author on every block + /// and portion of storage fees goes back into this pot. + #[pallet::storage] + #[pallet::getter(fn storage_fees_escrow)] + pub(super) type CollectedStorageFeesEscrow = StorageValue<_, BalanceOf, ValueQuery>; + + /// Temporary value (cleared at block finalization) which contains cached value of + /// `TransactionByteFee` for current block. + #[pallet::storage] + pub(super) type TransactionByteFee = StorageValue<_, BalanceOf>; + + /// Temporary value (cleared at block finalization) which contains current block author, so we + /// can issue rewards during block finalization. + #[pallet::storage] + pub(super) type BlockAuthor = StorageValue<_, T::AccountId>; + + /// Temporary value (cleared at block finalization) which contains current block fees, so we can + /// issue rewards during block finalization. + #[pallet::storage] + pub(super) type CollectedBlockFees = StorageValue<_, CollectedFees>>; + + /// Pallet rewards for issuing rewards to block producers. + #[pallet::pallet] + #[pallet::generate_store(pub(super) trait Store)] + pub struct Pallet(_); + + /// `pallet-transaction-fees` events + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + /// Storage fees escrow change. + StorageFeesEscrowChange { + /// State of storage fees escrow before block execution. + before: BalanceOf, + /// State of storage fees escrow after block execution. + after: BalanceOf, + }, + /// Storage fees. + StorageFeesReward { + /// Receiver of the storage fees. + who: T::AccountId, + /// Amount of collected storage fees. + amount: BalanceOf, + }, + /// Compute fees. + ComputeFeesReward { + /// Receiver of the compute fees. + who: T::AccountId, + /// Amount of collected compute fees. + amount: BalanceOf, + }, + /// Tips. + TipsReward { + /// Receiver of the tip. + who: T::AccountId, + /// Amount of collected tips. + amount: BalanceOf, + }, + } + + #[pallet::hooks] + impl Hooks> for Pallet + where + BalanceOf: From + From, + { + fn on_initialize(now: BlockNumberFor) -> Weight { + Self::do_initialize(now); + T::WeightInfo::on_initialize() + } + + fn on_finalize(now: BlockNumberFor) { + Self::do_finalize(now); + } + } +} + +impl Pallet +where + BalanceOf: From, +{ + fn do_initialize(_n: T::BlockNumber) { + let block_author = T::FindAuthor::find_author( + frame_system::Pallet::::digest() + .logs + .iter() + .filter_map(|d| d.as_pre_runtime()), + ) + .expect("Block author must always be present; qed"); + + BlockAuthor::::put(block_author); + + CollectedBlockFees::::put(CollectedFees { + storage: BalanceOf::::zero(), + compute: BalanceOf::::zero(), + tips: BalanceOf::::zero(), + }); + } + + // TODO: Fees will be split between farmers and executors in the future + fn do_finalize(_n: T::BlockNumber) { + TransactionByteFee::::take(); + + let block_author = + BlockAuthor::::take().expect("`BlockAuthor` was set in `on_initialize`; qed"); + + let original_storage_fees_escrow = CollectedStorageFeesEscrow::::get(); + let collected_fees = CollectedBlockFees::::take() + .expect("`CollectedBlockFees` was set in `on_initialize`; qed"); + + let mut storage_fees_escrow = original_storage_fees_escrow; + + // Take a portion of storage fees escrow as a farmer reward. + let storage_fees_escrow_reward = storage_fees_escrow + / T::StorageFeesEscrowBlockReward::get().1.into() + * T::StorageFeesEscrowBlockReward::get().0.into(); + storage_fees_escrow -= storage_fees_escrow_reward; + + // Take a portion of storage fees collected in this block as a farmer reward. + let collected_storage_fees_reward = collected_fees.storage + / T::StorageFeesEscrowBlockTax::get().1.into() + * (T::StorageFeesEscrowBlockTax::get().1 - T::StorageFeesEscrowBlockTax::get().0) + .into(); + storage_fees_escrow += collected_fees.storage - collected_storage_fees_reward; + + // Update storage fees escrow. + if storage_fees_escrow != original_storage_fees_escrow { + CollectedStorageFeesEscrow::::put(storage_fees_escrow); + Self::deposit_event(Event::::StorageFeesEscrowChange { + before: original_storage_fees_escrow, + after: storage_fees_escrow, + }); + } + + // Issue storage fees reward. + let storage_fees_reward = storage_fees_escrow_reward + collected_storage_fees_reward; + if !storage_fees_reward.is_zero() { + T::Currency::deposit_into_existing(&block_author, storage_fees_reward).expect( + "Farmer account must have already received the block reward before fees are \ + collected; qed", + ); + Self::deposit_event(Event::::StorageFeesReward { + who: block_author.clone(), + amount: storage_fees_reward, + }); + } + + // Issue compute fees reward. + if !collected_fees.compute.is_zero() { + T::Currency::deposit_into_existing(&block_author, collected_fees.compute) + .expect("Executor account must already exist before they execute the block; qed"); + Self::deposit_event(Event::::ComputeFeesReward { + who: block_author.clone(), + amount: collected_fees.compute, + }); + } + + // Issue tips reward. + if !collected_fees.tips.is_zero() { + T::Currency::deposit_into_existing(&block_author, collected_fees.tips) + .expect("Executor account must already exist before they execute the block; qed"); + Self::deposit_event(Event::::TipsReward { + who: block_author, + amount: collected_fees.tips, + }); + } + } + + pub fn transaction_byte_fee() -> BalanceOf { + if let Some(transaction_byte_fee) = TransactionByteFee::::get() { + return transaction_byte_fee; + } + + let credit_supply = T::CreditSupply::get(); + + let transaction_byte_fee = match T::TotalSpacePledged::get().checked_sub( + T::BlockchainHistorySize::get() * u64::from(T::MinReplicationFactor::get()), + ) { + Some(free_space) if free_space > 0 => credit_supply / BalanceOf::::from(free_space), + _ => credit_supply, + }; + + // Cache value for this block. + TransactionByteFee::::put(transaction_byte_fee); + + transaction_byte_fee + } + + pub fn note_transaction_fees( + storage_fee: BalanceOf, + compute_fee: BalanceOf, + tip: BalanceOf, + ) { + CollectedBlockFees::::mutate(|collected_block_fees| { + let collected_block_fees = collected_block_fees + .as_mut() + .expect("`CollectedBlockFees` was set in `on_initialize`; qed"); + collected_block_fees.storage += storage_fee; + collected_block_fees.compute += compute_fee; + collected_block_fees.tips += tip; + }); + } +} diff --git a/crates/sp-executor/src/lib.rs b/crates/sp-executor/src/lib.rs index dd29d985d0..87da293c9a 100644 --- a/crates/sp-executor/src/lib.rs +++ b/crates/sp-executor/src/lib.rs @@ -18,7 +18,6 @@ #![cfg_attr(not(feature = "std"), no_std)] use sp_runtime::traits::{Block as BlockT, Header as HeaderT}; -use sp_std::vec::Vec; sp_api::decl_runtime_apis! { /// API necessary for executor pallet. diff --git a/crates/subspace-runtime-primitives/src/lib.rs b/crates/subspace-runtime-primitives/src/lib.rs index f10aa05d28..479fc07633 100644 --- a/crates/subspace-runtime-primitives/src/lib.rs +++ b/crates/subspace-runtime-primitives/src/lib.rs @@ -46,10 +46,17 @@ pub const RECORD_SIZE: u32 = PIECE_SIZE as u32 - WITNESS_SIZE; /// be erasure coded and together with corresponding witnesses will result in `MERKLE_NUM_LEAVES` /// pieces of archival history. pub const RECORDED_HISTORY_SEGMENT_SIZE: u32 = RECORD_SIZE * MERKLE_NUM_LEAVES / 2; -/// Replication factor, defines minimum desired number of replicas of the blockchain to be -/// stored by the network. +/// Minimum desired number of replicas of the blockchain to be stored by the network, +/// impacts storage fees. // TODO: Proper value here -pub const REPLICATION_FACTOR: u16 = 1; +pub const MIN_REPLICATION_FACTOR: u16 = 1; +/// How much (ratio) of storage fees escrow should be given to farmer each block as a reward. +// TODO: Proper value here +pub const STORAGE_FEES_ESCROW_BLOCK_REWARD: (u64, u64) = (1, 100); +/// How much (ratio) of storage fees collected in a block should be put into storage fees escrow +/// (with remaining issued to farmer immediately). +// TODO: Proper value here +pub const STORAGE_FEES_ESCROW_BLOCK_TAX: (u64, u64) = (1, 2); /// An index to a block. pub type BlockNumber = u32; diff --git a/crates/subspace-runtime/Cargo.toml b/crates/subspace-runtime/Cargo.toml index 4ab2de7664..9563dfe7d3 100644 --- a/crates/subspace-runtime/Cargo.toml +++ b/crates/subspace-runtime/Cargo.toml @@ -31,6 +31,7 @@ pallet-rewards = { version = "0.1.0", default-features = false, path = "../palle pallet-subspace = { version = "0.1.0", default-features = false, features = ["no-early-solution-range-updates"], path = "../pallet-subspace" } pallet-sudo = { version = "4.0.0-dev", default-features = false, git = "https://github.com/paritytech/substrate", rev = "b6c1c1bcfa5d831bfd1f278064d7af757f9b38f5" } pallet-timestamp = { version = "4.0.0-dev", default-features = false, git = "https://github.com/paritytech/substrate", rev = "b6c1c1bcfa5d831bfd1f278064d7af757f9b38f5" } +pallet-transaction-fees = { version = "0.1.0", default-features = false, path = "../pallet-transaction-fees" } pallet-transaction-payment = { version = "4.0.0-dev", default-features = false, git = "https://github.com/paritytech/substrate", rev = "b6c1c1bcfa5d831bfd1f278064d7af757f9b38f5" } pallet-utility = { version = "4.0.0-dev", default-features = false, git = "https://github.com/paritytech/substrate", rev = "b6c1c1bcfa5d831bfd1f278064d7af757f9b38f5" } scale-info = { version = "1.0", default-features = false, features = ["derive"] } @@ -79,6 +80,7 @@ std = [ "pallet-subspace/std", "pallet-sudo/std", "pallet-timestamp/std", + "pallet-transaction-fees/std", "pallet-transaction-payment-rpc-runtime-api/std", "pallet-transaction-payment/std", "pallet-utility/std", diff --git a/crates/subspace-runtime/src/lib.rs b/crates/subspace-runtime/src/lib.rs index 3b4781b42e..549bb5d0a7 100644 --- a/crates/subspace-runtime/src/lib.rs +++ b/crates/subspace-runtime/src/lib.rs @@ -23,7 +23,7 @@ include!(concat!(env!("OUT_DIR"), "/wasm_binary.rs")); use codec::{Compact, CompactLen, Encode}; -use frame_support::traits::Get; +use frame_support::traits::{Currency, ExistenceRequirement, Get, Imbalance, WithdrawReasons}; use frame_support::weights::{ constants::{RocksDbWeight, WEIGHT_PER_SECOND}, IdentityFee, @@ -31,19 +31,21 @@ use frame_support::weights::{ use frame_support::{construct_runtime, parameter_types}; use frame_system::limits::{BlockLength, BlockWeights}; use frame_system::EnsureNever; -use pallet_transaction_payment::CurrencyAdapter; +use pallet_balances::NegativeImbalance; use sp_api::impl_runtime_apis; use sp_consensus_subspace::{ Epoch, EquivocationProof, FarmerPublicKey, Salts, SubspaceEpochConfiguration, SubspaceGenesisConfiguration, }; use sp_core::{crypto::KeyTypeId, OpaqueMetadata}; -use sp_runtime::traits::{AccountIdLookup, BlakeTwo256, Block as BlockT, Header as HeaderT}; -use sp_runtime::{ - create_runtime_str, generic, - transaction_validity::{TransactionSource, TransactionValidity}, - ApplyExtrinsicResult, Perbill, +use sp_runtime::traits::{ + AccountIdLookup, BlakeTwo256, Block as BlockT, DispatchInfoOf, Header as HeaderT, + PostDispatchInfoOf, Zero, }; +use sp_runtime::transaction_validity::{ + InvalidTransaction, TransactionSource, TransactionValidity, TransactionValidityError, +}; +use sp_runtime::{create_runtime_str, generic, ApplyExtrinsicResult, Perbill}; use sp_std::prelude::*; #[cfg(feature = "std")] use sp_version::NativeVersion; @@ -52,7 +54,8 @@ use subspace_core_primitives::objects::{BlockObject, BlockObjectMapping}; use subspace_core_primitives::{RootBlock, Sha256Hash, PIECE_SIZE}; pub use subspace_runtime_primitives::{ opaque, AccountId, Balance, BlockNumber, Hash, Index, Moment, Signature, CONFIRMATION_DEPTH_K, - RECORDED_HISTORY_SEGMENT_SIZE, RECORD_SIZE, REPLICATION_FACTOR, + MIN_REPLICATION_FACTOR, RECORDED_HISTORY_SEGMENT_SIZE, RECORD_SIZE, + STORAGE_FEES_ESCROW_BLOCK_REWARD, STORAGE_FEES_ESCROW_BLOCK_TAX, }; sp_runtime::impl_opaque_keys! { @@ -220,7 +223,6 @@ parameter_types! { pub const ConfirmationDepthK: u32 = CONFIRMATION_DEPTH_K; pub const RecordSize: u32 = RECORD_SIZE; pub const RecordedHistorySegmentSize: u32 = RECORDED_HISTORY_SEGMENT_SIZE; - pub const ReplicationFactor: u16 = REPLICATION_FACTOR; pub const ReportLongevity: u64 = EPOCH_DURATION_IN_BLOCKS as u64; } @@ -235,7 +237,6 @@ impl pallet_subspace::Config for Runtime { type ConfirmationDepthK = ConfirmationDepthK; type RecordSize = RecordSize; type RecordedHistorySegmentSize = RecordedHistorySegmentSize; - type ReplicationFactor = ReplicationFactor; type EpochChangeTrigger = pallet_subspace::NormalEpochChange; type EraChangeTrigger = pallet_subspace::NormalEraChange; type EonChangeTrigger = pallet_subspace::NormalEonChange; @@ -278,6 +279,54 @@ impl pallet_balances::Config for Runtime { type WeightInfo = pallet_balances::weights::SubstrateWeight; } +parameter_types! { + pub const ReplicationFactor: u16 = MIN_REPLICATION_FACTOR; + pub const StorageFeesEscrowBlockReward: (u64, u64) = STORAGE_FEES_ESCROW_BLOCK_REWARD; + pub const StorageFeesEscrowBlockTax: (u64, u64) = STORAGE_FEES_ESCROW_BLOCK_TAX; +} + +pub struct CreditSupply; + +impl Get for CreditSupply { + fn get() -> Balance { + Balances::total_issuance() + } +} + +pub struct TotalSpacePledged; + +impl Get for TotalSpacePledged { + fn get() -> u64 { + let piece_size = u64::try_from(PIECE_SIZE) + .expect("Piece size is definitely small enough to fit into u64; qed"); + // Operations reordered to avoid u64 overflow, but essentially are: + // u64::MAX * SlotProbability / (solution_range / PIECE_SIZE) + u64::MAX / Subspace::solution_range() * piece_size * SlotProbability::get().0 + / SlotProbability::get().1 + } +} + +pub struct BlockchainHistorySize; + +impl Get for BlockchainHistorySize { + fn get() -> u64 { + Subspace::archived_history_size() + } +} + +impl pallet_transaction_fees::Config for Runtime { + type Event = Event; + type MinReplicationFactor = ReplicationFactor; + type StorageFeesEscrowBlockReward = StorageFeesEscrowBlockReward; + type StorageFeesEscrowBlockTax = StorageFeesEscrowBlockTax; + type CreditSupply = CreditSupply; + type TotalSpacePledged = TotalSpacePledged; + type BlockchainHistorySize = BlockchainHistorySize; + type Currency = Balances; + type FindAuthor = Subspace; + type WeightInfo = (); +} + pub struct TransactionByteFee; impl Get for TransactionByteFee { @@ -285,21 +334,7 @@ impl Get for TransactionByteFee { if cfg!(feature = "do-not-enforce-cost-of-storage") { 1 } else { - let credit_supply = Balances::total_issuance(); - let total_space = Balance::from( - u64::MAX * SlotProbability::get().0 - / SlotProbability::get().1 - / (Subspace::solution_range() - / u64::try_from(PIECE_SIZE) - .expect("Piece size is definitely small enough to fit into u64; qed")), - ); - let blockchain_size = Balance::from(Subspace::archived_history_size()); - - match total_space.checked_sub(blockchain_size * Balance::from(ReplicationFactor::get())) - { - Some(free_space) if free_space > 0 => credit_supply / free_space, - _ => Balance::MAX, - } + TransactionFees::transaction_byte_fee() } } } @@ -308,8 +343,97 @@ parameter_types! { pub const OperationalFeeMultiplier: u8 = 5; } +pub struct LiquidityInfo { + storage_fee: Balance, + imbalance: NegativeImbalance, +} + +/// Implementation of [`pallet_transaction_payment::OnChargeTransaction`] that charges transaction +/// fees and distributes storage/compute fees and tip separately. +pub struct OnChargeTransaction; + +impl pallet_transaction_payment::OnChargeTransaction for OnChargeTransaction { + type LiquidityInfo = Option; + type Balance = Balance; + + fn withdraw_fee( + who: &AccountId, + call: &Call, + _info: &DispatchInfoOf, + fee: Self::Balance, + tip: Self::Balance, + ) -> Result { + if fee.is_zero() { + return Ok(None); + } + + let withdraw_reason = if tip.is_zero() { + WithdrawReasons::TRANSACTION_PAYMENT + } else { + WithdrawReasons::TRANSACTION_PAYMENT | WithdrawReasons::TIP + }; + + let withdraw_result = >::withdraw( + who, + fee, + withdraw_reason, + ExistenceRequirement::KeepAlive, + ); + let imbalance = withdraw_result.map_err(|_error| InvalidTransaction::Payment)?; + + // Separate storage fee while we have access to the call data structure to calculate it. + let storage_fee = TransactionByteFee::get() + * Balance::try_from(call.encoded_size()) + .expect("Size of the call never exceeds balance units; qed"); + + Ok(Some(LiquidityInfo { + storage_fee, + imbalance, + })) + } + + fn correct_and_deposit_fee( + who: &AccountId, + _dispatch_info: &DispatchInfoOf, + _post_info: &PostDispatchInfoOf, + corrected_fee: Self::Balance, + tip: Self::Balance, + liquidity_info: Self::LiquidityInfo, + ) -> Result<(), TransactionValidityError> { + if let Some(LiquidityInfo { + storage_fee, + imbalance, + }) = liquidity_info + { + // Calculate how much refund we should return + let refund_amount = imbalance.peek().saturating_sub(corrected_fee); + // Refund to the the account that paid the fees. If this fails, the account might have + // dropped below the existential balance. In that case we don't refund anything. + let refund_imbalance = Balances::deposit_into_existing(who, refund_amount) + .unwrap_or_else(|_| >::PositiveImbalance::zero()); + // Merge the imbalance caused by paying the fees and refunding parts of it again. + let adjusted_paid = imbalance + .offset(refund_imbalance) + .same() + .map_err(|_| TransactionValidityError::Invalid(InvalidTransaction::Payment))?; + + // Split the tip from the total fee that ended up being paid. + let (tip, fee) = adjusted_paid.split(tip); + // Split paid storage and compute fees so that they can be distributed separately. + let (paid_storage_fee, paid_compute_fee) = fee.split(storage_fee); + + TransactionFees::note_transaction_fees( + paid_storage_fee.peek(), + paid_compute_fee.peek(), + tip.peek(), + ); + } + Ok(()) + } +} + impl pallet_transaction_payment::Config for Runtime { - type OnChargeTransaction = CurrencyAdapter; + type OnChargeTransaction = OnChargeTransaction; type TransactionByteFee = TransactionByteFee; type OperationalFeeMultiplier = OperationalFeeMultiplier; type WeightToFee = IdentityFee; @@ -353,6 +477,7 @@ impl pallet_rewards::Config for Runtime { type Event = Event; type Currency = Balances; type BlockReward = BlockReward; + type FindAuthor = Subspace; type WeightInfo = (); } @@ -394,6 +519,7 @@ construct_runtime!( Rewards: pallet_rewards = 9, Balances: pallet_balances = 4, + TransactionFees: pallet_transaction_fees = 12, TransactionPayment: pallet_transaction_payment = 5, Utility: pallet_utility = 8, diff --git a/substrate/substrate-test-runtime/src/lib.rs b/substrate/substrate-test-runtime/src/lib.rs index 69a67fd2c9..fbf01c4220 100644 --- a/substrate/substrate-test-runtime/src/lib.rs +++ b/substrate/substrate-test-runtime/src/lib.rs @@ -662,7 +662,6 @@ impl pallet_subspace::Config for Runtime { type ConfirmationDepthK = ConfirmationDepthK; type RecordSize = RecordSize; type RecordedHistorySegmentSize = RecordedHistorySegmentSize; - type ReplicationFactor = ReplicationFactor; type EpochChangeTrigger = pallet_subspace::NormalEpochChange; type EraChangeTrigger = pallet_subspace::NormalEraChange; type EonChangeTrigger = pallet_subspace::NormalEonChange;