diff --git a/Cargo.lock b/Cargo.lock index fc47d862f1..83115ddb40 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3707,6 +3707,7 @@ dependencies = [ "pallet-frequency-tx-payment", "pallet-handles", "pallet-msa", + "pallet-stateful-storage", "pallet-transaction-payment", "polkadot-cli", "polkadot-parachain-primitives", diff --git a/e2e/package-lock.json b/e2e/package-lock.json index 841fe0cb73..c138c0fc72 100644 --- a/e2e/package-lock.json +++ b/e2e/package-lock.json @@ -2839,11 +2839,12 @@ "node_modules/@frequency-chain/api-augment": { "version": "0.0.0", "resolved": "file:../js/api-augment/dist/frequency-chain-api-augment-0.0.0.tgz", - "integrity": "sha512-7prkQiSHJ0aCa9vqR6g5HwVZ55rstu94m1FjQvJQO7MIt8BgSQYAsP0N1pc2CmtN0k72wYeGRV3+nLnWkfKG1w==", + "integrity": "sha512-E39EoMsjnD17kIyA7U/ib24vxvyQmeWGNaKyWblacWyENcUhUGMFtPlIFK2xgyIPXvSsDDwLwYuue35/pX23Hg==", + "license": "Apache-2.0", "dependencies": { - "@polkadot/api": "^12.2.3", - "@polkadot/rpc-provider": "^12.2.3", - "@polkadot/types": "^12.2.3", + "@polkadot/api": "^12.3.1", + "@polkadot/rpc-provider": "^12.3.1", + "@polkadot/types": "^12.3.1", "globals": "^15.9.0" } }, diff --git a/e2e/stateful-pallet-storage/handleItemized.test.ts b/e2e/stateful-pallet-storage/handleItemized.test.ts index 9df72132da..e5a1669123 100644 --- a/e2e/stateful-pallet-storage/handleItemized.test.ts +++ b/e2e/stateful-pallet-storage/handleItemized.test.ts @@ -180,7 +180,10 @@ describe('📗 Stateful Pallet Storage', function () { add_actions, 0 ); - await assert.rejects(itemized_add_result_1.fundAndSend(fundingSource), { name: 'StalePageState' }); + await assert.rejects(itemized_add_result_1.signAndSend('current'), { + name: 'RpcError', + message: /1010: Invalid Transaction: Custom error: 9/, + }); }); }); @@ -285,7 +288,10 @@ describe('📗 Stateful Pallet Storage', function () { }; const remove_actions = [remove_action]; const op = ExtrinsicHelper.applyItemActions(providerKeys, schemaId_deletable, msa_id, remove_actions, 0); - await assert.rejects(op.fundAndSend(fundingSource), { name: 'StalePageState' }); + await assert.rejects(op.signAndSend('current'), { + name: 'RpcError', + message: /1010: Invalid Transaction: Custom error: 9/, + }); }); }); diff --git a/e2e/stateful-pallet-storage/handlePaginated.test.ts b/e2e/stateful-pallet-storage/handlePaginated.test.ts index 83a4cdebbc..25584d8cb7 100644 --- a/e2e/stateful-pallet-storage/handlePaginated.test.ts +++ b/e2e/stateful-pallet-storage/handlePaginated.test.ts @@ -201,9 +201,9 @@ describe('📗 Stateful Pallet Storage', function () { const payload_1 = new Bytes(ExtrinsicHelper.api.registry, 'Hello World From Frequency'); const paginated_add_result_1 = ExtrinsicHelper.upsertPage(providerKeys, schemaId, msa_id, page_id, payload_1, 0); - await assert.rejects(paginated_add_result_1.fundAndSend(fundingSource), { - name: 'StalePageState', - section: 'statefulStorage', + await assert.rejects(paginated_add_result_1.signAndSend('current'), { + name: 'RpcError', + message: /1010: Invalid Transaction: Custom error: 9/, }); }); }); @@ -242,10 +242,10 @@ describe('📗 Stateful Pallet Storage', function () { }); it('🛑 should fail call to remove page with stale target hash', async function () { - const paginated_add_result_1 = ExtrinsicHelper.removePage(providerKeys, schemaId, msa_id, 0, 0); - await assert.rejects(paginated_add_result_1.fundAndSend(fundingSource), { - name: 'StalePageState', - section: 'statefulStorage', + const paginated_add_result_1 = ExtrinsicHelper.removePage(providerKeys, schemaId, msa_id, 0, 1000); + await assert.rejects(paginated_add_result_1.signAndSend('current'), { + name: 'RpcError', + message: /1010: Invalid Transaction: Custom error: 9/, }); }); }); diff --git a/node/cli/Cargo.toml b/node/cli/Cargo.toml index b1b1634832..56a81da1f3 100644 --- a/node/cli/Cargo.toml +++ b/node/cli/Cargo.toml @@ -24,6 +24,7 @@ frequency-service = { package = "frequency-service", path = "../service", defaul pallet-msa = { package = "pallet-msa", path = "../../pallets/msa", default-features = false } pallet-frequency-tx-payment = { package = "pallet-frequency-tx-payment", path = "../../pallets/frequency-tx-payment", default-features = false } pallet-handles = { package = "pallet-handles", path = "../../pallets/handles", default-features = false } +pallet-stateful-storage = { package = "pallet-stateful-storage", path = "../../pallets/stateful-storage", default-features = false } cli-opt = { default-features = false, path = "../cli-opt" } # Substrate diff --git a/node/cli/src/benchmarking.rs b/node/cli/src/benchmarking.rs index d69b0dfb88..7b6a2dd205 100644 --- a/node/cli/src/benchmarking.rs +++ b/node/cli/src/benchmarking.rs @@ -13,6 +13,7 @@ use sp_core::{Encode, Pair}; use sp_keyring::Sr25519Keyring; use sp_runtime::{OpaqueExtrinsic, SaturatedConversion}; +use frequency_runtime::StaleHashCheckExtension; use pallet_balances::Call as BalancesCall; use pallet_msa; use sp_inherents::InherentDataProvider; @@ -118,8 +119,10 @@ pub fn create_benchmark_extrinsic( .unwrap_or(2) as u64; let extra: runtime::SignedExtra = ( frame_system::CheckNonZeroSender::::new(), - frame_system::CheckSpecVersion::::new(), - frame_system::CheckTxVersion::::new(), + ( + frame_system::CheckSpecVersion::::new(), + frame_system::CheckTxVersion::::new(), + ), frame_system::CheckGenesis::::new(), frame_system::CheckEra::::from(sp_runtime::generic::Era::mortal( period, @@ -130,6 +133,7 @@ pub fn create_benchmark_extrinsic( pallet_frequency_tx_payment::ChargeFrqTransactionPayment::::from(0), pallet_msa::CheckFreeExtrinsicUse::::new(), pallet_handles::handles_signed_extension::HandlesSignedExtension::::new(), + StaleHashCheckExtension, frame_metadata_hash_extension::CheckMetadataHash::::new(false), cumulus_primitives_storage_weight_reclaim::StorageWeightReclaim::::new(), ); @@ -139,8 +143,7 @@ pub fn create_benchmark_extrinsic( extra.clone(), ( (), - runtime::VERSION.spec_version, - runtime::VERSION.transaction_version, + (runtime::VERSION.spec_version, runtime::VERSION.transaction_version), genesis_hash, best_hash, (), @@ -148,6 +151,7 @@ pub fn create_benchmark_extrinsic( (), (), (), + (), None, (), ), diff --git a/pallets/stateful-storage/src/lib.rs b/pallets/stateful-storage/src/lib.rs index 488a9fc253..660c46d567 100644 --- a/pallets/stateful-storage/src/lib.rs +++ b/pallets/stateful-storage/src/lib.rs @@ -39,7 +39,6 @@ use sp_std::prelude::*; mod stateful_child_tree; pub mod types; - pub mod weights; use crate::{stateful_child_tree::StatefulChildTree, types::*}; @@ -939,7 +938,7 @@ impl Pallet { } /// Gets a paginated storage for desired parameters - fn get_paginated_page_for( + pub fn get_paginated_page_for( msa_id: MessageSourceId, schema_id: SchemaId, page_id: PageId, @@ -955,7 +954,7 @@ impl Pallet { } /// Gets an itemized storage for desired parameters - fn get_itemized_page_for( + pub fn get_itemized_page_for( msa_id: MessageSourceId, schema_id: SchemaId, ) -> Result>, DispatchError> { diff --git a/runtime/common/src/weights/block_weights.rs b/runtime/common/src/weights/block_weights.rs index eaa3e8190c..02302f986e 100644 --- a/runtime/common/src/weights/block_weights.rs +++ b/runtime/common/src/weights/block_weights.rs @@ -16,8 +16,8 @@ // limitations under the License. //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 32.0.0 -//! DATE: 2024-08-15 (Y/M/D) -//! HOSTNAME: `ip-10-173-11-47`, CPU: `Intel(R) Xeon(R) Platinum 8375C CPU @ 2.90GHz` +//! DATE: 2024-08-21 (Y/M/D) +//! HOSTNAME: `ip-10-173-4-165`, CPU: `Intel(R) Xeon(R) Platinum 8375C CPU @ 2.90GHz` //! //! SHORT-NAME: `block`, LONG-NAME: `BlockExecution`, RUNTIME: `Frequency Development (No Relay)` //! WARMUPS: `10`, REPEAT: `100` @@ -43,17 +43,17 @@ parameter_types! { /// Calculated by multiplying the *Average* with `1.0` and adding `0`. /// /// Stats nanoseconds: - /// Min, Max: 337_630, 378_812 - /// Average: 345_638 - /// Median: 342_194 - /// Std-Dev: 8479.5 + /// Min, Max: 333_594, 560_445 + /// Average: 345_593 + /// Median: 339_315 + /// Std-Dev: 24323.47 /// /// Percentiles nanoseconds: - /// 99th: 372_459 - /// 95th: 362_361 - /// 75th: 346_630 + /// 99th: 398_789 + /// 95th: 364_915 + /// 75th: 346_466 pub const BlockExecutionWeight: Weight = - Weight::from_parts(WEIGHT_REF_TIME_PER_NANOS.saturating_mul(345_638), 0); + Weight::from_parts(WEIGHT_REF_TIME_PER_NANOS.saturating_mul(345_593), 0); } #[cfg(test)] diff --git a/runtime/common/src/weights/extrinsic_weights.rs b/runtime/common/src/weights/extrinsic_weights.rs index bd0f5ef722..5abd4ade13 100644 --- a/runtime/common/src/weights/extrinsic_weights.rs +++ b/runtime/common/src/weights/extrinsic_weights.rs @@ -16,8 +16,8 @@ // limitations under the License. //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 32.0.0 -//! DATE: 2024-08-15 (Y/M/D) -//! HOSTNAME: `ip-10-173-11-47`, CPU: `Intel(R) Xeon(R) Platinum 8375C CPU @ 2.90GHz` +//! DATE: 2024-08-21 (Y/M/D) +//! HOSTNAME: `ip-10-173-4-165`, CPU: `Intel(R) Xeon(R) Platinum 8375C CPU @ 2.90GHz` //! //! SHORT-NAME: `extrinsic`, LONG-NAME: `ExtrinsicBase`, RUNTIME: `Frequency Development (No Relay)` //! WARMUPS: `10`, REPEAT: `100` @@ -43,17 +43,17 @@ parameter_types! { /// Calculated by multiplying the *Average* with `1.0` and adding `0`. /// /// Stats nanoseconds: - /// Min, Max: 92_351, 94_986 - /// Average: 93_521 - /// Median: 93_543 - /// Std-Dev: 592.1 + /// Min, Max: 90_815, 92_870 + /// Average: 91_811 + /// Median: 91_829 + /// Std-Dev: 403.29 /// /// Percentiles nanoseconds: - /// 99th: 94_946 - /// 95th: 94_443 - /// 75th: 93_970 + /// 99th: 92_749 + /// 95th: 92_366 + /// 75th: 92_088 pub const ExtrinsicBaseWeight: Weight = - Weight::from_parts(WEIGHT_REF_TIME_PER_NANOS.saturating_mul(93_521), 0); + Weight::from_parts(WEIGHT_REF_TIME_PER_NANOS.saturating_mul(91_811), 0); } #[cfg(test)] diff --git a/runtime/frequency/src/lib.rs b/runtime/frequency/src/lib.rs index b45e48b993..31ee18bfa7 100644 --- a/runtime/frequency/src/lib.rs +++ b/runtime/frequency/src/lib.rs @@ -31,7 +31,7 @@ use pallet_collective::Members; #[cfg(any(feature = "runtime-benchmarks", feature = "test"))] use pallet_collective::ProposalCount; -use parity_scale_codec::Encode; +use parity_scale_codec::{Decode, Encode}; use sp_std::prelude::*; #[cfg(feature = "std")] @@ -52,7 +52,8 @@ pub use common_runtime::{ use frame_support::{ construct_runtime, dispatch::{DispatchClass, GetDispatchInfo, Pays}, - pallet_prelude::DispatchResultWithPostInfo, + ensure, + pallet_prelude::{DispatchResultWithPostInfo, TypeInfo}, parameter_types, traits::{ fungible::HoldConsideration, @@ -94,9 +95,20 @@ pub use common_runtime::{ }; use frame_support::traits::Contains; +use common_primitives::{ + msa::MessageSourceId, + schema::SchemaId, + stateful_storage::{PageHash, PageId}, +}; use common_runtime::weights::rocksdb_weights::constants::RocksDbWeight; #[cfg(feature = "try-runtime")] use frame_support::traits::{TryStateSelect, UpgradeCheckSelect}; +use sp_runtime::{ + traits::{DispatchInfoOf, SignedExtension}, + transaction_validity::{ + InvalidTransaction, TransactionValidity, TransactionValidityError, ValidTransaction, + }, +}; /// Interface to collective pallet to propose a proposal. pub struct CouncilProposalProvider; @@ -289,8 +301,8 @@ impl Contains for PasskeyCallFilter { /// The SignedExtension to the basic transaction logic. pub type SignedExtra = ( frame_system::CheckNonZeroSender, - frame_system::CheckSpecVersion, - frame_system::CheckTxVersion, + // merging these types so that we can have more than 12 extensions + (frame_system::CheckSpecVersion, frame_system::CheckTxVersion), frame_system::CheckGenesis, frame_system::CheckEra, common_runtime::extensions::check_nonce::CheckNonce, @@ -298,6 +310,7 @@ pub type SignedExtra = ( pallet_frequency_tx_payment::ChargeFrqTransactionPayment, pallet_msa::CheckFreeExtrinsicUse, pallet_handles::handles_signed_extension::HandlesSignedExtension, + StaleHashCheckExtension, frame_metadata_hash_extension::CheckMetadataHash, cumulus_primitives_storage_weight_reclaim::StorageWeightReclaim, ); @@ -361,7 +374,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { spec_name: create_runtime_str!("frequency"), impl_name: create_runtime_str!("frequency"), authoring_version: 1, - spec_version: 108, + spec_version: 109, impl_version: 0, apis: apis::RUNTIME_API_VERSIONS, transaction_version: 1, @@ -375,7 +388,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { spec_name: create_runtime_str!("frequency-testnet"), impl_name: create_runtime_str!("frequency"), authoring_version: 1, - spec_version: 108, + spec_version: 109, impl_version: 0, apis: apis::RUNTIME_API_VERSIONS, transaction_version: 1, @@ -882,6 +895,7 @@ use pallet_handles::Call as HandlesCall; use pallet_messages::Call as MessagesCall; use pallet_msa::Call as MsaCall; use pallet_stateful_storage::Call as StatefulStorageCall; +use pallet_utility::Call as UtilityCall; pub struct CapacityEligibleCalls; impl GetStableWeight for CapacityEligibleCalls { @@ -1235,6 +1249,279 @@ construct_runtime!( } ); +/// The SignedExtension trait is implemented on StaleHashCheckExtension to validate the +/// request. The purpose of this is to ensure that the target_hash is verified in transaction pool +/// before getting into block. This is to reduce the chance of capacity consumption due to stale hash +#[derive(Encode, Decode, Clone, Eq, PartialEq, TypeInfo)] +pub struct StaleHashCheckExtension; + +/// Extracted data type from transactions to verify target hash +struct HashCheckData { + message_source_id: MessageSourceId, + schema_id: SchemaId, + page: Option, + hash: PageHash, +} + +impl HashCheckData { + fn new_itemized( + message_source_id: MessageSourceId, + schema_id: SchemaId, + hash: PageHash, + ) -> Self { + Self { message_source_id, schema_id, page: None, hash } + } + + fn new_paginated( + message_source_id: MessageSourceId, + schema_id: SchemaId, + page_id: PageId, + hash: PageHash, + ) -> Self { + Self { message_source_id, schema_id, page: Some(page_id), hash } + } +} + +impl sp_std::fmt::Debug for StaleHashCheckExtension { + #[cfg(feature = "std")] + fn fmt(&self, f: &mut sp_std::fmt::Formatter) -> sp_std::fmt::Result { + write!(f, "StaleHashCheckExtension") + } + #[cfg(not(feature = "std"))] + fn fmt(&self, _: &mut sp_std::fmt::Formatter) -> sp_std::fmt::Result { + Ok(()) + } +} + +impl SignedExtension for StaleHashCheckExtension { + const IDENTIFIER: &'static str = "StaleHashCheckExtension"; + type AccountId = AccountId; + type Call = RuntimeCall; + type AdditionalSigned = (); + type Pre = (); + + fn additional_signed(&self) -> Result { + Ok(()) + } + + fn validate( + &self, + _who: &Self::AccountId, + call: &Self::Call, + _info: &DispatchInfoOf, + _len: usize, + ) -> TransactionValidity { + let mut valid_tx = ValidTransaction::default(); + for trx in Self::extract_hash_data(call) { + match trx.page { + Some(page_id) => { + let r = Self::verify_hash_paginated( + &trx.message_source_id, + &trx.schema_id, + &page_id, + &trx.hash, + ); + valid_tx = valid_tx.combine_with(r?); + }, + None => { + let r = Self::verify_hash_itemized( + &trx.message_source_id, + &trx.schema_id, + &trx.hash, + ); + valid_tx = valid_tx.combine_with(r?); + }, + } + } + Ok(valid_tx) + } + + /// Pre dispatch hook. Called before extriniscs execution in the block. + fn pre_dispatch( + self, + _who: &Self::AccountId, + _call: &Self::Call, + _info: &DispatchInfoOf, + _len: usize, + ) -> Result { + // Since we already check the hash in stateful-storage-pallet extrinsics we do not need to + // check the hash before dispatching + Ok(()) + } +} +impl StaleHashCheckExtension { + /// extracts the relevant data to check the hash + fn extract_hash_data(call: &RuntimeCall) -> Vec { + match call { + RuntimeCall::StatefulStorage(StatefulStorageCall::apply_item_actions { + state_owner_msa_id, + schema_id, + target_hash, + .. + }) => vec![HashCheckData::new_itemized(*state_owner_msa_id, *schema_id, *target_hash)], + RuntimeCall::StatefulStorage(StatefulStorageCall::upsert_page { + state_owner_msa_id, + schema_id, + target_hash, + page_id, + .. + }) | + RuntimeCall::StatefulStorage(StatefulStorageCall::delete_page { + state_owner_msa_id, + schema_id, + target_hash, + page_id, + .. + }) => vec![HashCheckData::new_paginated( + *state_owner_msa_id, + *schema_id, + *page_id, + *target_hash, + )], + RuntimeCall::StatefulStorage( + StatefulStorageCall::apply_item_actions_with_signature { payload, .. }, + ) => vec![HashCheckData::new_itemized( + payload.msa_id, + payload.schema_id, + payload.target_hash, + )], + RuntimeCall::StatefulStorage(StatefulStorageCall::upsert_page_with_signature { + payload, + .. + }) => vec![HashCheckData::new_paginated( + payload.msa_id, + payload.schema_id, + payload.page_id, + payload.target_hash, + )], + RuntimeCall::StatefulStorage(StatefulStorageCall::delete_page_with_signature { + payload, + .. + }) => vec![HashCheckData::new_paginated( + payload.msa_id, + payload.schema_id, + payload.page_id, + payload.target_hash, + )], + RuntimeCall::StatefulStorage( + StatefulStorageCall::apply_item_actions_with_signature_v2 { + payload, + delegator_key, + .. + }, + ) => match Msa::ensure_valid_msa_key(delegator_key) { + Ok(state_owner_msa_id) => vec![HashCheckData::new_itemized( + state_owner_msa_id, + payload.schema_id, + payload.target_hash, + )], + _ => vec![], + }, + RuntimeCall::StatefulStorage(StatefulStorageCall::upsert_page_with_signature_v2 { + payload, + delegator_key, + .. + }) => match Msa::ensure_valid_msa_key(delegator_key) { + Ok(state_owner_msa_id) => vec![HashCheckData::new_paginated( + state_owner_msa_id, + payload.schema_id, + payload.page_id, + payload.target_hash, + )], + _ => vec![], + }, + RuntimeCall::StatefulStorage(StatefulStorageCall::delete_page_with_signature_v2 { + payload, + delegator_key, + .. + }) => match Msa::ensure_valid_msa_key(delegator_key) { + Ok(state_owner_msa_id) => vec![HashCheckData::new_paginated( + state_owner_msa_id, + payload.schema_id, + payload.page_id, + payload.target_hash, + )], + _ => vec![], + }, + RuntimeCall::FrequencyTxPayment(FrequencyPaymentCall::pay_with_capacity { + call, + .. + }) => Self::extract_hash_data(call), + RuntimeCall::FrequencyTxPayment( + FrequencyPaymentCall::pay_with_capacity_batch_all { calls, .. }, + ) => calls.iter().flat_map(|c| Self::extract_hash_data(c)).collect(), + RuntimeCall::Utility(UtilityCall::batch { calls, .. }) | + RuntimeCall::Utility(UtilityCall::batch_all { calls, .. }) => + calls.iter().flat_map(|c| Self::extract_hash_data(c)).collect(), + _ => vec![], + } + } + + /// Verifies the hashes for an Itemized Stateful Storage extrinsic + fn verify_hash_itemized( + msa_id: &MessageSourceId, + schema_id: &SchemaId, + target_hash: &PageHash, + ) -> TransactionValidity { + const TAG_PREFIX: &str = "StatefulStorageHashItemized"; + + if let Ok(Some(page)) = StatefulStorage::get_itemized_page_for(*msa_id, *schema_id) { + let current_hash: PageHash = page.get_hash(); + ensure!( + ¤t_hash == target_hash, + Self::map_dispatch_error( + pallet_stateful_storage::Error::::StalePageState.into() + ) + ); + + return ValidTransaction::with_tag_prefix(TAG_PREFIX) + .and_provides((msa_id, schema_id)) + .build() + } + Ok(Default::default()) + } + + /// Verifies the hashes for a Paginated Stateful Storage extrinsic + fn verify_hash_paginated( + msa_id: &MessageSourceId, + schema_id: &SchemaId, + page_id: &PageId, + target_hash: &PageHash, + ) -> TransactionValidity { + const TAG_PREFIX: &str = "StatefulStorageHashPaginated"; + if let Ok(Some(page)) = + StatefulStorage::get_paginated_page_for(*msa_id, *schema_id, *page_id) + { + let current_hash: PageHash = page.get_hash(); + + ensure!( + ¤t_hash == target_hash, + Self::map_dispatch_error( + pallet_stateful_storage::Error::::StalePageState.into() + ) + ); + + return ValidTransaction::with_tag_prefix(TAG_PREFIX) + .and_provides((msa_id, schema_id, page_id)) + .build() + } + + Ok(Default::default()) + } + + /// Map a module DispatchError to an InvalidTransaction::Custom error + fn map_dispatch_error(err: DispatchError) -> InvalidTransaction { + InvalidTransaction::Custom(match err { + DispatchError::Module(module_err) => + ::decode(&mut module_err.error.as_slice()) + .unwrap_or_default() + .try_into() + .unwrap_or_default(), + _ => 255u8, + }) + } +} + #[cfg(feature = "runtime-benchmarks")] #[macro_use] extern crate frame_benchmarking;