From f6217c27494229f4adcd227283069de5505843f8 Mon Sep 17 00:00:00 2001 From: Toufeeq Pasha Date: Mon, 18 May 2026 13:47:18 +0530 Subject: [PATCH 1/4] Implement submit data whitelisting and enforce checks in DA transactions --- pallets/dactr/src/extensions/check_app_id.rs | 22 ++- .../extensions/check_batch_transactions.rs | 12 +- pallets/dactr/src/lib.rs | 38 +++++ pallets/dactr/src/tests.rs | 15 +- runtime/src/impls.rs | 150 +++++++++++++----- 5 files changed, 189 insertions(+), 48 deletions(-) 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..eeef77357 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,13 +33,14 @@ 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::{ generic::Era, - traits::{self, BlakeTwo256, Bounded, Convert, IdentityLookup, OpaqueKeys}, + traits::{self, BlakeTwo256, Bounded, Convert, IdentityLookup, OpaqueKeys, Zero}, FixedPointNumber, FixedU128, Perbill, Permill, Perquintill, }; @@ -160,6 +159,57 @@ parameter_types! { pub MaximumMultiplier: Multiplier = Bounded::max_value(); } +pub struct RtuOnChargeTransaction; + +impl OnChargeTransaction for RtuOnChargeTransaction { + 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 { + if tip.is_zero() + && matches!( + call, + RuntimeCall::DataAvailability(da_control::Call::submit_data { .. }) + ) && da_control::Pallet::::is_submit_data_whitelisted(who) + { + return Ok(Default::default()); + } + + > as OnChargeTransaction>::withdraw_fee( + who, + call, + dispatch_info, + fee, + tip, + ) + } + + 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 +219,7 @@ impl pallet_transaction_payment::Config for Runtime { MaximumMultiplier, >; type LengthToFee = ConstantMultiplier; - type OnChargeTransaction = FungibleAdapter>; + type OnChargeTransaction = RtuOnChargeTransaction; type OperationalFeeMultiplier = OperationalFeeMultiplier; type RuntimeEvent = RuntimeEvent; type WeightToFee = ConstantMultiplier; // 1 weight = 10 picoAVAIL -> second_price = 10 AVAIL @@ -209,42 +259,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 +520,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 +827,43 @@ impl pallet_tx_pause::Config for Runtime { type WeightInfo = weights::pallet_tx_pause::WeightInfo; } +/// Hard-cutover runtime filter. +/// +/// This keeps consensus/admin/governance paths live, allows direct DA submissions +/// for the DA pallet whitelist, and blocks the ordinary economic/user-facing call +/// surface at the runtime boundary. +pub struct RtuHardCutoverCallFilter; + +impl Contains for RtuHardCutoverCallFilter { + 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::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 +880,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). From 830b92ed142d72f17d9551aeda5e3d7ad1f3bb38 Mon Sep 17 00:00:00 2001 From: Toufeeq Pasha Date: Mon, 18 May 2026 13:48:12 +0530 Subject: [PATCH 2/4] bump runtime version --- runtime/src/version.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 667311f22b92a4d86819a8f1be6be08311ad6b6e Mon Sep 17 00:00:00 2001 From: Toufeeq Pasha Date: Tue, 19 May 2026 15:03:43 +0530 Subject: [PATCH 3/4] Remove fees & whitelist post-inherent call --- runtime/src/impls.rs | 30 ++++++++---------------------- 1 file changed, 8 insertions(+), 22 deletions(-) diff --git a/runtime/src/impls.rs b/runtime/src/impls.rs index eeef77357..c8668b9cd 100644 --- a/runtime/src/impls.rs +++ b/runtime/src/impls.rs @@ -40,7 +40,7 @@ use pallet_tx_pause::RuntimeCallNameOf; use sp_core::{ConstU64, RuntimeDebug}; use sp_runtime::{ generic::Era, - traits::{self, BlakeTwo256, Bounded, Convert, IdentityLookup, OpaqueKeys, Zero}, + traits::{self, BlakeTwo256, Bounded, Convert, IdentityLookup, OpaqueKeys}, FixedPointNumber, FixedU128, Perbill, Permill, Perquintill, }; @@ -167,28 +167,13 @@ impl OnChargeTransaction for RtuOnChargeTransaction { > as OnChargeTransaction>::LiquidityInfo; fn withdraw_fee( - who: &AccountId, - call: &RuntimeCall, - dispatch_info: &traits::DispatchInfoOf, - fee: Self::Balance, - tip: Self::Balance, + _who: &AccountId, + _call: &RuntimeCall, + _dispatch_info: &traits::DispatchInfoOf, + _fee: Self::Balance, + _tip: Self::Balance, ) -> Result { - if tip.is_zero() - && matches!( - call, - RuntimeCall::DataAvailability(da_control::Call::submit_data { .. }) - ) && da_control::Pallet::::is_submit_data_whitelisted(who) - { - return Ok(Default::default()); - } - - > as OnChargeTransaction>::withdraw_fee( - who, - call, - dispatch_info, - fee, - tip, - ) + Ok(Default::default()) } fn correct_and_deposit_fee( @@ -855,6 +840,7 @@ impl Contains for RtuHardCutoverCallFilter { 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, From e096a37871ec87c73f1d61302d8d9b2c7dfcae17 Mon Sep 17 00:00:00 2001 From: Toufeeq Pasha Date: Tue, 19 May 2026 15:32:37 +0530 Subject: [PATCH 4/4] refactor --- runtime/src/impls.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/runtime/src/impls.rs b/runtime/src/impls.rs index c8668b9cd..b05c763b8 100644 --- a/runtime/src/impls.rs +++ b/runtime/src/impls.rs @@ -159,9 +159,9 @@ parameter_types! { pub MaximumMultiplier: Multiplier = Bounded::max_value(); } -pub struct RtuOnChargeTransaction; +pub struct FeeLessTransaction; -impl OnChargeTransaction for RtuOnChargeTransaction { +impl OnChargeTransaction for FeeLessTransaction { type Balance = Balance; type LiquidityInfo = > as OnChargeTransaction>::LiquidityInfo; @@ -204,7 +204,7 @@ impl pallet_transaction_payment::Config for Runtime { MaximumMultiplier, >; type LengthToFee = ConstantMultiplier; - type OnChargeTransaction = RtuOnChargeTransaction; + type OnChargeTransaction = FeeLessTransaction; type OperationalFeeMultiplier = OperationalFeeMultiplier; type RuntimeEvent = RuntimeEvent; type WeightToFee = ConstantMultiplier; // 1 weight = 10 picoAVAIL -> second_price = 10 AVAIL @@ -814,12 +814,12 @@ impl pallet_tx_pause::Config for Runtime { /// Hard-cutover runtime filter. /// -/// This keeps consensus/admin/governance paths live, allows direct DA submissions -/// for the DA pallet whitelist, and blocks the ordinary economic/user-facing call -/// surface at the runtime boundary. -pub struct RtuHardCutoverCallFilter; +/// 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 RtuHardCutoverCallFilter { +impl Contains for HardCutoverCallFilter { fn contains(call: &RuntimeCall) -> bool { match call { RuntimeCall::Timestamp(..) => true, @@ -866,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).