diff --git a/Cargo.lock b/Cargo.lock index 57d3281..5ba1fe3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5121,6 +5121,7 @@ dependencies = [ "frame-support 37.0.1", "frame-system 37.1.0", "log", + "pallet-balances", "pallet-issuers", "parity-scale-codec", "scale-info", diff --git a/frame-weight-template.hbs b/frame-weight-template.hbs new file mode 100644 index 0000000..ba2fa45 --- /dev/null +++ b/frame-weight-template.hbs @@ -0,0 +1,121 @@ +{{header}} +//! Autogenerated weights for `{{pallet}}` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION {{version}} +//! DATE: {{date}}, STEPS: `{{cmd.steps}}`, REPEAT: `{{cmd.repeat}}`, LOW RANGE: `{{cmd.lowest_range_values}}`, HIGH RANGE: `{{cmd.highest_range_values}}` +//! WORST CASE MAP SIZE: `{{cmd.worst_case_map_values}}` +//! HOSTNAME: `{{hostname}}`, CPU: `{{cpuname}}` +//! EXECUTION: `{{cmd.execution}}`, WASM-EXECUTION: `{{cmd.wasm_execution}}`, CHAIN: `{{cmd.chain}}`, DB CACHE: `{{cmd.db_cache}}` + +// Executed Command: +{{#each args as |arg|}} +// {{arg}} +{{/each}} + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; +use core::marker::PhantomData; + +/// Weight functions needed for `{{pallet}}`. +pub trait WeightInfo { + {{#each benchmarks as |benchmark|}} + fn {{benchmark.name~}} + ( + {{~#each benchmark.components as |c| ~}} + {{c.name}}: u32, {{/each~}} + ) -> Weight; + {{/each}} +} + +/// Weights for `{{pallet}}` using the Substrate node and recommended hardware. +pub struct SubstrateWeight(PhantomData); +{{#if (eq pallet "frame_system")}} +impl WeightInfo for SubstrateWeight { +{{else}} +impl WeightInfo for SubstrateWeight { +{{/if}} + {{#each benchmarks as |benchmark|}} + {{#each benchmark.comments as |comment|}} + /// {{comment}} + {{/each}} + {{#each benchmark.component_ranges as |range|}} + /// The range of component `{{range.name}}` is `[{{range.min}}, {{range.max}}]`. + {{/each}} + fn {{benchmark.name~}} + ( + {{~#each benchmark.components as |c| ~}} + {{~#if (not c.is_used)}}_{{/if}}{{c.name}}: u32, {{/each~}} + ) -> Weight { + // Proof Size summary in bytes: + // Measured: `{{benchmark.base_recorded_proof_size}}{{#each benchmark.component_recorded_proof_size as |cp|}} + {{cp.name}} * ({{cp.slope}} ±{{underscore cp.error}}){{/each}}` + // Estimated: `{{benchmark.base_calculated_proof_size}}{{#each benchmark.component_calculated_proof_size as |cp|}} + {{cp.name}} * ({{cp.slope}} ±{{underscore cp.error}}){{/each}}` + // Minimum execution time: {{underscore benchmark.min_execution_time}}_000 picoseconds. + Weight::from_parts({{underscore benchmark.base_weight}}, {{benchmark.base_calculated_proof_size}}) + {{#each benchmark.component_weight as |cw|}} + // Standard Error: {{underscore cw.error}} + .saturating_add(Weight::from_parts({{underscore cw.slope}}, 0).saturating_mul({{cw.name}}.into())) + {{/each}} + {{#if (ne benchmark.base_reads "0")}} + .saturating_add(T::DbWeight::get().reads({{benchmark.base_reads}}_u64)) + {{/if}} + {{#each benchmark.component_reads as |cr|}} + .saturating_add(T::DbWeight::get().reads(({{cr.slope}}_u64).saturating_mul({{cr.name}}.into()))) + {{/each}} + {{#if (ne benchmark.base_writes "0")}} + .saturating_add(T::DbWeight::get().writes({{benchmark.base_writes}}_u64)) + {{/if}} + {{#each benchmark.component_writes as |cw|}} + .saturating_add(T::DbWeight::get().writes(({{cw.slope}}_u64).saturating_mul({{cw.name}}.into()))) + {{/each}} + {{#each benchmark.component_calculated_proof_size as |cp|}} + .saturating_add(Weight::from_parts(0, {{cp.slope}}).saturating_mul({{cp.name}}.into())) + {{/each}} + } + {{/each}} +} + +// For backwards compatibility and tests. +impl WeightInfo for () { + {{#each benchmarks as |benchmark|}} + {{#each benchmark.comments as |comment|}} + /// {{comment}} + {{/each}} + {{#each benchmark.component_ranges as |range|}} + /// The range of component `{{range.name}}` is `[{{range.min}}, {{range.max}}]`. + {{/each}} + fn {{benchmark.name~}} + ( + {{~#each benchmark.components as |c| ~}} + {{~#if (not c.is_used)}}_{{/if}}{{c.name}}: u32, {{/each~}} + ) -> Weight { + // Proof Size summary in bytes: + // Measured: `{{benchmark.base_recorded_proof_size}}{{#each benchmark.component_recorded_proof_size as |cp|}} + {{cp.name}} * ({{cp.slope}} ±{{underscore cp.error}}){{/each}}` + // Estimated: `{{benchmark.base_calculated_proof_size}}{{#each benchmark.component_calculated_proof_size as |cp|}} + {{cp.name}} * ({{cp.slope}} ±{{underscore cp.error}}){{/each}}` + // Minimum execution time: {{underscore benchmark.min_execution_time}}_000 picoseconds. + Weight::from_parts({{underscore benchmark.base_weight}}, {{benchmark.base_calculated_proof_size}}) + {{#each benchmark.component_weight as |cw|}} + // Standard Error: {{underscore cw.error}} + .saturating_add(Weight::from_parts({{underscore cw.slope}}, 0).saturating_mul({{cw.name}}.into())) + {{/each}} + {{#if (ne benchmark.base_reads "0")}} + .saturating_add(RocksDbWeight::get().reads({{benchmark.base_reads}}_u64)) + {{/if}} + {{#each benchmark.component_reads as |cr|}} + .saturating_add(RocksDbWeight::get().reads(({{cr.slope}}_u64).saturating_mul({{cr.name}}.into()))) + {{/each}} + {{#if (ne benchmark.base_writes "0")}} + .saturating_add(RocksDbWeight::get().writes({{benchmark.base_writes}}_u64)) + {{/if}} + {{#each benchmark.component_writes as |cw|}} + .saturating_add(RocksDbWeight::get().writes(({{cw.slope}}_u64).saturating_mul({{cw.name}}.into()))) + {{/each}} + {{#each benchmark.component_calculated_proof_size as |cp|}} + .saturating_add(Weight::from_parts(0, {{cp.slope}}).saturating_mul({{cp.name}}.into())) + {{/each}} + } + {{/each}} +} \ No newline at end of file diff --git a/node/Cargo.toml b/node/Cargo.toml index e83d60c..54ec3c1 100644 --- a/node/Cargo.toml +++ b/node/Cargo.toml @@ -76,7 +76,7 @@ pallet-transaction-payment-rpc.workspace = true substrate-frame-rpc-system.workspace = true substrate-frame-rpc-system.default-features = true frame-benchmarking-cli.workspace = true -frame-benchmarking-cli.default-features = false +frame-benchmarking-cli.default-features = true solochain-template-runtime.workspace = true [build-dependencies] diff --git a/pallets/algorithms/src/lib.rs b/pallets/algorithms/src/lib.rs index 55b0ec9..bfb3689 100644 --- a/pallets/algorithms/src/lib.rs +++ b/pallets/algorithms/src/lib.rs @@ -13,16 +13,43 @@ pub mod pallet { use sp_runtime::Vec; use sp_runtime::traits::Hash; use wasmi::{Func, Caller}; + use wasmi::core::Trap; use pallet_credentials::{self as credentials, Attestations, CredAttestation, CredSchema, AcquirerAddress}; use super::*; + #[derive(Clone, Encode, Decode, PartialEq, RuntimeDebug, TypeInfo)] + pub struct GasMeter { + pub consumed: u64, + pub limit: u64, + } + + impl GasMeter { + pub fn new(limit: u64) -> Self { + Self { + consumed: 0, + limit + } + } + + pub fn charge(&mut self, amount: u64) -> Result<(), DispatchError> { + self.consumed = self.consumed.checked_add(amount) + .ok_or(DispatchError::Other("Gas Overflow"))?; + + if self.consumed > self.limit { + return Err(DispatchError::Other("Out of Gas")); + } + Ok(()) + } + } + #[derive(Clone, Encode, Decode, PartialEq, RuntimeDebug, TypeInfo)] #[scale_info(skip_type_params(T))] pub struct Algorithm { pub schema_hashes: BoundedVec, pub code: BoundedVec, + pub gas_limit: u64, } #[pallet::pallet] @@ -39,6 +66,22 @@ pub mod pallet { #[pallet::constant] type MaxCodeSize: Get; + + #[pallet::constant] + type MaxMemoryPages: Get; + + #[pallet::constant] + type DefaultGasLimit: Get; + + #[pallet::constant] + type GasCost: Get; + } + + #[derive(Clone, Encode, Decode, PartialEq, RuntimeDebug, TypeInfo)] + pub struct GasCosts { + pub basic_op: u64, + pub memory_op: u64, + pub call_op: u64, } #[pallet::storage] @@ -74,15 +117,19 @@ pub mod pallet { AlgoError6, InvalidWasmProvided, TooManySchemas, - CodeTooHeavy - + CodeTooHeavy, + AlgoExecutionFailed, + TooComplexModule, + OutOfGas, + GasOverflow, + GasMeteringNotSupported, } #[pallet::call] impl Pallet { #[pallet::call_index(1)] #[pallet::weight(100_000)] - pub fn save_algo(origin: OriginFor, schema_hashes: Vec, code: Vec) -> DispatchResult { + pub fn save_algo(origin: OriginFor, schema_hashes: Vec, code: Vec, gas_limit: Option) -> DispatchResult { let who = ensure_signed(origin)?; ensure!(schema_hashes.len() <= T::MaxSchemas::get() as usize, Error::::TooManySchemas); @@ -103,6 +150,7 @@ pub mod pallet { Algorithms::::insert(id, Algorithm { schema_hashes: BoundedVec::try_from(schema_hashes).map_err(|_| Error::::TooManySchemas)?, code: BoundedVec::try_from(code).map_err(|_| Error::::CodeTooHeavy)?, + gas_limit: gas_limit.unwrap_or_else(|| T::DefaultGasLimit::get()), }); Self::deposit_event(Event::AlgorithmAdded { @@ -130,40 +178,46 @@ pub mod pallet { } - return Pallet::::run_code(algorithm.code.to_vec(), attestations); + return Pallet::::run_code(algorithm.code.to_vec(), attestations, algorithm.gas_limit); } } impl Pallet { - pub fn run_code(code: Vec, attestations: Vec>) -> DispatchResult { + pub fn run_code(code: Vec, attestations: Vec>, gas_limit: u64) -> DispatchResult { let engine = wasmi::Engine::default(); + let gas_meter = GasMeter::new(gas_limit); + let module = - wasmi::Module::new(&engine, code.as_slice()).map_err(|_| Error::::AlgoError1)?; + wasmi::Module::new(&engine, code.as_slice()).map_err(|_| Error::::InvalidWasmProvided)?; - type HostState = u32; - let mut store = wasmi::Store::new(&engine, 42); + let mut store = wasmi::Store::new(&engine, gas_meter); + let host_print = wasmi::Func::wrap( &mut store, - |caller: wasmi::Caller<'_, HostState>, param: i32| { + |mut caller: wasmi::Caller<'_, GasMeter>, param: i32| { + caller.data_mut().charge(T::GasCost::get().basic_op).map_err(|_| Trap::new("Gas charge failed"))?; log::debug!(target: "algo", "Message:{:?}", param); + Ok(()) }, ); + let abort_func = wasmi::Func::wrap( &mut store, - |_: Caller<'_, HostState>, msg_id: i32, filename: i32, line: i32, col: i32| { + |mut caller: Caller<'_, GasMeter>, msg_id: i32, filename: i32, line: i32, col: i32| -> Result<(), Trap> { + caller.data_mut().charge(T::GasCost::get().call_op).map_err(|_| Trap::new("Gas charge failed"))?; log::error!( target: "algo", "Abort called: msg_id={}, file={}, line={}, col={}", msg_id, filename, line, col ); - // Err(wasmi::Trap::new(wasmi::TrapKind::Unreachable)) + Err(Trap::new("Gas charge failed")) }, ); let memory = wasmi::Memory::new( &mut store, - wasmi::MemoryType::new(8, None).map_err(|_| Error::::AlgoError2)?, + wasmi::MemoryType::new(T::MaxMemoryPages::get(), Some(T::MaxMemoryPages::get())).map_err(|_| Error::::AlgoError2)?, ) .map_err(|_| Error::::AlgoError2)?; @@ -171,12 +225,17 @@ pub mod pallet { let bytes = attestations.into_iter().flatten().flatten().collect::>(); memory.write(&mut store, 0, &bytes).map_err(|e| { - log::error!(target: "algo", "Algo1 {:?}", e); + log::error!(target: "algo", "Memory write error {:?}", e); Error::::AlgoError1 })?; + + store.data_mut().charge( + T::GasCost::get().memory_op * (bytes.len() as u64 / 32 + 1)) + .map_err(|_| Error::::OutOfGas)?; + // memory.write(&mut store, 0, 5); - let mut linker = >::new(&engine); + let mut linker = >::new(&engine); linker.define("host", "print", host_print).map_err(|_| Error::::AlgoError2)?; linker.define("env", "memory", memory).map_err(|_| Error::::AlgoError2)?; @@ -185,14 +244,15 @@ pub mod pallet { log::error!(target: "algo", "Algo3 {:?}", bytes.clone()); log::error!(target: "algo", "Algo3 {:?}", bytes.len()); + let instance = linker - .instantiate(&mut store, &module) - .map_err(|e| { - log::error!(target: "algo", "Algo3 {:?}", e); - Error::::AlgoError3 - })? - .start(&mut store) - .map_err(|_| Error::::AlgoError4)?; + .instantiate(&mut store, &module) + .map_err(|e| { + log::error!(target: "algo", "Instantiation error {:?}", e); + Error::::AlgoError3 + })? + .start(&mut store) + .map_err(|_| Error::::AlgoError4)?; let calc = instance .get_typed_func::<(), i64>(&store, "calc") @@ -200,9 +260,11 @@ pub mod pallet { // And finally we can call the wasm! let result = calc.call(&mut store, ()).map_err(|e| { - log::error!(target: "algo", "Algo6 {:?}", e); + log::error!(target: "algo", "Execution error {:?}", e); Error::::AlgoError6 })?; + + Self::deposit_event(Event::AlgoResult { result, }); diff --git a/pallets/credentials/Cargo.toml b/pallets/credentials/Cargo.toml index ccf06dd..cd80f23 100644 --- a/pallets/credentials/Cargo.toml +++ b/pallets/credentials/Cargo.toml @@ -20,9 +20,10 @@ codec = { features = [ scale-info = { features = [ "derive", ], workspace = true } -frame-benchmarking = { optional = true, workspace = true } +frame-benchmarking.workspace = true frame-support.workspace = true frame-system.workspace = true +pallet-balances.workspace = true pallet-issuers = { path = "../issuers", default-features = false } bs58 = { version = "0.5.1", default-features = false } sp-std.workspace = true @@ -39,13 +40,14 @@ sp-runtime = { default-features = true, workspace = true } default = ["std"] std = [ "codec/std", - "frame-benchmarking?/std", + "frame-benchmarking/std", "frame-support/std", "frame-system/std", "scale-info/std", "sp-runtime/std", "pallet-issuers/std", - "sp-core/std" + "sp-core/std", + "pallet-balances/std" ] runtime-benchmarks = [ "frame-benchmarking/runtime-benchmarks", diff --git a/pallets/credentials/src/benchmarking.rs b/pallets/credentials/src/benchmarking.rs new file mode 100644 index 0000000..bb9ab3f --- /dev/null +++ b/pallets/credentials/src/benchmarking.rs @@ -0,0 +1,158 @@ +#![cfg(feature = "runtime-benchmarks")] + +use super::*; + +use frame_benchmarking::{v2::*, whitelisted_caller, BenchmarkError}; +use frame_support::{BoundedVec, ensure, traits::Get}; +use frame_system::RawOrigin; +use sp_std::{vec, iter}; +use sp_std::vec::Vec; +use sp_runtime::traits::Hash; +use codec::Encode; +use sp_core::{crypto::{Ss58Codec}}; +use sp_runtime::AccountId32; + + + +#[benchmarks] +mod benchmarks { + use super::*; + + fn generate_issuer(controllers: Vec) -> T::Hash{ + let name_length = T::MaxNameLength::get(); + let issuer_name = BoundedVec::::try_from(vec![1; name_length as usize]).expect("name too long"); + let issuer_hash = ::Hashing::hash(&issuer_name); + let bounded_controllers = BoundedVec::::try_from(controllers).expect("too many controllers"); + pallet_issuers::Issuers::::insert(issuer_hash, pallet_issuers::Issuer:: { name: issuer_name, controllers: bounded_controllers }); + issuer_hash + } + + fn generate_schema(max_num_fields: usize, max_field_size: usize) -> T::Hash { + + let max_num_fields = max_num_fields.min(T::MaxSchemaFields::get() as usize); + + let field_name: BoundedVec = BoundedVec::try_from(vec![b'a'; max_field_size]).expect("schema field name too long"); + + let schema: BoundedVec<(BoundedVec, CredType), T::MaxSchemaFields> = BoundedVec::try_from(vec![(field_name, CredType::Hash); max_num_fields]).expect("too many schema fields"); + + let bytes: Vec = schema.iter() + .flat_map(|(vec, cred_type)| { + let mut bytes = vec.to_vec(); + bytes.extend_from_slice(&cred_type.encode()); + bytes + }) + .collect(); + + let schema_hash = ::Hashing::hash(&bytes); + + Schemas::::insert(schema_hash, schema.clone()); + + schema_hash + } + + fn get_hash_attestation(num_fields: usize) -> Vec>{ + let max_fields = num_fields.min(T::MaxSchemaFields::get() as usize); + let hash_value = ::Hashing::hash(&[42u8]).as_ref().to_vec(); + + vec![hash_value; max_fields] + } + + fn generate_test_address(address_type: usize) -> Vec { + match address_type % 3 { + 0 => { + // Ethereum address (20 bytes) + let mut eth_addr = [0u8; 20]; + for i in 0..20 { + eth_addr[i] = i as u8; + } + eth_addr.to_vec() + }, + 1 => { + // Solana address (base58 encoded public key, typically 32 or 44 bytes) + // Using a valid base58 encoded Solana pubkey + "7QX6LJUz7LyEm7fRNMpLhRk2ye5h3k6JDJ3VXZsqnFan".as_bytes().to_vec() + }, + _ => { + // Substrate address + // let account: T::AccountId = whitelisted_caller(); + // account.encode() + "15LvsPtVtXB3Xr3yk3WGLczSk7jdzhvPf22C1e7ceiCHkkjU".to_string().into_bytes() + } + } + } + + + // Benchmark `create_schema` extrinsic + #[benchmark] + fn create_schema(x: Linear<1, { T::MaxSchemaFields::get() - 1 }>) -> Result<(), BenchmarkError> { + let caller: T::AccountId = whitelisted_caller(); + + let issuer_hash = generate_issuer::(vec![caller.clone()]); + + ensure!(pallet_issuers::Issuers::::contains_key(issuer_hash.clone()), "Issuer did not get created."); + + // Prepare a schema with maximum field name size + let max_field_size = T::MaxSchemaFieldSize::get() as usize; + let num_fields = x as usize; + + let field_name: Vec = vec![b'a'; max_field_size]; + + let schema: Vec<(Vec, CredType)> = vec![(field_name, CredType::Hash); num_fields]; + + #[extrinsic_call] + create_schema(RawOrigin::Signed(caller.clone()), issuer_hash, schema.clone()); + + let bytes: Vec = schema.iter() + .flat_map(|(vec, cred_type)| { + let mut bytes = vec.to_vec(); + bytes.extend_from_slice(&cred_type.encode()); + bytes + }) + .collect(); + + let schema_hash = ::Hashing::hash(&bytes); + + ensure!(Schemas::::contains_key(schema_hash), "Schema did not get registered"); + + Ok(()) + } + + #[benchmark] + fn attest( + x: Linear<1, { T::MaxSchemaFields::get() - 1 }>, // Number of hash fields (1 to 100) + y: Linear<0, 3> + ) -> Result<(), BenchmarkError>{ + + // create issuer + let caller: T::AccountId = whitelisted_caller(); + + let max_field_size = T::MaxSchemaFieldSize::get() as usize; + + let issuer_hash = generate_issuer::(vec![caller.clone()]); + + // create schema + let schema_hash = generate_schema::(x as usize, max_field_size); + + // create attestation + let attestation = get_hash_attestation::(x as usize); + + // create for account + + let for_account: Vec = generate_test_address::(y as usize); + + ensure!(!for_account.is_empty(), "Invalid account generation"); + + + #[extrinsic_call] + attest(RawOrigin::Signed(caller), issuer_hash, schema_hash, for_account, attestation); + + Ok(()) + + } + + impl_benchmark_test_suite!( + Pallet, + crate::tests::new_test_ext(), + crate::tests::Test, + ); +} \ No newline at end of file diff --git a/pallets/credentials/src/lib.rs b/pallets/credentials/src/lib.rs index 4e0d72d..18e14b3 100644 --- a/pallets/credentials/src/lib.rs +++ b/pallets/credentials/src/lib.rs @@ -2,6 +2,14 @@ pub use pallet::*; +#[cfg(feature = "runtime-benchmarks")] +mod benchmarking; + +#[cfg(feature = "runtime-benchmarks")] +pub use benchmarking::*; + +pub mod tests; + #[frame_support::pallet] pub mod pallet { use frame_support::{ @@ -148,6 +156,14 @@ pub mod pallet { let who = ensure_signed(origin)?; ensure!(schema.len() <= T::MaxSchemaFields::get() as usize, Error::::TooManySchemaFields); + + let issuer = Issuers::::get(issuer_hash) + .ok_or(pallet_issuers::Error::::IssuerNotFound)?; + + ensure!( + issuer.controllers.contains(&who), + pallet_issuers::Error::::NotAuthorized + ); let mut bounded_schema = CredSchema::::default(); @@ -175,14 +191,6 @@ pub mod pallet { !Schemas::::contains_key(schema_hash), Error::::SchemaAlreadyExists ); - - let issuer = Issuers::::get(issuer_hash) - .ok_or(pallet_issuers::Error::::IssuerNotFound)?; - ensure!( - issuer.controllers.contains(&who), - pallet_issuers::Error::::NotAuthorized - ); - let cred_schema = CredSchema::::try_from(bounded_schema).map_err(|_| Error::::TooManySchemaFields)?; diff --git a/pallets/credentials/src/tests.rs b/pallets/credentials/src/tests.rs new file mode 100644 index 0000000..b92ad4a --- /dev/null +++ b/pallets/credentials/src/tests.rs @@ -0,0 +1,121 @@ +#![cfg(test)] +// Tests for Credentials Pallet + +use super::*; +use crate::{ + self as pallet_credentials +}; +use pallet_issuers; +use pallet_balances; + +use codec::{Decode, Encode}; +use frame_support::{ + assert_err, assert_noop, assert_ok, derive_impl, parameter_types, + traits::{ConstU32, ConstU64}, + BoundedVec, +}; +use frame_system::EnsureRoot; +use sp_core::H256; +use sp_io::crypto::{sr25519_generate, sr25519_sign}; +use sp_runtime::{ + traits::{BadOrigin, BlakeTwo256, IdentifyAccount, IdentityLookup, Verify}, + BuildStorage, MultiSignature, MultiSigner, +}; + +use frame_support::traits::{tokens::ExistenceRequirement, Currency}; + +type AccountIdOf = ::AccountId; +pub type AccountPublic = ::Signer; +pub type AccountId = ::AccountId; + +type Block = frame_system::mocking::MockBlock; + +type Balance = ::Balance; +type AccountData = pallet_balances::AccountData; + +frame_support::construct_runtime!( + pub enum Test + { + System: frame_system, + CredentialsModule: pallet_credentials, + Issuers: pallet_issuers, + Balances: pallet_balances, + } +); + +#[derive_impl(frame_system::config_preludes::TestDefaultConfig)] +impl frame_system::Config for Test { + type AccountId = AccountId; + type BaseCallFilter = frame_support::traits::Everything; + type BlockWeights = (); + type BlockLength = (); + type DbWeight = (); + type RuntimeOrigin = RuntimeOrigin; + type RuntimeCall = RuntimeCall; + type Hash = H256; + type Hashing = BlakeTwo256; + type Lookup = IdentityLookup; + type RuntimeEvent = RuntimeEvent; + type BlockHashCount = ConstU64<250>; + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type OnSetCode = (); + type MaxConsumers = frame_support::traits::ConstU32<16>; + type Block = Block; + type RuntimeTask = (); +} + +parameter_types! { + pub const MaxSchemaFields: u32 = 20; + pub const MaxSchemaFieldSize: u32 = 120; + pub const MaxNameLength: u32 = 120; + pub const MaxControllers: u32 = 20; + pub const IssuerRegistryDeposit: u128 = 1_000_000_000_000; + pub const ExistentialDeposit: u64 = 1; + pub const MaxFreezes: u32 = 1; +} + +impl pallet_balances::Config for Test { + type Balance = u128; + type RuntimeEvent = RuntimeEvent; + type DustRemoval = (); + type ExistentialDeposit = ExistentialDeposit; + type AccountStore = frame_system::Pallet; + type WeightInfo = (); + type MaxLocks = ConstU32<50>; + type MaxReserves = (); + type ReserveIdentifier = [u8; 8]; + type RuntimeHoldReason = (); + type RuntimeFreezeReason = (); + type FreezeIdentifier = (); + type MaxFreezes = MaxFreezes; +} + +impl pallet_issuers::Config for Test { + type RuntimeEvent = RuntimeEvent; + type Hashing = BlakeTwo256; + + type MaxNameLength = MaxNameLength; + type MaxControllers = MaxControllers; + + type WeightInfo = pallet_issuers::weights::SubstrateWeight; + type Currency = Balances; + + type IssuerRegistryDeposit = IssuerRegistryDeposit; +} + +impl pallet_credentials::Config for Test { + type RuntimeEvent = RuntimeEvent; + type Hashing = BlakeTwo256; + + type MaxSchemaFields = MaxSchemaFields; + type MaxSchemaFieldSize = MaxSchemaFieldSize; +} + +pub fn new_test_ext() -> sp_io::TestExternalities { + frame_system::GenesisConfig::::default().build_storage().unwrap().into() +} \ No newline at end of file diff --git a/pallets/issuers/Cargo.toml b/pallets/issuers/Cargo.toml index 6e8e8fe..0a9513e 100644 --- a/pallets/issuers/Cargo.toml +++ b/pallets/issuers/Cargo.toml @@ -19,7 +19,7 @@ codec = { features = [ scale-info = { features = [ "derive", ], workspace = true } -frame-benchmarking = { optional = true, workspace = true } +frame-benchmarking = {workspace = true } frame-support.workspace = true frame-system.workspace = true sp-std.workspace = true @@ -34,17 +34,13 @@ sp-runtime = { default-features = true, workspace = true } default = ["std"] std = [ "codec/std", - "frame-benchmarking?/std", + "frame-benchmarking/std", "frame-support/std", "frame-system/std", "scale-info/std", "sp-runtime/std", ] -runtime-benchmarks = [ - "frame-benchmarking/runtime-benchmarks", - "frame-support/runtime-benchmarks", - "frame-system/runtime-benchmarks", -] +runtime-benchmarks = ["frame-benchmarking/runtime-benchmarks"] try-runtime = [ "frame-support/try-runtime", "frame-system/try-runtime", diff --git a/pallets/issuers/src/benchmarking.rs b/pallets/issuers/src/benchmarking.rs new file mode 100644 index 0000000..15a03bf --- /dev/null +++ b/pallets/issuers/src/benchmarking.rs @@ -0,0 +1,91 @@ +#![cfg(feature = "runtime-benchmarks")] + +use super::*; + +use frame_benchmarking::{v2::*, whitelisted_caller, BenchmarkError}; +use frame_support::{BoundedVec, ensure, traits::Get}; +use frame_system::RawOrigin; +use sp_std::vec; +use sp_std::vec::Vec; +use sp_runtime::traits::Hash; + + +#[benchmarks] +mod benchmarks { + use super::*; + + fn generate_controllers(n: u32) -> Vec { + let caller: T::AccountId = whitelisted_caller(); + vec![caller.clone(); n as usize] + } + + // Benchmark `create_issuer` extrinsic + #[benchmark] + fn create_issuer(n: Linear<1, { T::MaxNameLength::get() - 1 }>, c: Linear<1, { T::MaxControllers::get() - 1}>) -> Result<(), BenchmarkError> { + // Setup: Create test data + let caller: T::AccountId = whitelisted_caller(); + let name: Vec = vec![1; n as usize]; + let controllers = generate_controllers::(c); + + #[extrinsic_call] + create_issuer(RawOrigin::Signed(caller), name.clone(), controllers); + + // Verify the issuer was created + let hash = ::Hashing::hash(&name); + ensure!(Issuers::::contains_key(hash), "Issuer did not get created."); + + Ok(()) + } + + // Benchmark `edit_controllers` extrinsic + #[benchmark] + fn edit_controllers(c: Linear<1, { T::MaxControllers::get() - 1}>) -> Result<(), BenchmarkError> { + + // Setup: create an issuer + let caller: T::AccountId = whitelisted_caller(); + let name: Vec = vec![1; T::MaxNameLength::get() as usize]; + let initial_controllers: Vec = vec![caller.clone(); T::MaxControllers::get() as usize]; + + let hash = ::Hashing::hash(&name); + + // Create initial issuer + let issuer_name = BoundedVec::::try_from(name).unwrap(); + let controllers_identified = BoundedVec::::try_from(initial_controllers).unwrap(); + let issuer = Issuer:: { + name: issuer_name, + controllers: controllers_identified, + }; + Issuers::::insert(hash, issuer); + + // Create new controllers for the update + let new_controllers: Vec = generate_controllers::(c); + let new_controllers_clone = new_controllers.clone(); + + #[extrinsic_call] + edit_controllers( + RawOrigin::Signed(caller), + hash, + Some(new_controllers), + ); + + // Verify the issuer was updated + ensure!(Issuers::::contains_key(hash), "issuer got removed"); + + let stored_issuer = Issuers::::get(hash); + + ensure!( + stored_issuer.unwrap().controllers == + BoundedVec::::try_from(new_controllers_clone).unwrap(), + "Controllers were not updated correctly" + ); + + Ok(()) + + } + + impl_benchmark_test_suite!( + IssuersModule, + crate::tests::new_test_ext(), + crate::tests::Test, + ); +} \ No newline at end of file diff --git a/pallets/issuers/src/lib.rs b/pallets/issuers/src/lib.rs index 615dd84..0c044ad 100644 --- a/pallets/issuers/src/lib.rs +++ b/pallets/issuers/src/lib.rs @@ -3,15 +3,29 @@ pub use pallet::*; +#[cfg(feature = "runtime-benchmarks")] +mod benchmarking; + +#[cfg(feature = "runtime-benchmarks")] +pub use benchmarking::*; + +pub mod weights; + #[frame_support::pallet] pub mod pallet { use frame_support::pallet_prelude::{*, OptionQuery}; use frame_system::pallet_prelude::*; + use frame_support::{ + traits::{Currency, ReservableCurrency, Get}, + }; use sp_runtime::traits::Hash; + use sp_std::collections::btree_set::BTreeSet; use sp_std::prelude::*; use super::*; + pub use weights::WeightInfo; + #[derive(Clone, Encode, Decode, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)] #[scale_info(skip_type_params(T))] pub struct Issuer { @@ -30,13 +44,23 @@ pub mod pallet { type RuntimeEvent: From> + IsType<::RuntimeEvent>; type Hashing: Hash; + type Currency: Currency + ReservableCurrency; + #[pallet::constant] type MaxNameLength: Get; #[pallet::constant] type MaxControllers: Get; + + type WeightInfo: WeightInfo; + + /// The amount that needs to be deposited to create an issuer + #[pallet::constant] + type IssuerRegistryDeposit: Get>; } + type BalanceOf = <::Currency as Currency<::AccountId>>::Balance; + #[pallet::storage] pub type Issuers = StorageMap<_, Blake2_128Concat, T::Hash, Issuer, OptionQuery>; @@ -55,19 +79,21 @@ pub mod pallet { IssuerNotFound, NotAuthorized, IssuerNameTooLong, - TooManyControllers + TooManyControllers, + + InsufficientBalance } #[pallet::call] impl Pallet { #[pallet::call_index(0)] - #[pallet::weight(100_000)] + #[pallet::weight(T::WeightInfo::create_issuer(T::MaxNameLength::get() as u32, T::MaxControllers::get() as u32))] pub fn create_issuer( origin: OriginFor, name: Vec, controllers: Vec, - ) -> DispatchResult { + ) -> DispatchResultWithPostInfo { let who = ensure_signed(origin)?; //TODO: trim trailing spaces from spaces from issuer name @@ -83,29 +109,37 @@ pub mod pallet { let issuer_name = BoundedVec::::try_from(name) .map_err(|_| Error::::IssuerNameTooLong)?; - let controllers_identified = BoundedVec::::try_from(controllers) + let unique_controllers: Vec = controllers + .into_iter() + .collect::>() + .into_iter() + .collect(); + + let controllers_identified = BoundedVec::::try_from(unique_controllers) .map_err(|_| Error::::TooManyControllers)?; - + + T::Currency::reserve(&who, T::IssuerRegistryDeposit::get().into()) + .map_err(|_| Error::::InsufficientBalance)?; // let issuer = Issuer:: { name: issuer_name.clone(), controllers: controllers_identified.clone() }; Issuers::::insert(hash, Issuer:: { name: issuer_name.clone(), controllers: controllers_identified.clone() }); - Self::deposit_event(Event::IssuerCreated { hash, issuer_name, controllers_identified }); + Self::deposit_event(Event::IssuerCreated { hash, issuer_name: issuer_name.clone(), controllers_identified: controllers_identified.clone() }); - Ok(()) + Ok(Some(T::WeightInfo::create_issuer(issuer_name.len() as u32, controllers_identified.len() as u32)).into()) } #[pallet::call_index(1)] - #[pallet::weight(100_000)] + #[pallet::weight(T::WeightInfo::edit_controllers(T::MaxControllers::get() as u32))] pub fn edit_controllers( origin: OriginFor, hash: T::Hash, controllers: Option>, - ) -> DispatchResult { + ) -> DispatchResultWithPostInfo { let who = ensure_signed(origin)?; let mut issuer = Issuers::::get(hash) .ok_or(Error::::IssuerNotFound)?; - let mut hash = hash; + let hash = hash; ensure!(issuer.controllers.contains(&who), Error::::NotAuthorized); @@ -118,6 +152,8 @@ pub mod pallet { // .map_err(|_| Error::::IssuerNameTooLong)?; // } + // TODO: Duplicates in controllers + if let Some(controllers) = controllers { ensure!(controllers.len() <= T::MaxControllers::get() as usize, Error::::TooManyControllers); issuer.controllers = BoundedVec::::try_from(controllers) @@ -126,9 +162,9 @@ pub mod pallet { Issuers::::insert(hash, Issuer:: { name: issuer.name.clone(), controllers: issuer.controllers.clone() }); - Self::deposit_event(Event::IssuerUpdated { hash, issuer_name: issuer.name, controllers_identified: issuer.controllers}); + Self::deposit_event(Event::IssuerUpdated { hash, issuer_name: issuer.name, controllers_identified: issuer.controllers.clone()}); - Ok(()) + Ok(Some(T::WeightInfo::edit_controllers(issuer.controllers.len() as u32)).into()) } } } diff --git a/pallets/issuers/src/tests.rs b/pallets/issuers/src/tests.rs new file mode 100644 index 0000000..0c550bb --- /dev/null +++ b/pallets/issuers/src/tests.rs @@ -0,0 +1,80 @@ +#![cfg(test)] +// Tests for Issuers Pallet + +use super::*; +use crate::{ + self as pallet_issuers +}; + +use codec::{Decode, Encode}; +use frame_support::{ + assert_err, assert_noop, assert_ok, derive_impl, parameter_types, + traits::{ConstU32, ConstU64}, + BoundedVec, +}; +use frame_system::EnsureRoot; +use sp_core::H256; +use sp_io::crypto::{sr25519_generate, sr25519_sign}; +use sp_runtime::{ + traits::{BadOrigin, BlakeTwo256, IdentifyAccount, IdentityLookup, Verify}, + BuildStorage, MultiSignature, MultiSigner, +}; + +type AccountIdOf = ::AccountId; +pub type AccountPublic = ::Signer; +pub type AccountId = ::AccountId; + +type Block = frame_system::mocking::MockBlock; + +frame_support::construct_runtime!( + pub enum Test + { + System: frame_system, + Issuers: pallet_issuers, + } +); + +#[derive_impl(frame_system::config_preludes::TestDefaultConfig)] +impl frame_system::Config for Test { + type AccountId = AccountId; + type BaseCallFilter = frame_support::traits::Everything; + type BlockWeights = (); + type BlockLength = (); + type DbWeight = (); + type RuntimeOrigin = RuntimeOrigin; + type RuntimeCall = RuntimeCall; + type Hash = H256; + type Hashing = BlakeTwo256; + type Lookup = IdentityLookup; + type RuntimeEvent = RuntimeEvent; + type BlockHashCount = ConstU64<250>; + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = (); + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type OnSetCode = (); + type MaxConsumers = frame_support::traits::ConstU32<16>; + type Block = Block; + type RuntimeTask = (); + + +} + +parameter_types! { + pub const MaxNameLength: u32 = 120; + pub const MaxControllers: u32 = 20; +} + +impl pallet_issuers::Config for Test { + type RuntimeEvent = RuntimeEvent; + type Hashing = BlakeTwo256; + + type MaxNameLength = MaxNameLength; + type MaxControllers = MaxControllers; +} + +pub fn new_test_ext() -> sp_io::TestExternalities { + frame_system::GenesisConfig::::default().build_storage().unwrap().into() +} \ No newline at end of file diff --git a/pallets/issuers/src/weights.rs b/pallets/issuers/src/weights.rs new file mode 100644 index 0000000..f7a686a --- /dev/null +++ b/pallets/issuers/src/weights.rs @@ -0,0 +1,107 @@ + +//! Autogenerated weights for `pallet_issuers` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 42.0.0 +//! DATE: 2024-11-24, STEPS: `100`, REPEAT: `50`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `ip-172-31-28-224`, CPU: `Intel(R) Xeon(R) CPU E5-2686 v4 @ 2.30GHz` +//! EXECUTION: ``, WASM-EXECUTION: `Compiled`, CHAIN: `Some("dev")`, DB CACHE: `1024` + +// Executed Command: +// target/release/solochain-template-node +// benchmark +// pallet +// --chain=dev +// --steps=100 +// --repeat=50 +// --pallet=pallet-issuers +// --extrinsic=* +// --wasm-execution=compiled +// --heap-pages=4096 +// --output=./pallets/issuers/src/weights.rs +// --template=frame-weight-template.hbs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; +use core::marker::PhantomData; + +/// Weight functions needed for `pallet_issuers`. +pub trait WeightInfo { + fn create_issuer(n: u32, c: u32, ) -> Weight; + fn edit_controllers(c: u32, ) -> Weight; +} + +/// Weights for `pallet_issuers` using the Substrate node and recommended hardware. +pub struct SubstrateWeight(PhantomData); +impl WeightInfo for SubstrateWeight { + /// Storage: `IssuersModule::Issuers` (r:1 w:1) + /// Proof: `IssuersModule::Issuers` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// The range of component `n` is `[1, 119]`. + /// The range of component `c` is `[1, 19]`. + fn create_issuer(n: u32, c: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `6` + // Estimated: `3471` + // Minimum execution time: 29_850_000 picoseconds. + Weight::from_parts(30_991_355, 3471) + // Standard Error: 276 + .saturating_add(Weight::from_parts(5_593, 0).saturating_mul(n.into())) + // Standard Error: 1_779 + .saturating_add(Weight::from_parts(47_701, 0).saturating_mul(c.into())) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: `IssuersModule::Issuers` (r:1 w:1) + /// Proof: `IssuersModule::Issuers` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// The range of component `c` is `[1, 19]`. + fn edit_controllers(c: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `845` + // Estimated: `4310` + // Minimum execution time: 32_842_000 picoseconds. + Weight::from_parts(34_288_735, 4310) + // Standard Error: 1_427 + .saturating_add(Weight::from_parts(77_486, 0).saturating_mul(c.into())) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } +} + +// For backwards compatibility and tests. +impl WeightInfo for () { + /// Storage: `IssuersModule::Issuers` (r:1 w:1) + /// Proof: `IssuersModule::Issuers` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// The range of component `n` is `[1, 119]`. + /// The range of component `c` is `[1, 19]`. + fn create_issuer(n: u32, c: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `6` + // Estimated: `3471` + // Minimum execution time: 29_850_000 picoseconds. + Weight::from_parts(30_991_355, 3471) + // Standard Error: 276 + .saturating_add(Weight::from_parts(5_593, 0).saturating_mul(n.into())) + // Standard Error: 1_779 + .saturating_add(Weight::from_parts(47_701, 0).saturating_mul(c.into())) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: `IssuersModule::Issuers` (r:1 w:1) + /// Proof: `IssuersModule::Issuers` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// The range of component `c` is `[1, 19]`. + fn edit_controllers(c: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `845` + // Estimated: `4310` + // Minimum execution time: 32_842_000 picoseconds. + Weight::from_parts(34_288_735, 4310) + // Standard Error: 1_427 + .saturating_add(Weight::from_parts(77_486, 0).saturating_mul(c.into())) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } +} \ No newline at end of file diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index 0e1fde1..62c171d 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -22,7 +22,7 @@ scale-info = { features = [ ], workspace = true } frame-support = { features = ["experimental"], workspace = true } frame-system.workspace = true -frame-try-runtime = { optional = true, workspace = true } +frame-try-runtime = { workspace = true } frame-executive.workspace = true pallet-aura.workspace = true pallet-balances.workspace = true @@ -46,8 +46,8 @@ sp-version = { features = ["serde"], workspace = true } sp-genesis-builder.workspace = true frame-system-rpc-runtime-api.workspace = true pallet-transaction-payment-rpc-runtime-api.workspace = true -frame-benchmarking = { optional = true, workspace = true } -frame-system-benchmarking = { optional = true, workspace = true } +frame-benchmarking = { workspace = true } +frame-system-benchmarking = { workspace = true } pallet-issuers.workspace = true pallet-credentials.workspace = true pallet-algorithms.workspace = true @@ -64,13 +64,13 @@ std = [ "scale-info/std", "frame-executive/std", "frame-support/std", - "frame-system-benchmarking?/std", + "frame-system-benchmarking/std", "frame-system-rpc-runtime-api/std", "frame-system/std", "pallet-uniques/std", - "frame-benchmarking?/std", - "frame-try-runtime?/std", + "frame-benchmarking/std", + "frame-try-runtime/std", "pallet-aura/std", "pallet-balances/std", diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index a3da3f0..188d377 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -51,6 +51,10 @@ pub use pallet_algorithms; pub use pallet_issuers; pub use pallet_credentials; +pub use pallet_algorithms::GasCosts; + +use pallet_issuers::weights::WeightInfo; + /// An index to a block. pub type BlockNumber = u32; @@ -269,12 +273,33 @@ impl pallet_sudo::Config for Runtime { type WeightInfo = pallet_sudo::weights::SubstrateWeight; } +// pub const DEFAULT_GAS_COSTS: GasCosts = GasCosts { +// basic_op: 1, +// memory_op: 10, +// call_op: 50, +// }; + +pub struct ConstGasCosts; + +impl frame_support::traits::Get for ConstGasCosts { + fn get() -> GasCosts { + GasCosts { + basic_op: 100, // Set a value for basic operations + memory_op: 200, // Set a value for memory operations + call_op: 300, // Set a value for call operations + } + } +} + /// Configure the pallet-algorithms in pallets/algorithms. impl pallet_algorithms::Config for Runtime { type RuntimeEvent = RuntimeEvent; type Hashing = BlakeTwo256; type MaxSchemas= ConstU32<10>; type MaxCodeSize = ConstU32<25000>; + type MaxMemoryPages = ConstU32<40>; + type DefaultGasLimit = ConstU64<1000>; + type GasCost = ConstGasCosts; } @@ -282,8 +307,13 @@ impl pallet_issuers::Config for Runtime { type RuntimeEvent = RuntimeEvent; type Hashing = BlakeTwo256; + type WeightInfo = pallet_issuers::weights::SubstrateWeight; + type MaxNameLength = ConstU32<120>; type MaxControllers = ConstU32<20>; + type Currency = Balances; + + type IssuerRegistryDeposit = ConstU128<1_000_000_000_000>; } impl pallet_credentials::Config for Runtime { @@ -352,6 +382,8 @@ pub type Executive = frame_executive::Executive< >; #[cfg(feature = "runtime-benchmarks")] +#[macro_use] +extern crate frame_benchmarking; mod benches { frame_benchmarking::define_benchmarks!( [frame_benchmarking, BaselineBench::] @@ -359,14 +391,10 @@ mod benches { [pallet_balances, Balances] [pallet_timestamp, Timestamp] [pallet_sudo, Sudo] - [pallet_utility, Utility] - [pallet_credentials, CredentialsModule] [pallet_issuers, IssuersModule] - [pallet_algorithms, AlgorithmsModule] - [pallet_algorithms, AlgorithmsModule] - [pallet_utility, Utility] - [pallet_algorithms, AlgorithmsModule] + [pallet_credentials, CredentialsModule] [pallet_utility, Utility] + ); } @@ -551,7 +579,7 @@ impl_runtime_apis! { let mut list = Vec::::new(); - // list_benchmarks!(list, extra); + list_benchmarks!(list, extra); let storage_info = AllPalletsWithSystem::storage_info(); @@ -575,7 +603,9 @@ impl_runtime_apis! { let mut batches = Vec::::new(); let params = (&config, &whitelist); - // add_benchmarks!(params, batches); + add_benchmarks!(params, batches); + + // add_benchmark!(params, batches, pallet_issuers, IssuersModule); Ok(batches) }