diff --git a/pallets/dactr/src/extensions/check_app_id.rs b/pallets/dactr/src/extensions/check_app_id.rs index 51bb2c639..77a036f02 100644 --- a/pallets/dactr/src/extensions/check_app_id.rs +++ b/pallets/dactr/src/extensions/check_app_id.rs @@ -81,11 +81,17 @@ where /// production. pub fn do_validate( &self, + who: &T::AccountId, call: &::RuntimeCall, len: usize, ) -> TransactionValidity { self.ensure_valid_app_id(call)?; if let Some(DACall::::submit_data { .. }) = call.is_sub_type() { + ensure!( + >::is_submit_data_whitelisted(who), + InvalidTransaction::BadSigner + ); + let all_extrinsics_len = self .next_all_extrinsics_len(len) .ok_or(InvalidTransaction::ExhaustsResources)?; @@ -254,7 +260,7 @@ where _info: &DispatchInfoOf, len: usize, ) -> TransactionValidity { - self.do_validate(call, len) + self.do_validate(_who, call, len) } fn pre_dispatch( @@ -264,7 +270,7 @@ where _info: &DispatchInfoOf, len: usize, ) -> Result { - self.do_validate(call, len)?; + self.do_validate(_who, call, len)?; Ok(()) } @@ -298,6 +304,7 @@ mod tests { }; use frame_system::pallet::Call as SysCall; use sp_runtime::transaction_validity::InvalidTransaction; + use sp_runtime::AccountId32; use test_case::test_case; use super::*; @@ -320,6 +327,12 @@ mod tests { )) } + fn alice() -> AccountId32 { + let mut account = [0u8; 32]; + account[0] = 1; + AccountId32::new(account) + } + #[test_case(1, submit_data_call() => Ok(ValidTransaction::default()); "Submit Data call should be allowed to use any valid AppId" )] #[test_case(100, submit_data_call() => to_invalid_tx(InvalidAppId); "Submit Data call with invalid AppId should be blocked" )] #[test_case(0, remark_call() => Ok(ValidTransaction::default()); "Any Non-Submit-Data call with AppId == 0 should be allowed" )] @@ -328,6 +341,9 @@ mod tests { let extrinsic = AppUncheckedExtrinsic::::new_unsigned(call.clone()); let len = extrinsic.encoded_size(); - new_test_ext().execute_with(|| CheckAppId::::from(AppId(id)).do_validate(&call, len)) + new_test_ext().execute_with(|| { + crate::SubmitDataWhitelist::::insert(alice(), ()); + CheckAppId::::from(AppId(id)).do_validate(&alice(), &call, len) + }) } } diff --git a/pallets/dactr/src/extensions/check_batch_transactions.rs b/pallets/dactr/src/extensions/check_batch_transactions.rs index d83dad270..5b11d8b5a 100644 --- a/pallets/dactr/src/extensions/check_batch_transactions.rs +++ b/pallets/dactr/src/extensions/check_batch_transactions.rs @@ -393,6 +393,7 @@ mod tests { use pallet_utility::pallet::Call as UtilityCall; use sp_core::H256; use sp_runtime::transaction_validity::{InvalidTransaction, TransactionValidityError}; + use sp_runtime::AccountId32; use test_case::test_case; use super::*; @@ -442,11 +443,20 @@ mod tests { )) } + fn alice() -> AccountId32 { + let mut account = [0u8; 32]; + account[0] = 1; + AccountId32::new(account) + } + fn validate(call: RuntimeCall) -> TransactionValidity { let extrinsic = AppUncheckedExtrinsic::::new_unsigned(call.clone()); let len = extrinsic.encoded_size(); - new_test_ext().execute_with(|| CheckAppId::::from(AppId(0)).do_validate(&call, len)) + new_test_ext().execute_with(|| { + crate::SubmitDataWhitelist::::insert(alice(), ()); + CheckAppId::::from(AppId(0)).do_validate(&alice(), &call, len) + }) } #[test] diff --git a/pallets/dactr/src/lib.rs b/pallets/dactr/src/lib.rs index 1a52eb9ae..5bdf20e69 100644 --- a/pallets/dactr/src/lib.rs +++ b/pallets/dactr/src/lib.rs @@ -150,6 +150,11 @@ pub mod pallet { #[pallet::storage] pub type SubmitDataFeeModifier = StorageValue<_, DispatchFeeModifier, ValueQuery>; + /// Accounts allowed to submit DA transactions after the hard cutover. + #[pallet::storage] + pub type SubmitDataWhitelist = + StorageMap<_, Blake2_128Concat, T::AccountId, (), OptionQuery>; + #[pallet::call] impl Pallet { /// Creates an application key if `key` does not exist yet. @@ -188,6 +193,10 @@ pub mod pallet { data: AppDataFor, ) -> DispatchResultWithPostInfo { let who = ensure_signed(origin)?; + ensure!( + SubmitDataWhitelist::::contains_key(&who), + Error::::SubmitDataSignerNotWhitelisted + ); ensure!(!data.is_empty(), Error::::DataCannotBeEmpty); let data_hash = blake2_256(&data); @@ -300,6 +309,25 @@ pub mod pallet { Ok(().into()) } + + #[pallet::call_index(5)] + #[pallet::weight(::DbWeight::get().writes(1))] + pub fn set_submit_data_whitelist( + origin: OriginFor, + account: T::AccountId, + allowed: bool, + ) -> DispatchResultWithPostInfo { + ensure_root(origin)?; + + if allowed { + SubmitDataWhitelist::::insert(&account, ()); + } else { + SubmitDataWhitelist::::remove(&account); + } + + Self::deposit_event(Event::SubmitDataWhitelistUpdated { account, allowed }); + Ok(().into()) + } } /// Event for the pallet. @@ -327,6 +355,10 @@ pub mod pallet { SubmitDataFeeModifierSet { value: DispatchFeeModifier, }, + SubmitDataWhitelistUpdated { + account: T::AccountId, + allowed: bool, + }, } /// Error for the System pallet @@ -354,6 +386,8 @@ pub mod pallet { UnknownAppKey, /// Submit block length proposal was made with values not power of 2 NotPowerOfTwo, + /// Submit data signer is not whitelisted after the hard cutover. + SubmitDataSignerNotWhitelisted, } #[pallet::genesis_config] @@ -391,6 +425,10 @@ pub mod pallet { } impl Pallet { + pub fn is_submit_data_whitelisted(who: &T::AccountId) -> bool { + SubmitDataWhitelist::::contains_key(who) + } + /// Returns the latest available application ID and increases it. pub fn next_application_id() -> Result> { NextAppId::::try_mutate(|id| { diff --git a/pallets/dactr/src/tests.rs b/pallets/dactr/src/tests.rs index 3a2df2c7e..fc4f41193 100644 --- a/pallets/dactr/src/tests.rs +++ b/pallets/dactr/src/tests.rs @@ -8,7 +8,7 @@ use crate::config_preludes::{ }; use crate::{ mock::{new_test_ext, DataAvailability, RuntimeEvent, RuntimeOrigin, System, Test}, - AppDataFor, AppKeyFor, AppKeyInfoFor, Event, DA_DISPATCH_RATIO_PERBILL, + AppDataFor, AppKeyFor, AppKeyInfoFor, Event, SubmitDataWhitelist, DA_DISPATCH_RATIO_PERBILL, }; type Error = crate::Error; @@ -81,6 +81,7 @@ mod submit_data { #[test] fn submit_data() { new_test_ext().execute_with(|| { + SubmitDataWhitelist::::insert(ALICE, ()); let alice: RuntimeOrigin = RawOrigin::Signed(ALICE).into(); let max_app_key_length: usize = MaxAppDataLength::get().try_into().unwrap(); let data = AppDataFor::::try_from(vec![b'X'; max_app_key_length]).unwrap(); @@ -99,6 +100,7 @@ mod submit_data { #[test] fn data_cannot_be_empty() { new_test_ext().execute_with(|| { + SubmitDataWhitelist::::insert(ALICE, ()); let alice: RuntimeOrigin = RawOrigin::Signed(ALICE).into(); let data = AppDataFor::::try_from(vec![]).unwrap(); @@ -107,6 +109,17 @@ mod submit_data { }) } + #[test] + fn signer_must_be_whitelisted() { + new_test_ext().execute_with(|| { + let alice: RuntimeOrigin = RawOrigin::Signed(ALICE).into(); + let data = AppDataFor::::try_from(vec![b'X']).unwrap(); + + let err = DataAvailability::submit_data(alice, data); + assert_noop!(err, Error::SubmitDataSignerNotWhitelisted); + }) + } + #[test] fn submit_data_too_long() { new_test_ext().execute_with(|| { diff --git a/runtime/src/impls.rs b/runtime/src/impls.rs index 8c85aa7d2..b05c763b8 100644 --- a/runtime/src/impls.rs +++ b/runtime/src/impls.rs @@ -1,11 +1,11 @@ use crate::{ constants, prod_or_fast, voter_bags, weights, AccountId, AccountIndex, Babe, Balances, Block, - BlockNumber, ElectionProviderMultiPhase, Everything, Hash, Header, Historical, ImOnline, - ImOnlineId, Index, Indices, Moment, NominationPools, Offences, OriginCaller, PalletInfo, - Preimage, ReserveIdentifier, Runtime, RuntimeCall, RuntimeEvent, RuntimeFreezeReason, - RuntimeHoldReason, RuntimeOrigin, RuntimeVersion, Session, SessionKeys, Signature, - SignedPayload, Staking, System, Timestamp, TransactionPayment, Treasury, TxPause, - UncheckedExtrinsic, VoterList, MINUTES, SLOT_DURATION, VERSION, + BlockNumber, ElectionProviderMultiPhase, Hash, Header, Historical, ImOnline, ImOnlineId, Index, + Indices, Moment, NominationPools, Offences, OriginCaller, PalletInfo, Preimage, + ReserveIdentifier, Runtime, RuntimeCall, RuntimeEvent, RuntimeFreezeReason, RuntimeHoldReason, + RuntimeOrigin, RuntimeVersion, Session, SessionKeys, Signature, SignedPayload, Staking, System, + Timestamp, TransactionPayment, Treasury, TxPause, UncheckedExtrinsic, VoterList, MINUTES, + SLOT_DURATION, VERSION, }; use avail_core::{ currency::{Balance, AVAIL, CENTS, NANO_AVAIL, PICO_AVAIL}, @@ -22,10 +22,8 @@ use frame_support::{ pallet_prelude::{Get, Weight}, parameter_types, traits::{ - fungible::{Balanced, Credit, HoldConsideration}, - tokens::{ - imbalance::ResolveTo, pay::PayFromAccount, Imbalance, UnityAssetBalanceConversion, - }, + fungible::{Credit, HoldConsideration}, + tokens::{pay::PayFromAccount, Imbalance, UnityAssetBalanceConversion}, ConstU32, Contains, Currency, EitherOf, EitherOfDiverse, EqualPrivilegeOnly, InsideBoth, InstanceFilter, LinearStoragePrice, OnUnbalanced, }, @@ -35,8 +33,9 @@ use frame_support::{ use frame_system::{limits::BlockLength, EnsureRoot, EnsureRootWithSuccess, EnsureWithSuccess}; use pallet_election_provider_multi_phase::{GeometricDepositBase, SolutionAccuracyOf}; use pallet_identity::legacy::IdentityInfo; -use pallet_transaction_payment::{FungibleAdapter, Multiplier, TargetedFeeAdjustment}; -use pallet_treasury::TreasuryAccountId; +use pallet_transaction_payment::{ + FungibleAdapter, Multiplier, OnChargeTransaction, TargetedFeeAdjustment, +}; use pallet_tx_pause::RuntimeCallNameOf; use sp_core::{ConstU64, RuntimeDebug}; use sp_runtime::{ @@ -160,6 +159,42 @@ parameter_types! { pub MaximumMultiplier: Multiplier = Bounded::max_value(); } +pub struct FeeLessTransaction; + +impl OnChargeTransaction for FeeLessTransaction { + type Balance = Balance; + type LiquidityInfo = + > as OnChargeTransaction>::LiquidityInfo; + + fn withdraw_fee( + _who: &AccountId, + _call: &RuntimeCall, + _dispatch_info: &traits::DispatchInfoOf, + _fee: Self::Balance, + _tip: Self::Balance, + ) -> Result { + Ok(Default::default()) + } + + fn correct_and_deposit_fee( + who: &AccountId, + dispatch_info: &traits::DispatchInfoOf, + post_info: &traits::PostDispatchInfoOf, + corrected_fee: Self::Balance, + tip: Self::Balance, + already_withdrawn: Self::LiquidityInfo, + ) -> Result<(), sp_runtime::transaction_validity::TransactionValidityError> { + > as OnChargeTransaction>::correct_and_deposit_fee( + who, + dispatch_info, + post_info, + corrected_fee, + tip, + already_withdrawn, + ) + } +} + impl pallet_transaction_payment::Config for Runtime { type FeeMultiplierUpdate = TargetedFeeAdjustment< Self, @@ -169,7 +204,7 @@ impl pallet_transaction_payment::Config for Runtime { MaximumMultiplier, >; type LengthToFee = ConstantMultiplier; - type OnChargeTransaction = FungibleAdapter>; + type OnChargeTransaction = FeeLessTransaction; type OperationalFeeMultiplier = OperationalFeeMultiplier; type RuntimeEvent = RuntimeEvent; type WeightToFee = ConstantMultiplier; // 1 weight = 10 picoAVAIL -> second_price = 10 AVAIL @@ -209,42 +244,19 @@ impl pallet_session::historical::Config for Runtime { type FullIdentificationOf = pallet_staking::ExposureOf; } -/// Logic for the author to get a portion of fees. -pub struct Author(sp_std::marker::PhantomData); -impl OnUnbalanced>> for Author -where - R: pallet_balances::Config + pallet_authorship::Config, - ::AccountId: From, - ::AccountId: Into, -{ - fn on_nonzero_unbalanced( - amount: Credit<::AccountId, pallet_balances::Pallet>, - ) { - if let Some(author) = >::author() { - let _ = >::resolve(&author, amount); - } - } -} - pub struct DealWithFees(core::marker::PhantomData); impl OnUnbalanced>> for DealWithFees where - R: pallet_balances::Config + pallet_authorship::Config + pallet_treasury::Config, - ::AccountId: From, - ::AccountId: Into, + R: pallet_balances::Config, { fn on_unbalanceds( mut fees_then_tips: impl Iterator>>, ) { - if let Some(fees) = fees_then_tips.next() { - // for fees, 20% to author, 80% to treasury - let mut split = fees.ration(80, 20); + if let Some(mut fees) = fees_then_tips.next() { if let Some(tips) = fees_then_tips.next() { - // for tips, if any, 100% to author - tips.merge_into(&mut split.1); + tips.merge_into(&mut fees); } - ResolveTo::, pallet_balances::Pallet>::on_unbalanced(split.0); - as OnUnbalanced<_>>::on_unbalanced(split.1); + drop(fees); } } } @@ -493,7 +505,7 @@ impl pallet_staking::Config for Runtime { type CurrencyBalance = Balance; type CurrencyToVote = sp_staking::currency_to_vote::U128CurrencyToVote; type ElectionProvider = ElectionProviderMultiPhase; - type EraPayout = pallet_staking::ConvertCurve; + type EraPayout = (); type EventListeners = NominationPools; type GenesisElectionProvider = onchain::OnChainExecution; type HistoryDepth = constants::staking::HistoryDepth; @@ -800,6 +812,44 @@ impl pallet_tx_pause::Config for Runtime { type WeightInfo = weights::pallet_tx_pause::WeightInfo; } +/// Hard-cutover runtime filter. +/// +/// This keeps consensus/governance paths live, allows direct DA submissions +/// for the DA pallet for whitelisted accounts, and blocks the ordinary economic/user-facing +/// calls at the runtime boundary. +pub struct HardCutoverCallFilter; + +impl Contains for HardCutoverCallFilter { + fn contains(call: &RuntimeCall) -> bool { + match call { + RuntimeCall::Timestamp(..) => true, + + RuntimeCall::TechnicalCommittee(..) + | RuntimeCall::TreasuryCommittee(..) + | RuntimeCall::Treasury(..) + | RuntimeCall::Sudo(..) + | RuntimeCall::Scheduler(..) + | RuntimeCall::Mandate(..) + | RuntimeCall::TxPause(..) => true, + + RuntimeCall::System(frame_system::Call::set_code { .. }) + | RuntimeCall::System(frame_system::Call::set_code_without_checks { .. }) + | RuntimeCall::System(frame_system::Call::set_heap_pages { .. }) => true, + + RuntimeCall::DataAvailability(da_control::Call::submit_data { .. }) => true, + RuntimeCall::DataAvailability(da_control::Call::set_submit_data_whitelist { + .. + }) => true, + RuntimeCall::Vector(pallet_vector::Call::failed_send_message_txs { .. }) => true, + + RuntimeCall::Proxy(pallet_proxy::Call::proxy { call, .. }) => Self::contains(call), + RuntimeCall::Proxy(..) => false, + + _ => false, + } + } +} + parameter_types! { pub const BlockHashCount: BlockNumber = 2400; pub const Version: RuntimeVersion = VERSION; @@ -816,7 +866,7 @@ impl frame_system::Config for Runtime { /// The identifier used to distinguish between accounts. type AccountId = AccountId; /// The basic call filter to use in dispatchable. - type BaseCallFilter = InsideBoth; + type BaseCallFilter = InsideBoth; /// The Block type used by the runtime type Block = Block; /// Maximum number of block number to block hash mappings to keep (oldest pruned first). diff --git a/runtime/src/version.rs b/runtime/src/version.rs index a0e04f7f9..63745b8d3 100644 --- a/runtime/src/version.rs +++ b/runtime/src/version.rs @@ -17,7 +17,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { // Per convention: if the runtime behavior changes, increment spec_version // and set impl_version to 0. This paramenter is typically incremented when // there's an update to the transaction_version. - spec_version: 51, + spec_version: 52, // The version of the implementation of the specification. Nodes can ignore this. It is only // used to indicate that the code is different. As long as the authoring_version and the // spec_version are the same, the code itself might have changed, but the native and Wasm