Skip to main content

pallet_block_fees/
lib.rs

1// Copyright (C) 2023 Subspace Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at
7//
8// 	http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16//! Pallet Domain Transaction Fees
17
18#![cfg_attr(not(feature = "std"), no_std)]
19// TODO: Remove once FRAME macros stop generating `.clone()` calls on `Copy` types with user spans
20#![expect(clippy::clone_on_copy, reason = "Comes from Substrate")]
21
22#[cfg(not(feature = "std"))]
23extern crate alloc;
24
25pub mod fees;
26
27pub use pallet::*;
28
29#[frame_support::pallet]
30mod pallet {
31    #[cfg(not(feature = "std"))]
32    use alloc::vec::Vec;
33    use frame_support::pallet_prelude::*;
34    use frame_support::storage::generator::StorageValue as _;
35    use frame_system::pallet_prelude::*;
36    use parity_scale_codec::{Codec, MaxEncodedLen};
37    use scale_info::TypeInfo;
38    use sp_block_fees::{INHERENT_IDENTIFIER, InherentError, InherentType};
39    use sp_domains::ChainId;
40    use sp_domains::execution_receipt::BlockFees;
41    use sp_runtime::traits::{AtLeast32BitUnsigned, MaybeSerializeDeserialize, Saturating};
42    use sp_runtime::{FixedPointOperand, SaturatedConversion};
43    use sp_std::fmt::Debug;
44    use sp_std::result;
45
46    #[pallet::config]
47    pub trait Config: frame_system::Config {
48        /// The balance of an account.
49        type Balance: Parameter
50            + Member
51            + AtLeast32BitUnsigned
52            + Codec
53            + Default
54            + Copy
55            + MaybeSerializeDeserialize
56            + Debug
57            + MaxEncodedLen
58            + TypeInfo
59            + FixedPointOperand;
60
61        /// The domain chain byte fee
62        type DomainChainByteFee: Get<Self::Balance>;
63    }
64
65    /// The accumulated rewards of the current block
66    ///
67    /// Currently, the only source of rewards is the transaction fees, in the future it
68    /// will include the XDM reward.
69    #[pallet::storage]
70    #[pallet::getter(fn collected_block_fees)]
71    pub(super) type CollectedBlockFees<T: Config> =
72        StorageValue<_, BlockFees<T::Balance>, ValueQuery>;
73
74    /// The consensus chain byte fee
75    ///
76    /// NOTE: we are using `ValueQuery` for convenience, which means the transactions in the domain block #1
77    // are not charged for the consensus chain storage fees.
78    #[pallet::storage]
79    #[pallet::getter(fn consensus_chain_byte_fee)]
80    pub(super) type ConsensusChainByteFee<T: Config> = StorageValue<_, T::Balance, ValueQuery>;
81
82    /// The next consensus chain byte fee, it will take affect after the execution of the current
83    /// block to ensure the operator are using the same fee for both validating and executing the domain
84    /// transaction in the next block.
85    #[pallet::storage]
86    pub(super) type NextConsensusChainByteFee<T: Config> = StorageValue<_, T::Balance, ValueQuery>;
87
88    /// Pallet block-fees to store the accumulated rewards of the current block
89    #[pallet::pallet]
90    #[pallet::without_storage_info]
91    pub struct Pallet<T>(_);
92
93    #[pallet::hooks]
94    impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
95        fn on_initialize(_now: BlockNumberFor<T>) -> Weight {
96            // NOTE: set the `CollectedBlockFees` to an empty value instead of removing the value
97            // completely so we can generate a storage proof to prove the empty value, which is used
98            // in the fraud proof.
99            CollectedBlockFees::<T>::set(BlockFees::<T::Balance>::default());
100            T::DbWeight::get().writes(1)
101        }
102
103        fn on_finalize(_now: BlockNumberFor<T>) {
104            let transaction_byte_fee = NextConsensusChainByteFee::<T>::take();
105            ConsensusChainByteFee::<T>::put(transaction_byte_fee);
106        }
107    }
108
109    #[pallet::call]
110    impl<T: Config> Pallet<T> {
111        #[pallet::call_index(0)]
112        #[pallet::weight((
113        // For now, it is safe to treat this call as lightweight, because ensure_none() only
114        // performs a few enum variant checks (which may optimise to a single instruction).
115        // Each instruction typically weighs under 300 picoseconds. Similarly, a Balance is at most
116        // 16 bytes.
117        // TODO: eventually we should benchmark this weight
118        Weight::from_all(10_000).saturating_add(
119            T::DbWeight::get().writes(1)
120        ),
121        DispatchClass::Mandatory
122        ))]
123        pub fn set_next_consensus_chain_byte_fee(
124            origin: OriginFor<T>,
125            #[pallet::compact] transaction_byte_fee: T::Balance,
126        ) -> DispatchResult {
127            ensure_none(origin)?;
128            NextConsensusChainByteFee::<T>::put(transaction_byte_fee);
129            Ok(())
130        }
131    }
132
133    #[pallet::inherent]
134    impl<T: Config> ProvideInherent for Pallet<T> {
135        type Call = Call<T>;
136        type Error = InherentError;
137        const INHERENT_IDENTIFIER: InherentIdentifier = INHERENT_IDENTIFIER;
138
139        fn create_inherent(data: &InherentData) -> Option<Self::Call> {
140            let inherent_data = data
141                .get_data::<InherentType>(&INHERENT_IDENTIFIER)
142                .expect("Domain block fees inherent data not correctly encoded")
143                .expect("Domain block fees inherent data must be provided");
144
145            let transaction_byte_fee = inherent_data.saturated_into::<T::Balance>();
146
147            Some(Call::set_next_consensus_chain_byte_fee {
148                transaction_byte_fee,
149            })
150        }
151
152        fn check_inherent(
153            call: &Self::Call,
154            data: &InherentData,
155        ) -> result::Result<(), Self::Error> {
156            let inherent_data = data
157                .get_data::<InherentType>(&INHERENT_IDENTIFIER)
158                .expect("Domain block fees inherent data not correctly encoded")
159                .expect("Domain block fees inherent data must be provided");
160
161            let provided_transaction_byte_fee = inherent_data.saturated_into::<T::Balance>();
162
163            if let Call::set_next_consensus_chain_byte_fee {
164                transaction_byte_fee,
165            } = call
166                && transaction_byte_fee != &provided_transaction_byte_fee
167            {
168                return Err(InherentError::IncorrectConsensusChainByteFee);
169            }
170
171            Ok(())
172        }
173
174        fn is_inherent(call: &Self::Call) -> bool {
175            matches!(call, Call::set_next_consensus_chain_byte_fee { .. })
176        }
177    }
178
179    impl<T: Config> Pallet<T> {
180        /// Note the domain execution fee including the storage and compute fee on domain chain,
181        /// tip, and the XDM reward.
182        pub fn note_domain_execution_fee(rewards: T::Balance) {
183            CollectedBlockFees::<T>::mutate(|block_fees| {
184                block_fees.domain_execution_fee =
185                    block_fees.domain_execution_fee.saturating_add(rewards);
186            });
187        }
188
189        /// Note consensus chain storage fee
190        pub fn note_consensus_storage_fee(storage_fee: T::Balance) {
191            CollectedBlockFees::<T>::mutate(|block_fees| {
192                block_fees.consensus_storage_fee =
193                    block_fees.consensus_storage_fee.saturating_add(storage_fee);
194            });
195        }
196
197        /// Note burned balance on domains
198        pub fn note_burned_balance(burned_balance: T::Balance) {
199            CollectedBlockFees::<T>::mutate(|block_fees| {
200                block_fees.burned_balance =
201                    block_fees.burned_balance.saturating_add(burned_balance);
202            });
203        }
204
205        /// Note chain reward fees.
206        pub fn note_chain_rewards(chain_id: ChainId, balance: T::Balance) {
207            CollectedBlockFees::<T>::mutate(|block_fees| {
208                let total_balance = match block_fees.chain_rewards.get(&chain_id) {
209                    None => balance,
210                    Some(prev_balance) => prev_balance.saturating_add(balance),
211                };
212                block_fees.chain_rewards.insert(chain_id, total_balance)
213            });
214        }
215
216        /// Return the final domain transaction byte fee, which consist of:
217        /// - The `ConsensusChainByteFee` for the consensus chain storage cost since the domain
218        ///   transaction need to be bundled and submitted to the consensus chain first.
219        ///
220        /// - The `DomainChainByteFee` for the domain chain storage cost
221        pub fn final_domain_transaction_byte_fee() -> T::Balance {
222            ConsensusChainByteFee::<T>::get().saturating_add(T::DomainChainByteFee::get())
223        }
224
225        pub fn block_fees_storage_key() -> Vec<u8> {
226            CollectedBlockFees::<T>::storage_value_final_key().to_vec()
227        }
228    }
229}