From dbd8d33d5356ac79f0e101e81db7c04c10472884 Mon Sep 17 00:00:00 2001 From: David Salami <31099392+Wizdave97@users.noreply.github.com> Date: Mon, 22 May 2023 18:17:24 +0100 Subject: [PATCH] Benchmarking (#39) * benchmarking frameowrk * add some information * progress on benchmarks * benchmark test: not running yet * add an assertion after benchmarks * added some comments --- pallet-ismp/Cargo.toml | 8 +- pallet-ismp/src/benchmarking.rs | 272 ++++++++++++++++++++++++++++++++ pallet-ismp/src/lib.rs | 23 ++- pallet-ismp/src/mock.rs | 61 ++++++- pallet-ismp/src/weight_info.rs | 224 ++++++++++++++++++++++++++ 5 files changed, 578 insertions(+), 10 deletions(-) create mode 100644 pallet-ismp/src/benchmarking.rs create mode 100644 pallet-ismp/src/weight_info.rs diff --git a/pallet-ismp/Cargo.toml b/pallet-ismp/Cargo.toml index 23c4f00bc..48ad0c646 100644 --- a/pallet-ismp/Cargo.toml +++ b/pallet-ismp/Cargo.toml @@ -14,6 +14,7 @@ sp-runtime = { git = "https://github.com/paritytech/substrate", branch = "polkad sp-core = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.40", default-features = false } sp-std = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.40", default-features = false } sp-api = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.40", default-features = false } +pallet-timestamp = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.40", default-features = false, optional = true } # polytope labs ismp-rs = { package = "ismp", git = "ssh://git@github.com/polytope-labs/ismp-rs.git", branch = "main", default-features = false } @@ -54,4 +55,9 @@ std = [ "ismp-primitives/std", ] -runtime-benchmarks = ["frame-benchmarking/runtime-benchmarks"] +runtime-benchmarks = [ + "frame-benchmarking/runtime-benchmarks", + "pallet-timestamp/runtime-benchmarks", + "frame-system/runtime-benchmarks", + "frame-support/runtime-benchmarks" +] diff --git a/pallet-ismp/src/benchmarking.rs b/pallet-ismp/src/benchmarking.rs new file mode 100644 index 000000000..a3ff0c1da --- /dev/null +++ b/pallet-ismp/src/benchmarking.rs @@ -0,0 +1,272 @@ +// Only enable this module for benchmarking. +#![cfg(feature = "runtime-benchmarks")] + +use crate::*; +use frame_benchmarking::v2::*; +use frame_system::RawOrigin; + +// Running the benchmarks correctly +// Add the [`BenchmarkClient`] as one of the consensus clients available to pallet-ismp in the +// runtime configuration +// In your module router configuration add the [`BenchmarkIsmpModule`] as one of the ismp modules +// using the pallet id defined here as it's module id. + +// Details on using the benchmarks macro can be seen at: +// https://paritytech.github.io/substrate/master/frame_benchmarking/trait.Benchmarking.html#tymethod.benchmarks +#[benchmarks( + where + ::Hash: From, + T: pallet_timestamp::Config, + ::Moment: From +)] +pub mod benchmarks { + use super::*; + use crate::router::Receipt; + use frame_support::PalletId; + use frame_system::EventRecord; + use ismp_rs::{ + consensus::{ConsensusClient, IntermediateState, StateCommitment, StateMachineHeight}, + error::Error as IsmpError, + messaging::{Message, Proof, RequestMessage, ResponseMessage, TimeoutMessage}, + module::ISMPModule, + router::{Post, RequestResponse}, + util::hash_request, + }; + + fn assert_last_event(generic_event: ::RuntimeEvent) { + let events = frame_system::Pallet::::events(); + let system_event: ::RuntimeEvent = generic_event.into(); + let EventRecord { event, .. } = &events[events.len() - 1]; + assert_eq!(event, &system_event); + } + + #[derive(Default)] + pub struct BenchmarkClient; + + pub const BENCHMARK_CONSENSUS_CLIENT_ID: [u8; 4] = [1u8; 4]; + + impl ConsensusClient for BenchmarkClient { + fn verify_consensus( + &self, + _host: &dyn ISMPHost, + _trusted_consensus_state: Vec, + _proof: Vec, + ) -> Result<(Vec, Vec), IsmpError> { + Ok(Default::default()) + } + + fn unbonding_period(&self) -> Duration { + Duration::from_secs(60 * 60 * 60) + } + + fn verify_membership( + &self, + _host: &dyn ISMPHost, + _item: RequestResponse, + _root: StateCommitment, + _proof: &Proof, + ) -> Result<(), IsmpError> { + Ok(()) + } + + fn state_trie_key(&self, _request: RequestResponse) -> Vec> { + Default::default() + } + + fn verify_state_proof( + &self, + _host: &dyn ISMPHost, + _keys: Vec>, + _root: StateCommitment, + _proof: &Proof, + ) -> Result>>, IsmpError> { + Ok(Default::default()) + } + + fn is_frozen(&self, _trusted_consensus_state: &[u8]) -> Result<(), IsmpError> { + Ok(()) + } + } + + /// This module should be added to the module router in runtime for benchmarks to pass + pub struct BenchmarkIsmpModule; + pub const MODULE_ID: PalletId = PalletId(*b"benchmak"); + impl ISMPModule for BenchmarkIsmpModule { + fn on_accept(_request: Request) -> Result<(), IsmpError> { + Ok(()) + } + + fn on_response(_response: Response) -> Result<(), IsmpError> { + Ok(()) + } + + fn on_timeout(_request: Request) -> Result<(), IsmpError> { + Ok(()) + } + } + + fn set_timestamp() + where + ::Moment: From, + { + pallet_timestamp::Pallet::::set_timestamp(1000_000_000u64.into()); + } + + #[benchmark] + fn create_consensus_client() { + set_timestamp::(); + let intermediate_state = IntermediateState { + height: StateMachineHeight { + id: StateMachineId { + state_id: StateMachine::Polkadot(1000), + consensus_client: BENCHMARK_CONSENSUS_CLIENT_ID, + }, + height: 1, + }, + + commitment: StateCommitment { + timestamp: 1651280681, + ismp_root: None, + state_root: Default::default(), + }, + }; + + let message = CreateConsensusClient { + consensus_state: Default::default(), + consensus_client_id: BENCHMARK_CONSENSUS_CLIENT_ID, + state_machine_commitments: vec![intermediate_state], + }; + + #[extrinsic_call] + _(RawOrigin::Root, message); + + assert_last_event::( + Event::ConsensusClientCreated { consensus_client_id: BENCHMARK_CONSENSUS_CLIENT_ID } + .into(), + ); + } + + fn setup_mock_client(host: &H) -> IntermediateState { + let intermediate_state = IntermediateState { + height: StateMachineHeight { + id: StateMachineId { + state_id: StateMachine::Ethereum, + consensus_client: BENCHMARK_CONSENSUS_CLIENT_ID, + }, + height: 1, + }, + commitment: StateCommitment { + timestamp: 1000, + ismp_root: None, + state_root: Default::default(), + }, + }; + + host.store_consensus_state(BENCHMARK_CONSENSUS_CLIENT_ID, vec![]).unwrap(); + host.store_consensus_update_time(BENCHMARK_CONSENSUS_CLIENT_ID, Duration::from_secs(1000)) + .unwrap(); + host.store_state_machine_commitment( + intermediate_state.height, + intermediate_state.commitment, + ) + .unwrap(); + + intermediate_state + } + + // The Benchmark consensus client should be added to the runtime for these benchmarks to work + #[benchmark] + fn handle_request_message() { + set_timestamp::(); + let host = Host::::default(); + let intermediate_state = setup_mock_client(&host); + let post = Post { + source_chain: StateMachine::Ethereum, + dest_chain: ::StateMachine::get(), + nonce: 0, + from: MODULE_ID.0.to_vec(), + to: MODULE_ID.0.to_vec(), + timeout_timestamp: 5000, + data: vec![], + }; + + let msg = RequestMessage { + requests: vec![Request::Post(post.clone())], + proof: Proof { height: intermediate_state.height, proof: vec![] }, + }; + let caller = whitelisted_caller(); + + #[extrinsic_call] + handle(RawOrigin::Signed(caller), vec![Message::Request(msg)]); + + let commitment = hash_request::>(&Request::Post(post)); + assert!(RequestAcks::::get(commitment.0.to_vec()).is_some()); + } + + #[benchmark] + fn handle_response_message() { + set_timestamp::(); + let host = Host::::default(); + let intermediate_state = setup_mock_client(&host); + let post = Post { + source_chain: ::StateMachine::get(), + dest_chain: StateMachine::Ethereum, + nonce: 0, + from: MODULE_ID.0.to_vec(), + to: MODULE_ID.0.to_vec(), + timeout_timestamp: 5000, + data: vec![], + }; + let request = Request::Post(post.clone()); + + let commitment = hash_request::>(&request); + RequestAcks::::insert(commitment.0.to_vec(), Receipt::Ok); + + let response = Response::Post { post, response: vec![] }; + + let msg = ResponseMessage::Post { + responses: vec![response], + proof: Proof { height: intermediate_state.height, proof: vec![] }, + }; + + let caller = whitelisted_caller(); + + #[extrinsic_call] + handle(RawOrigin::Signed(caller), vec![Message::Response(msg)]); + + assert!(RequestAcks::::get(commitment.0.to_vec()).is_none()); + } + + #[benchmark] + fn handle_timeout_message() { + set_timestamp::(); + let host = Host::::default(); + let intermediate_state = setup_mock_client(&host); + let post = Post { + source_chain: ::StateMachine::get(), + dest_chain: StateMachine::Ethereum, + nonce: 0, + from: MODULE_ID.0.to_vec(), + to: MODULE_ID.0.to_vec(), + timeout_timestamp: 500, + data: vec![], + }; + let request = Request::Post(post.clone()); + + let commitment = hash_request::>(&request); + RequestAcks::::insert(commitment.0.to_vec(), Receipt::Ok); + + let msg = TimeoutMessage::Post { + requests: vec![request], + timeout_proof: Proof { height: intermediate_state.height, proof: vec![] }, + }; + let caller = whitelisted_caller(); + + #[extrinsic_call] + handle(RawOrigin::Signed(caller), vec![Message::Timeout(msg)]); + + assert!(RequestAcks::::get(commitment.0.to_vec()).is_none()); + } + + impl_benchmark_test_suite!(Pallet, crate::tests::new_test_ext(), crate::mock::Test); +} diff --git a/pallet-ismp/src/lib.rs b/pallet-ismp/src/lib.rs index fabeb0d97..9ed1d8ff3 100644 --- a/pallet-ismp/src/lib.rs +++ b/pallet-ismp/src/lib.rs @@ -18,16 +18,18 @@ extern crate alloc; +pub mod benchmarking; mod errors; pub mod events; pub mod host; mod mmr; #[cfg(test)] -mod mock; +pub mod mock; pub mod primitives; pub mod router; #[cfg(test)] -mod tests; +pub mod tests; +pub mod weight_info; pub use mmr::utils::NodesUtils; @@ -64,6 +66,7 @@ pub mod pallet { errors::HandlingError, primitives::{ConsensusClientProvider, ISMP_ID}, router::Receipt, + weight_info::{WeightInfo, WeightProvider}, }; use alloc::collections::BTreeSet; use frame_support::{pallet_prelude::*, traits::UnixTime}; @@ -77,6 +80,7 @@ pub mod pallet { router::ISMPRouter, }; use sp_core::H256; + use weight_info::get_weight; /// Our pallet's configuration trait. All our types and constants go in here. If the /// pallet is dependent on specific other pallets, then their configuration traits @@ -111,6 +115,10 @@ pub mod pallet { type IsmpRouter: ISMPRouter + Default; /// Provides concrete implementations of consensus clients type ConsensusClientProvider: ConsensusClientProvider; + /// Weight Info + type WeightInfo: WeightInfo; + /// Weight provider for consensus clients and module callbacks + type WeightProvider: WeightProvider; } // Simple declaration of the `Pallet` type. It is placeholder we use to implement traits and @@ -173,7 +181,7 @@ pub mod pallet { #[pallet::storage] #[pallet::getter(fn response_acks)] - /// Acknowledgements for receipt of responses + /// Acknowledgements for incoming and outgoing responses /// The key is the response commitment pub type ResponseAcks = StorageMap<_, Blake2_128Concat, Vec, Receipt, OptionQuery>; @@ -240,8 +248,9 @@ pub mod pallet { ::Hash: From, { /// Handles ismp messages - #[pallet::weight(0)] + #[pallet::weight(get_weight::(&messages))] #[pallet::call_index(0)] + #[frame_support::transactional] pub fn handle(origin: OriginFor, messages: Vec) -> DispatchResult { let _ = ensure_signed(origin)?; // Define a host @@ -306,7 +315,7 @@ pub mod pallet { } /// Create consensus clients - #[pallet::weight(0)] + #[pallet::weight(::WeightInfo::create_consensus_client())] #[pallet::call_index(1)] pub fn create_consensus_client( origin: OriginFor, @@ -343,7 +352,7 @@ pub mod pallet { }, /// Indicates that a consensus client has been created ConsensusClientCreated { consensus_client_id: ConsensusClientId }, - /// Response was process successfully + /// An Outgoing Response has been deposited Response { /// Chain that this response will be routed to dest_chain: StateMachine, @@ -352,7 +361,7 @@ pub mod pallet { /// Nonce for the request which this response is for request_nonce: u64, }, - /// Request processed successfully + /// An Outgoing Request has been deposited Request { /// Chain that this request will be routed to dest_chain: StateMachine, diff --git a/pallet-ismp/src/mock.rs b/pallet-ismp/src/mock.rs index 31da1b3c4..a7d0987c2 100644 --- a/pallet-ismp/src/mock.rs +++ b/pallet-ismp/src/mock.rs @@ -4,7 +4,10 @@ use crate::*; use crate::{primitives::ConsensusClientProvider, router::ProxyRouter}; use frame_support::traits::{ConstU32, ConstU64, Get}; use frame_system::EnsureRoot; -use ismp_rs::consensus::ConsensusClient; +use ismp_rs::{ + consensus::ConsensusClient, + router::{DispatchResult, DispatchSuccess}, +}; use sp_core::H256; use sp_runtime::{ testing::Header, @@ -88,6 +91,60 @@ impl Config for Test { type AdminOrigin = EnsureRoot; type StateMachine = StateMachineProvider; type TimeProvider = Timestamp; - type IsmpRouter = ProxyRouter; + type IsmpRouter = Router; type ConsensusClientProvider = ConsensusProvider; + type WeightInfo = (); + type WeightProvider = (); +} + +#[derive(Default)] +pub struct ModuleRouter; + +impl ISMPRouter for ModuleRouter { + fn dispatch(&self, request: Request) -> DispatchResult { + let dest = request.dest_chain(); + let source = request.source_chain(); + let nonce = request.nonce(); + + Ok(DispatchSuccess { dest_chain: dest, source_chain: source, nonce }) + } + + fn dispatch_timeout(&self, request: Request) -> DispatchResult { + let dest = request.dest_chain(); + let source = request.source_chain(); + let nonce = request.nonce(); + Ok(DispatchSuccess { dest_chain: dest, source_chain: source, nonce }) + } + + fn write_response(&self, response: Response) -> DispatchResult { + let request = &response.request(); + let dest = request.dest_chain(); + let source = request.source_chain(); + let nonce = request.nonce(); + Ok(DispatchSuccess { dest_chain: dest, source_chain: source, nonce }) + } +} + +pub struct Router { + inner: ProxyRouter, +} + +impl Default for Router { + fn default() -> Self { + Self { inner: ProxyRouter::::new(ModuleRouter::default()) } + } +} + +impl ISMPRouter for Router { + fn dispatch(&self, request: Request) -> DispatchResult { + self.inner.dispatch(request) + } + + fn dispatch_timeout(&self, request: Request) -> DispatchResult { + self.inner.dispatch_timeout(request) + } + + fn write_response(&self, response: Response) -> DispatchResult { + self.inner.write_response(response) + } } diff --git a/pallet-ismp/src/weight_info.rs b/pallet-ismp/src/weight_info.rs new file mode 100644 index 000000000..606bf7dcf --- /dev/null +++ b/pallet-ismp/src/weight_info.rs @@ -0,0 +1,224 @@ +//! Users of ismp should benchmark consensus clients and module callbacks + +use crate::Config; +use alloc::boxed::Box; +use frame_support::weights::Weight; +use ismp_rs::{ + consensus::ConsensusClientId, + messaging::{ConsensusMessage, Message, Proof, ResponseMessage, TimeoutMessage}, + router::{Request, Response}, +}; + +/// A trait that provides information about how consensus client execute in the runtime +pub trait ConsensusClientWeight { + /// Returns the weight that would be used in processing this consensus message + fn verify_consensus(&self, msg: ConsensusMessage) -> Weight; + /// Returns weight used in verifying this membership proof + /// `items` is the number of values being verified + /// The weight should ideally depend on the number of items being verified + fn verify_membership(&self, items: usize, proof: &Proof) -> Weight; + /// Returns weight used in verifying this state proof + /// `items` is the number of keys being verified + /// The weight should ideally depend on the number of items being verified + fn verify_state_proof(&self, items: usize, proof: &Proof) -> Weight; +} + +impl ConsensusClientWeight for () { + fn verify_consensus(&self, _msg: ConsensusMessage) -> Weight { + Weight::zero() + } + + fn verify_membership(&self, _items: usize, _proof: &Proof) -> Weight { + Weight::zero() + } + + fn verify_state_proof(&self, _items: usize, _proof: &Proof) -> Weight { + Weight::zero() + } +} + +/// A trait that provides weight information about how module callbacks execute +pub trait IsmpModuleWeight { + /// Returns the weight used in processing this request + fn on_accept(&self, request: &Request) -> Weight; + /// Returns the weight used in processing this timeout + fn on_timeout(&self, request: &Request) -> Weight; + /// Returns the weight used in processing this response + fn on_response(&self, response: &Response) -> Weight; +} + +impl IsmpModuleWeight for () { + fn on_accept(&self, _request: &Request) -> Weight { + Weight::zero() + } + + fn on_timeout(&self, _request: &Request) -> Weight { + Weight::zero() + } + + fn on_response(&self, _response: &Response) -> Weight { + Weight::zero() + } +} + +pub trait WeightProvider { + fn consensus_client(id: ConsensusClientId) -> Option>; + + fn module_callback(dest_module: &[u8]) -> Option>; +} + +impl WeightProvider for () { + fn consensus_client(_id: ConsensusClientId) -> Option> { + None + } + + fn module_callback(_dest_module: &[u8]) -> Option> { + None + } +} + +/// These functions account fot storage reads and writes in the ismp message handlers +pub trait WeightInfo { + fn create_consensus_client() -> Weight; + fn handle_request_message() -> Weight; + fn handle_response_message() -> Weight; + fn handle_timeout_message() -> Weight; +} + +impl WeightInfo for () { + fn create_consensus_client() -> Weight { + Weight::zero() + } + + fn handle_request_message() -> Weight { + Weight::zero() + } + + fn handle_response_message() -> Weight { + Weight::zero() + } + + fn handle_timeout_message() -> Weight { + Weight::zero() + } +} + +pub fn get_weight(messages: &[Message]) -> Weight { + messages.into_iter().fold(Weight::zero(), |acc, msg| { + match msg { + Message::Consensus(_) => acc + ::WeightInfo::create_consensus_client(), + Message::Request(msg) => { + let cb_weight = msg.requests.iter().fold(Weight::zero(), |acc, req| { + let dest_module = match req { + Request::Post(ref post) => post.to.as_slice(), + // Get requests are never submitted + _ => return acc, + }; + let handle = ::WeightProvider::module_callback(dest_module) + .unwrap_or(Box::new(())); + acc + handle.on_accept(&req) + }); + + let consensus_handler = ::WeightProvider::consensus_client( + msg.proof.height.id.consensus_client, + ) + .unwrap_or(Box::new(())); + + let proof_verification_weight = + consensus_handler.verify_membership(msg.requests.len(), &msg.proof); + + acc + cb_weight + + proof_verification_weight + + ::WeightInfo::handle_request_message() + } + Message::Response(msg) => match msg { + ResponseMessage::Post { responses, proof } => { + let cb_weight = responses.iter().fold(Weight::zero(), |acc, res| { + let dest_module = match res { + Response::Post { ref post, .. } => post.from.as_slice(), + _ => return acc, + }; + let handle = ::WeightProvider::module_callback(dest_module) + .unwrap_or(Box::new(())); + acc + handle.on_response(&res) + }); + + let consensus_handler = ::WeightProvider::consensus_client( + proof.height.id.consensus_client, + ) + .unwrap_or(Box::new(())); + + let proof_verification_weight = + consensus_handler.verify_membership(responses.len(), &proof); + + acc + cb_weight + + proof_verification_weight + + ::WeightInfo::handle_response_message() + } + ResponseMessage::Get { requests, proof } => { + let cb_weight = requests.iter().fold(Weight::zero(), |acc, req| { + let dest_module = match req { + Request::Get(ref get) => get.from.as_slice(), + _ => return acc, + }; + let handle = ::WeightProvider::module_callback(dest_module) + .unwrap_or(Box::new(())); + acc + handle.on_response(&Response::Get { + get: req.get_request().unwrap(), + values: Default::default(), + }) + }); + + let consensus_handler = ::WeightProvider::consensus_client( + proof.height.id.consensus_client, + ) + .unwrap_or(Box::new(())); + + let proof_verification_weight = + consensus_handler.verify_state_proof(requests.len(), &proof); + + acc + cb_weight + + proof_verification_weight + + ::WeightInfo::handle_response_message() + } + }, + Message::Timeout(msg) => match msg { + TimeoutMessage::Post { requests, timeout_proof } => { + let cb_weight = requests.iter().fold(Weight::zero(), |acc, req| { + let dest_module = match req { + Request::Post(ref post) => post.from.as_slice(), + _ => return acc, + }; + let handle = ::WeightProvider::module_callback(dest_module) + .unwrap_or(Box::new(())); + acc + handle.on_timeout(&req) + }); + + let consensus_handler = ::WeightProvider::consensus_client( + timeout_proof.height.id.consensus_client, + ) + .unwrap_or(Box::new(())); + + let proof_verification_weight = + consensus_handler.verify_state_proof(requests.len(), &timeout_proof); + + acc + cb_weight + + proof_verification_weight + + ::WeightInfo::handle_response_message() + } + TimeoutMessage::Get { requests } => { + let cb_weight = requests.iter().fold(Weight::zero(), |acc, req| { + let dest_module = match req { + Request::Get(ref get) => get.from.as_slice(), + _ => return acc, + }; + let handle = ::WeightProvider::module_callback(dest_module) + .unwrap_or(Box::new(())); + acc + handle.on_timeout(&req) + }); + acc + cb_weight + ::WeightInfo::handle_timeout_message() + } + }, + } + }) +}