From 99d0f6d76aaac6d366ae62a68983ea0878840685 Mon Sep 17 00:00:00 2001 From: Tin Chung <56880684+chungquantin@users.noreply.github.com> Date: Tue, 13 Aug 2024 23:00:33 +0700 Subject: [PATCH] refactor: generic extension crate to de-duplicate code between runtimes (#163) --- Cargo.lock | 17 + Cargo.toml | 26 +- extension/Cargo.toml | 51 ++ extension/src/lib.rs | 323 ++++++++++++ extension/src/tests.rs | 135 +++++ .../src/extensions => extension/src}/v0.rs | 4 +- runtime/devnet/Cargo.toml | 2 + runtime/devnet/src/config/api.rs | 57 ++- runtime/devnet/src/config/contracts.rs | 7 +- runtime/devnet/src/extensions/mod.rs | 480 ------------------ runtime/devnet/src/lib.rs | 1 - 11 files changed, 583 insertions(+), 520 deletions(-) create mode 100644 extension/Cargo.toml create mode 100644 extension/src/lib.rs create mode 100644 extension/src/tests.rs rename {runtime/devnet/src/extensions => extension/src}/v0.rs (98%) delete mode 100644 runtime/devnet/src/extensions/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 7f033a20..78e5e1d1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9460,6 +9460,22 @@ dependencies = [ "universal-hash", ] +[[package]] +name = "pop-chain-extension" +version = "0.1.0" +dependencies = [ + "frame-support", + "frame-system", + "log", + "pallet-contracts", + "parity-scale-codec", + "pop-primitives", + "rand", + "sp-core", + "sp-runtime", + "sp-std", +] + [[package]] name = "pop-node" version = "0.1.0-alpha" @@ -9594,6 +9610,7 @@ dependencies = [ "parity-scale-codec", "polkadot-parachain-primitives", "polkadot-runtime-common", + "pop-chain-extension", "pop-primitives", "pop-runtime-common", "rand", diff --git a/Cargo.toml b/Cargo.toml index d082e838..f037f6b7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,18 +23,19 @@ members = [ "pallets/*", "primitives", ] -exclude = [ - "pop-api", - "tests/contracts" -] +exclude = ["pop-api", "tests/contracts"] resolver = "2" [workspace.dependencies] -codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false, features = ["derive"] } +codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false, features = [ + "derive", +] } hex-literal = "0.4.1" log = { version = "0.4.20", default-features = false } -scale-info = { version = "2.10.0", default-features = false, features = ["derive"] } +scale-info = { version = "2.10.0", default-features = false, features = [ + "derive", +] } smallvec = "1.11.0" serde = "1.0.195" clap = { version = "4.4.18", features = ["derive"] } @@ -52,7 +53,8 @@ substrate-build-script-utils = "11.0.0" # Local pallet-api = { path = "pallets/api", default-features = false } -pop-runtime-devnet = { path = "runtime/devnet", default-features = true } # default-features=true required for `-p pop-node` builds +pop-chain-extension = { path = "./extension", default-features = false } +pop-runtime-devnet = { path = "runtime/devnet", default-features = true } # default-features=true required for `-p pop-node` builds pop-runtime-testnet = { path = "runtime/testnet", default-features = true } # default-features=true required for `-p pop-node` builds pop-runtime-common = { path = "runtime/common", default-features = false } pop-primitives = { path = "./primitives", default-features = false } @@ -82,7 +84,9 @@ frame-system = { version = "29.0.0", default-features = false } frame-system-benchmarking = { version = "29.0.0", default-features = false } frame-system-rpc-runtime-api = { version = "27.0.0", default-features = false } frame-try-runtime = { version = "0.35.0", default-features = false } -pallet-aura = { version = "28.0.0", default-features = false, features = ["experimental"] } +pallet-aura = { version = "28.0.0", default-features = false, features = [ + "experimental", +] } pallet-authorship = { version = "29.0.0", default-features = false } pallet-assets = { version = "30.0.0", default-features = false } pallet-balances = { version = "29.0.2", default-features = false } @@ -139,7 +143,9 @@ xcm-executor = { package = "staging-xcm-executor", version = "8.0.2", default-fe # Cumulus asset-test-utils = { version = "8.0.1", default-features = false } cumulus-pallet-aura-ext = { version = "0.8.0", default-features = false } -cumulus-pallet-parachain-system = { version = "0.8.1", default-features = false, features = ["parameterized-consensus-hook"] } +cumulus-pallet-parachain-system = { version = "0.8.1", default-features = false, features = [ + "parameterized-consensus-hook", +] } cumulus-pallet-session-benchmarking = { version = "10.0.0", default-features = false } cumulus-pallet-xcm = { version = "0.8.0", default-features = false } cumulus-pallet-xcmp-queue = { version = "0.8.0", default-features = false } @@ -163,4 +169,4 @@ cumulus-client-service = "0.8.0" # Paseo asset-hub-paseo-runtime = { git = "https://github.com/paseo-network/runtimes/", tag = "v1.2.5-system-chains", default-features = false } paseo-runtime = { git = "https://github.com/paseo-network/runtimes/", tag = "v1.2.5-system-chains", default-features = false } -paseo-runtime-constants = { git = "https://github.com/paseo-network/runtimes/", tag = "v1.2.5-system-chains", default-features = false } \ No newline at end of file +paseo-runtime-constants = { git = "https://github.com/paseo-network/runtimes/", tag = "v1.2.5-system-chains", default-features = false } diff --git a/extension/Cargo.toml b/extension/Cargo.toml new file mode 100644 index 00000000..03b4221a --- /dev/null +++ b/extension/Cargo.toml @@ -0,0 +1,51 @@ +[package] +name = "pop-chain-extension" +version = "0.1.0" +authors.workspace = true +description.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +edition.workspace = true +publish = false + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +codec.workspace = true +log.workspace = true + +# Local +pop-primitives.workspace = true + +# Substrate +frame-support.workspace = true +frame-system.workspace = true +pallet-contracts.workspace = true +sp-core.workspace = true +sp-runtime.workspace = true +sp-std.workspace = true + +[dev-dependencies] +rand = "0.8.5" + +[features] +default = ["std"] +std = [ + "log/std", + "codec/std", + "frame-support/std", + "frame-system/std", + "pallet-contracts/std", + "pop-primitives/std", + "sp-runtime/std", + "sp-core/std", + "sp-std/std", +] +runtime-benchmarks = [ + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", + "pallet-contracts/runtime-benchmarks", + "sp-runtime/runtime-benchmarks", +] diff --git a/extension/src/lib.rs b/extension/src/lib.rs new file mode 100644 index 00000000..582f346c --- /dev/null +++ b/extension/src/lib.rs @@ -0,0 +1,323 @@ +#![cfg_attr(not(feature = "std"), no_std)] + +#[cfg(test)] +mod tests; +mod v0; + +use codec::Encode; +use frame_support::{ + dispatch::{GetDispatchInfo, PostDispatchInfo}, + pallet_prelude::*, + traits::OriginTrait, +}; +use frame_system::RawOrigin; +use pallet_contracts::chain_extension::{ + BufInBufOutState, ChainExtension, Environment, Ext, InitState, RetVal, +}; +use sp_core::crypto::UncheckedFrom; +use sp_runtime::{traits::Dispatchable, DispatchError}; +use sp_std::vec::Vec; + +/// Logging target for categorizing messages from the Pop API extension module. +const LOG_TARGET: &str = "pop-api::extension"; + +const DECODING_FAILED_ERROR: DispatchError = DispatchError::Other("DecodingFailed"); +// TODO: issue #93, we can also encode the `pop_primitives::Error::UnknownCall` which means we do use +// `Error` in the runtime and it should stay in primitives. Perhaps issue #91 will also influence +// here. Should be looked at together. +const DECODING_FAILED_ERROR_ENCODED: [u8; 4] = [255u8, 0, 0, 0]; +const UNKNOWN_CALL_ERROR: DispatchError = DispatchError::Other("UnknownCall"); +// TODO: see above. +const UNKNOWN_CALL_ERROR_ENCODED: [u8; 4] = [254u8, 0, 0, 0]; + +type ContractSchedule = ::Schedule; + +/// Type of the state reader. +pub trait ReadState { + /// Query of the state read operations. + type StateQuery: Decode; + + /// Check if a state query is allowed. + fn contains(c: &Self::StateQuery) -> bool; + + /// Reads state using the provided query, returning the result as a byte vector. + fn read(read: Self::StateQuery) -> Vec; + + /// Decodes parameters into state query. + fn decode(params: &mut &[u8]) -> Result { + decode_checked(params) + } +} + +/// Type of the dispatch call filter. +pub trait CallFilter { + /// Query of the dispatch calls operations. + type Call: Decode; + + /// Check if runtime call is allowed. + fn contains(t: &Self::Call) -> bool; +} + +/// Pop API chain extension. +#[derive(Default)] +pub struct ApiExtension(PhantomData); + +impl ChainExtension for ApiExtension +where + T: pallet_contracts::Config + + frame_system::Config< + RuntimeCall: GetDispatchInfo + Dispatchable, + >, + T::AccountId: UncheckedFrom + AsRef<[u8]>, + // Bound the type by the two traits which need to be implemented by the runtime. + I: ReadState + CallFilter::RuntimeCall> + 'static, +{ + fn call>( + &mut self, + env: Environment, + ) -> Result { + log::debug!(target:LOG_TARGET, " extension called "); + let mut env = env.buf_in_buf_out(); + // Charge weight for making a call from a contract to the runtime. + // `debug_message` weight is a good approximation of the additional overhead of going + // from contract layer to substrate layer. + // reference: https://github.com/paritytech/ink-examples/blob/b8d2caa52cf4691e0ddd7c919e4462311deb5ad0/psp22-extension/runtime/psp22-extension-example.rs#L236 + let contract_host_weight = ContractSchedule::::get().host_fn_weights; + env.charge_weight(contract_host_weight.debug_message)?; + + let (version, function_id, pallet_index, call_index) = extract_env(&env); + + let result = match FuncId::try_from(function_id) { + // Read encoded parameters from buffer and calculate weight for reading `len` bytes`. + Ok(function_id) => { + // reference: https://github.com/paritytech/polkadot-sdk/blob/117a9433dac88d5ac00c058c9b39c511d47749d2/substrate/frame/contracts/src/wasm/runtime.rs#L267 + let len = env.in_len(); + env.charge_weight(contract_host_weight.return_per_byte.saturating_mul(len.into()))?; + let params = env.read(len)?; + match function_id { + FuncId::Dispatch => { + dispatch::(&mut env, version, pallet_index, call_index, params) + }, + FuncId::ReadState => { + read_state::(&mut env, version, pallet_index, call_index, params) + }, + } + }, + Err(e) => Err(e), + }; + + match result { + Ok(_) => Ok(RetVal::Converging(0)), + Err(e) => Ok(RetVal::Converging(convert_to_status_code(e, version))), + } + } +} + +/// Extract (version, function_id, pallet_index, call_index) from the payload bytes. +fn extract_env>(env: &Environment) -> (u8, u8, u8, u8) { + // Extract version and function_id from first two bytes. + let (version, function_id) = { + let bytes = env.func_id().to_le_bytes(); + (bytes[0], bytes[1]) + }; + // Extract pallet index and call / key index from last two bytes. + let (pallet_index, call_index) = { + let bytes = env.ext_id().to_le_bytes(); + (bytes[0], bytes[1]) + }; + + (version, function_id, pallet_index, call_index) +} + +fn read_state, StateReader: ReadState>( + env: &mut Environment, + version: u8, + pallet_index: u8, + call_index: u8, + mut params: Vec, +) -> Result<(), DispatchError> { + const LOG_PREFIX: &str = " read_state |"; + + // Prefix params with version, pallet, index to simplify decoding. + params.insert(0, version); + params.insert(1, pallet_index); + params.insert(2, call_index); + let (mut encoded_version, mut encoded_read) = (¶ms[..1], ¶ms[1..]); + let version = decode_checked::(&mut encoded_version)?; + + // Charge weight for doing one storage read. + env.charge_weight(T::DbWeight::get().reads(1_u64))?; + let result = match version { + VersionedStateRead::V0 => { + let read = StateReader::decode(&mut encoded_read)?; + ensure!(StateReader::contains(&read), UNKNOWN_CALL_ERROR); + StateReader::read(read) + }, + }; + log::trace!( + target:LOG_TARGET, + "{} result: {:?}.", LOG_PREFIX, result + ); + env.write(&result, false, None) +} + +fn dispatch( + env: &mut Environment, + version: u8, + pallet_index: u8, + call_index: u8, + mut params: Vec, +) -> Result<(), DispatchError> +where + T: frame_system::Config< + RuntimeCall: GetDispatchInfo + Dispatchable, + >, + E: Ext, + Filter: CallFilter::RuntimeCall> + 'static, +{ + const LOG_PREFIX: &str = " dispatch |"; + + // Prefix params with version, pallet, index to simplify decoding. + params.insert(0, version); + params.insert(1, pallet_index); + params.insert(2, call_index); + let call = decode_checked::>(&mut ¶ms[..])?; + // Contract is the origin by default. + let origin: T::RuntimeOrigin = RawOrigin::Signed(env.ext().address().clone()).into(); + match call { + VersionedDispatch::V0(call) => dispatch_call::(env, call, origin, LOG_PREFIX), + } +} + +/// Helper method to decode the byte data to a provided type and throws error if failed. +fn decode_checked(params: &mut &[u8]) -> Result { + T::decode(params).map_err(|_| DECODING_FAILED_ERROR) +} + +fn dispatch_call( + env: &mut Environment, + call: T::RuntimeCall, + mut origin: T::RuntimeOrigin, + log_prefix: &str, +) -> Result<(), DispatchError> +where + T: frame_system::Config< + RuntimeCall: GetDispatchInfo + Dispatchable, + >, + E: Ext, + Filter: CallFilter::RuntimeCall> + 'static, +{ + let charged_dispatch_weight = env.charge_weight(call.get_dispatch_info().weight)?; + log::debug!(target:LOG_TARGET, "{} Inputted RuntimeCall: {:?}", log_prefix, call); + origin.add_filter(Filter::contains); + match call.dispatch(origin) { + Ok(info) => { + log::debug!(target:LOG_TARGET, "{} success, actual weight: {:?}", log_prefix, info.actual_weight); + // Refund weight if the actual weight is less than the charged weight. + if let Some(actual_weight) = info.actual_weight { + env.adjust_weight(charged_dispatch_weight, actual_weight); + } + Ok(()) + }, + Err(err) => { + log::debug!(target:LOG_TARGET, "{} failed: error: {:?}", log_prefix, err.error); + Err(err.error) + }, + } +} + +/// Wrapper to enable versioning of runtime state reads. +#[derive(Decode, Debug)] +enum VersionedStateRead { + /// Version zero of state reads. + #[codec(index = 0)] + V0, +} + +/// Wrapper to enable versioning of runtime calls. +#[derive(Decode, Debug)] +enum VersionedDispatch { + /// Version zero of dispatch calls. + #[codec(index = 0)] + V0(RuntimeCall), +} + +/// Function identifiers used in the Pop API. +/// +/// The `FuncId` specifies the available functions that can be called through the Pop API. Each +/// variant corresponds to a specific functionality provided by the API, facilitating the +/// interaction between smart contracts and the runtime. +#[derive(Debug)] +pub enum FuncId { + /// Represents a function call to dispatch a runtime call. + Dispatch, + /// Represents a function call to read the state from the runtime. + ReadState, +} + +impl TryFrom for FuncId { + type Error = DispatchError; + + /// Attempts to convert a `u8` value to its corresponding `FuncId` variant. + /// + /// If the `u8` value does not match any known function identifier, it returns a + /// `DispatchError::Other` indicating an unknown function ID. + fn try_from(func_id: u8) -> Result { + let id = match func_id { + 0 => Self::Dispatch, + 1 => Self::ReadState, + _ => { + return Err(UNKNOWN_CALL_ERROR); + }, + }; + Ok(id) + } +} + +/// Converts a `DispatchError` to a `u32` status code based on the version of the API the contract uses. +/// The contract calling the chain extension can convert the status code to the descriptive `Error`. +/// +/// For `Error` see `pop_primitives::::error::Error`. +/// +/// The error encoding can vary per version, allowing for flexible and backward-compatible error handling. +/// As a result, contracts maintain compatibility across different versions of the runtime. +/// +/// # Parameters +/// +/// - `error`: The `DispatchError` encountered during contract execution. +/// - `version`: The version of the chain extension, used to determine the known errors. +pub(crate) fn convert_to_status_code(error: DispatchError, version: u8) -> u32 { + let mut encoded_error: [u8; 4] = match error { + // "UnknownCall" and "DecodingFailed" are mapped to specific errors in the API and will + // never change. + UNKNOWN_CALL_ERROR => UNKNOWN_CALL_ERROR_ENCODED, + DECODING_FAILED_ERROR => DECODING_FAILED_ERROR_ENCODED, + _ => { + let mut encoded_error = error.encode(); + // Resize the encoded value to 4 bytes in order to decode the value in a u32 (4 bytes). + encoded_error.resize(4, 0); + encoded_error.try_into().expect("qed, resized to 4 bytes line above") + }, + }; + match version { + // If an unknown variant of the `DispatchError` is detected the error needs to be converted + // into the encoded value of `Error::Other`. This conversion is performed by shifting the bytes one + // position forward (discarding the last byte as it is not used) and setting the first byte to the + // encoded value of `Other` (0u8). This ensures the error is correctly categorized as an `Other` + // variant which provides all the necessary information to debug which error occurred in the runtime. + // + // Byte layout explanation: + // - Byte 0: index of the variant within `Error` + // - Byte 1: + // - Must be zero for `UNIT_ERRORS`. + // - Represents the nested error in `SINGLE_NESTED_ERRORS`. + // - Represents the first level of nesting in `DOUBLE_NESTED_ERRORS`. + // - Byte 2: + // - Represents the second level of nesting in `DOUBLE_NESTED_ERRORS`. + // - Byte 3: + // - Unused or represents further nested information. + 0 => v0::handle_unknown_error(&mut encoded_error), + _ => encoded_error = UNKNOWN_CALL_ERROR_ENCODED, + } + u32::from_le_bytes(encoded_error) +} diff --git a/extension/src/tests.rs b/extension/src/tests.rs new file mode 100644 index 00000000..a47feef9 --- /dev/null +++ b/extension/src/tests.rs @@ -0,0 +1,135 @@ +use codec::{Decode, Encode}; + +// Test ensuring `func_id()` and `ext_id()` work as expected, i.e. extracting the first two +// bytes and the last two bytes, respectively, from a 4 byte array. +#[test] +fn test_byte_extraction() { + use rand::Rng; + + // Helper functions + fn func_id(id: u32) -> u16 { + (id & 0x0000FFFF) as u16 + } + fn ext_id(id: u32) -> u16 { + (id >> 16) as u16 + } + + // Number of test iterations + let test_iterations = 1_000_000; + + // Create a random number generator + let mut rng = rand::thread_rng(); + + // Run the test for a large number of random 4-byte arrays + for _ in 0..test_iterations { + // Generate a random 4-byte array + let bytes: [u8; 4] = rng.gen(); + + // Convert the 4-byte array to a u32 value + let value = u32::from_le_bytes(bytes); + + // Extract the first two bytes (least significant 2 bytes) + let first_two_bytes = func_id(value); + + // Extract the last two bytes (most significant 2 bytes) + let last_two_bytes = ext_id(value); + + // Check if the first two bytes match the expected value + assert_eq!([bytes[0], bytes[1]], first_two_bytes.to_le_bytes()); + + // Check if the last two bytes match the expected value + assert_eq!([bytes[2], bytes[3]], last_two_bytes.to_le_bytes()); + } +} + +// Test showing all the different type of variants and its encoding. +#[test] +fn encoding_of_enum() { + #[derive(Debug, PartialEq, Encode, Decode)] + enum ComprehensiveEnum { + SimpleVariant, + DataVariant(u8), + NamedFields { w: u8 }, + NestedEnum(InnerEnum), + OptionVariant(Option), + VecVariant(Vec), + TupleVariant(u8, u8), + NestedStructVariant(NestedStruct), + NestedEnumStructVariant(NestedEnumStruct), + } + + #[derive(Debug, PartialEq, Encode, Decode)] + enum InnerEnum { + A, + B { inner_data: u8 }, + C(u8), + } + + #[derive(Debug, PartialEq, Encode, Decode)] + struct NestedStruct { + x: u8, + y: u8, + } + + #[derive(Debug, PartialEq, Encode, Decode)] + struct NestedEnumStruct { + inner_enum: InnerEnum, + } + + // Creating each possible variant for an enum. + let enum_simple = ComprehensiveEnum::SimpleVariant; + let enum_data = ComprehensiveEnum::DataVariant(42); + let enum_named = ComprehensiveEnum::NamedFields { w: 42 }; + let enum_nested = ComprehensiveEnum::NestedEnum(InnerEnum::B { inner_data: 42 }); + let enum_option = ComprehensiveEnum::OptionVariant(Some(42)); + let enum_vec = ComprehensiveEnum::VecVariant(vec![1, 2, 3, 4, 5]); + let enum_tuple = ComprehensiveEnum::TupleVariant(42, 42); + let enum_nested_struct = ComprehensiveEnum::NestedStructVariant(NestedStruct { x: 42, y: 42 }); + let enum_nested_enum_struct = ComprehensiveEnum::NestedEnumStructVariant(NestedEnumStruct { + inner_enum: InnerEnum::C(42), + }); + + // Encode and print each variant individually to see their encoded values. + println!("{:?} -> {:?}", enum_simple, enum_simple.encode()); + println!("{:?} -> {:?}", enum_data, enum_data.encode()); + println!("{:?} -> {:?}", enum_named, enum_named.encode()); + println!("{:?} -> {:?}", enum_nested, enum_nested.encode()); + println!("{:?} -> {:?}", enum_option, enum_option.encode()); + println!("{:?} -> {:?}", enum_vec, enum_vec.encode()); + println!("{:?} -> {:?}", enum_tuple, enum_tuple.encode()); + println!("{:?} -> {:?}", enum_nested_struct, enum_nested_struct.encode()); + println!("{:?} -> {:?}", enum_nested_enum_struct, enum_nested_enum_struct.encode()); +} + +#[test] +fn encoding_decoding_dispatch_error() { + use sp_runtime::{ArithmeticError, DispatchError, ModuleError, TokenError}; + + let error = DispatchError::Module(ModuleError { + index: 255, + error: [2, 0, 0, 0], + message: Some("error message"), + }); + let encoded = error.encode(); + let decoded = DispatchError::decode(&mut &encoded[..]).unwrap(); + assert_eq!(encoded, vec![3, 255, 2, 0, 0, 0]); + assert_eq!( + decoded, + // `message` is skipped for encoding. + DispatchError::Module(ModuleError { index: 255, error: [2, 0, 0, 0], message: None }) + ); + + // Example DispatchError::Token + let error = DispatchError::Token(TokenError::UnknownAsset); + let encoded = error.encode(); + let decoded = DispatchError::decode(&mut &encoded[..]).unwrap(); + assert_eq!(encoded, vec![7, 4]); + assert_eq!(decoded, error); + + // Example DispatchError::Arithmetic + let error = DispatchError::Arithmetic(ArithmeticError::Overflow); + let encoded = error.encode(); + let decoded = DispatchError::decode(&mut &encoded[..]).unwrap(); + assert_eq!(encoded, vec![8, 1]); + assert_eq!(decoded, error); +} diff --git a/runtime/devnet/src/extensions/v0.rs b/extension/src/v0.rs similarity index 98% rename from runtime/devnet/src/extensions/v0.rs rename to extension/src/v0.rs index 72760323..4c3536a4 100644 --- a/runtime/devnet/src/extensions/v0.rs +++ b/extension/src/v0.rs @@ -1,5 +1,5 @@ #[cfg(test)] -use crate::extensions::convert_to_status_code; +use crate::convert_to_status_code; pub(crate) fn handle_unknown_error(encoded_error: &mut [u8; 4]) { let unknown = match encoded_error[0] { @@ -106,7 +106,7 @@ mod tests { (DispatchError::RootNotAllowed, RootNotAllowed), ]; for (dispatch_error, expected) in test_cases { - let status_code = crate::extensions::convert_to_status_code(dispatch_error, 0); + let status_code = crate::convert_to_status_code(dispatch_error, 0); let error: Error = status_code.into(); assert_eq!(error, expected); } diff --git a/runtime/devnet/Cargo.toml b/runtime/devnet/Cargo.toml index 87dce79e..995af269 100644 --- a/runtime/devnet/Cargo.toml +++ b/runtime/devnet/Cargo.toml @@ -22,6 +22,7 @@ scale-info.workspace = true smallvec.workspace = true # Local +pop-chain-extension.workspace = true pop-primitives.workspace = true pop-runtime-common.workspace = true pallet-api.workspace = true @@ -139,6 +140,7 @@ std = [ "parachains-common/std", "polkadot-parachain-primitives/std", "polkadot-runtime-common/std", + "pop-chain-extension/std", "pop-primitives/std", "scale-info/std", "sp-api/std", diff --git a/runtime/devnet/src/config/api.rs b/runtime/devnet/src/config/api.rs index bdff6bbb..5f234cd4 100644 --- a/runtime/devnet/src/config/api.rs +++ b/runtime/devnet/src/config/api.rs @@ -1,22 +1,47 @@ use crate::{config::assets::TrustBackedAssetsInstance, fungibles, Runtime, RuntimeCall}; use codec::{Decode, Encode, MaxEncodedLen}; -use frame_support::traits::Contains; +use pop_chain_extension::{CallFilter, ReadState}; +use sp_std::vec::Vec; /// A query of runtime state. #[derive(Encode, Decode, Debug, MaxEncodedLen)] #[repr(u8)] -pub enum RuntimeRead { +pub enum RuntimeRead { /// Fungible token queries. #[codec(index = 150)] - Fungibles(fungibles::Read), + Fungibles(fungibles::Read), } -/// A type to identify allowed calls to the Runtime from the API. -pub struct AllowedApiCalls; +/// A struct that implement requirements for the Pop API chain extension. +#[derive(Default)] +pub struct Extension; +impl ReadState for Extension { + type StateQuery = RuntimeRead; -impl Contains for AllowedApiCalls { - /// Allowed runtime calls from the API. - fn contains(c: &RuntimeCall) -> bool { + fn contains(c: &Self::StateQuery) -> bool { + use fungibles::Read::*; + matches!( + c, + RuntimeRead::Fungibles( + TotalSupply(..) + | BalanceOf { .. } | Allowance { .. } + | TokenName(..) | TokenSymbol(..) + | TokenDecimals(..) | AssetExists(..) + ) + ) + } + + fn read(read: RuntimeRead) -> Vec { + match read { + RuntimeRead::Fungibles(key) => fungibles::Pallet::read_state(key), + } + } +} + +impl CallFilter for Extension { + type Call = RuntimeCall; + + fn contains(c: &Self::Call) -> bool { use fungibles::Call::*; matches!( c, @@ -34,22 +59,6 @@ impl Contains for AllowedApiCalls { } } -impl Contains> for AllowedApiCalls { - /// Allowed state queries from the API. - fn contains(c: &RuntimeRead) -> bool { - use fungibles::Read::*; - matches!( - c, - RuntimeRead::Fungibles( - TotalSupply(..) - | BalanceOf { .. } | Allowance { .. } - | TokenName(..) | TokenSymbol(..) - | TokenDecimals(..) | AssetExists(..) - ) - ) - } -} - impl fungibles::Config for Runtime { type AssetsInstance = TrustBackedAssetsInstance; type WeightInfo = fungibles::weights::SubstrateWeight; diff --git a/runtime/devnet/src/config/contracts.rs b/runtime/devnet/src/config/contracts.rs index 36d62f7f..5dc613b5 100644 --- a/runtime/devnet/src/config/contracts.rs +++ b/runtime/devnet/src/config/contracts.rs @@ -1,6 +1,7 @@ +use super::api::Extension; use crate::{ - deposit, extensions, Balance, Balances, BalancesCall, Perbill, Runtime, RuntimeCall, - RuntimeEvent, RuntimeHoldReason, Timestamp, + deposit, Balance, Balances, BalancesCall, Perbill, Runtime, RuntimeCall, RuntimeEvent, + RuntimeHoldReason, Timestamp, }; use frame_support::{ parameter_types, @@ -63,7 +64,7 @@ impl pallet_contracts::Config for Runtime { type CallStack = [pallet_contracts::Frame; 23]; type WeightPrice = pallet_transaction_payment::Pallet; type WeightInfo = pallet_contracts::weights::SubstrateWeight; - type ChainExtension = extensions::PopApiExtension; + type ChainExtension = pop_chain_extension::ApiExtension; type Schedule = Schedule; type AddressGenerator = pallet_contracts::DefaultAddressGenerator; // This node is geared towards development and testing of contracts. diff --git a/runtime/devnet/src/extensions/mod.rs b/runtime/devnet/src/extensions/mod.rs deleted file mode 100644 index d3bbdd0b..00000000 --- a/runtime/devnet/src/extensions/mod.rs +++ /dev/null @@ -1,480 +0,0 @@ -mod v0; - -use crate::{ - config::{ - api::{AllowedApiCalls, RuntimeRead}, - assets::TrustBackedAssetsInstance, - }, - fungibles::{self}, - AccountId, RuntimeCall, RuntimeOrigin, -}; -use codec::{Decode, Encode}; -use frame_support::{ - dispatch::{GetDispatchInfo, RawOrigin}, - pallet_prelude::*, - traits::{Contains, OriginTrait}, -}; -use pallet_contracts::chain_extension::{ - BufInBufOutState, ChainExtension, Environment, Ext, InitState, RetVal, -}; -use pop_primitives::AssetId; -use sp_core::crypto::UncheckedFrom; -use sp_runtime::{traits::Dispatchable, DispatchError}; -use sp_std::vec::Vec; - -const LOG_TARGET: &str = "pop-api::extension"; -const DECODING_FAILED_ERROR: DispatchError = DispatchError::Other("DecodingFailed"); -// TODO: issue #93, we can also encode the `pop_primitives::Error::UnknownCall` which means we do use -// `Error` in the runtime and it should stay in primitives. Perhaps issue #91 will also influence -// here. Should be looked at together. -const DECODING_FAILED_ERROR_ENCODED: [u8; 4] = [255u8, 0, 0, 0]; -const UNKNOWN_CALL_ERROR: DispatchError = DispatchError::Other("UnknownCall"); -// TODO: see above. -const UNKNOWN_CALL_ERROR_ENCODED: [u8; 4] = [254u8, 0, 0, 0]; - -type ContractSchedule = ::Schedule; - -#[derive(Default)] -pub struct PopApiExtension; - -impl ChainExtension for PopApiExtension -where - T: pallet_contracts::Config - + pallet_assets::Config - + fungibles::Config - + frame_system::Config< - RuntimeOrigin = RuntimeOrigin, - AccountId = AccountId, - RuntimeCall = RuntimeCall, - >, - T::AccountId: UncheckedFrom + AsRef<[u8]>, -{ - fn call(&mut self, env: Environment) -> Result - where - E: Ext, - { - log::debug!(target:LOG_TARGET, " extension called "); - let mut env = env.buf_in_buf_out(); - // Charge weight for making a call from a contract to the runtime. - // `debug_message` weight is a good approximation of the additional overhead of going - // from contract layer to substrate layer. - // reference: https://github.com/paritytech/ink-examples/blob/b8d2caa52cf4691e0ddd7c919e4462311deb5ad0/psp22-extension/runtime/psp22-extension-example.rs#L236 - let contract_host_weight = ContractSchedule::::get().host_fn_weights; - env.charge_weight(contract_host_weight.debug_message)?; - - // Extract version and function_id from first two bytes. - let (version, function_id) = { - let bytes = env.func_id().to_le_bytes(); - (bytes[0], bytes[1]) - }; - // Extract pallet index and call / key index from last two bytes. - let (pallet_index, call_index) = { - let bytes = env.ext_id().to_le_bytes(); - (bytes[0], bytes[1]) - }; - - let result = match FuncId::try_from(function_id) { - Ok(function_id) => { - // Read encoded parameters from buffer and calculate weight for reading `len` bytes`. - // reference: https://github.com/paritytech/polkadot-sdk/blob/117a9433dac88d5ac00c058c9b39c511d47749d2/substrate/frame/contracts/src/wasm/runtime.rs#L267 - let len = env.in_len(); - env.charge_weight(contract_host_weight.return_per_byte.saturating_mul(len.into()))?; - let params = env.read(len)?; - log::debug!(target: LOG_TARGET, "Read input successfully"); - match function_id { - FuncId::Dispatch => { - dispatch::(&mut env, version, pallet_index, call_index, params) - }, - FuncId::ReadState => { - read_state::(&mut env, version, pallet_index, call_index, params) - }, - } - }, - Err(e) => Err(e), - }; - - match result { - Ok(_) => Ok(RetVal::Converging(0)), - Err(e) => Ok(RetVal::Converging(convert_to_status_code(e, version))), - } - } -} - -fn dispatch( - env: &mut Environment, - version: u8, - pallet_index: u8, - call_index: u8, - mut params: Vec, -) -> Result<(), DispatchError> -where - T: frame_system::Config, - RuntimeOrigin: From>, - E: Ext, -{ - const LOG_PREFIX: &str = " dispatch |"; - - // Prefix params with version, pallet, index to simplify decoding. - params.insert(0, version); - params.insert(1, pallet_index); - params.insert(2, call_index); - let call = ::decode(&mut ¶ms[..]).map_err(|_| DECODING_FAILED_ERROR)?; - - // Contract is the origin by default. - let origin: RuntimeOrigin = RawOrigin::Signed(env.ext().address().clone()).into(); - match call { - VersionedDispatch::V0(call) => dispatch_call::(env, call, origin, LOG_PREFIX), - } -} - -fn dispatch_call( - env: &mut Environment, - call: RuntimeCall, - mut origin: RuntimeOrigin, - log_prefix: &str, -) -> Result<(), DispatchError> -where - T: frame_system::Config, - RuntimeOrigin: From>, - E: Ext, -{ - let charged_dispatch_weight = env.charge_weight(call.get_dispatch_info().weight)?; - log::debug!(target:LOG_TARGET, "{} Inputted RuntimeCall: {:?}", log_prefix, call); - origin.add_filter(AllowedApiCalls::contains); - match call.dispatch(origin) { - Ok(info) => { - log::debug!(target:LOG_TARGET, "{} success, actual weight: {:?}", log_prefix, info.actual_weight); - // Refund weight if the actual weight is less than the charged weight. - if let Some(actual_weight) = info.actual_weight { - env.adjust_weight(charged_dispatch_weight, actual_weight); - } - Ok(()) - }, - Err(err) => { - log::debug!(target:LOG_TARGET, "{} failed: error: {:?}", log_prefix, err.error); - Err(err.error) - }, - } -} - -fn read_state( - env: &mut Environment, - version: u8, - pallet_index: u8, - call_index: u8, - mut params: Vec, -) -> Result<(), DispatchError> -where - T: pallet_contracts::Config - + pallet_assets::Config - + fungibles::Config - + frame_system::Config, - E: Ext, -{ - const LOG_PREFIX: &str = " read_state |"; - - // Prefix params with version, pallet, index to simplify decoding, and decode parameters for - // reading state. - params.insert(0, version); - params.insert(1, pallet_index); - params.insert(2, call_index); - let read = - >::decode(&mut ¶ms[..]).map_err(|_| DECODING_FAILED_ERROR)?; - - // Charge weight for doing one storage read. - env.charge_weight(T::DbWeight::get().reads(1_u64))?; - let result = match read { - VersionedStateRead::V0(read) => { - ensure!(AllowedApiCalls::contains(&read), UNKNOWN_CALL_ERROR); - match read { - RuntimeRead::Fungibles(key) => fungibles::Pallet::::read_state(key), - } - }, - }; - log::trace!( - target:LOG_TARGET, - "{} result: {:?}.", LOG_PREFIX, result - ); - env.write(&result, false, None) -} - -/// Wrapper to enable versioning of runtime state reads. -#[derive(Decode, Debug)] -enum VersionedStateRead { - /// Version zero of state reads. - #[codec(index = 0)] - V0(RuntimeRead), -} - -/// Wrapper to enable versioning of runtime calls. -#[derive(Decode, Debug)] -enum VersionedDispatch { - /// Version zero of dispatch calls. - #[codec(index = 0)] - V0(RuntimeCall), -} - -// Converts a `DispatchError` to a `u32` status code based on the version of the API the contract uses. -// The contract calling the chain extension can convert the status code to the descriptive `Error`. -// -// For `Error` see `pop_primitives::::error::Error`. -// -// The error encoding can vary per version, allowing for flexible and backward-compatible error handling. -// As a result, contracts maintain compatibility across different versions of the runtime. -// -// # Parameters -// -// - `error`: The `DispatchError` encountered during contract execution. -// - `version`: The version of the chain extension, used to determine the known errors. -pub(crate) fn convert_to_status_code(error: DispatchError, version: u8) -> u32 { - let mut encoded_error: [u8; 4] = match error { - // "UnknownCall" and "DecodingFailed" are mapped to specific errors in the API and will - // never change. - UNKNOWN_CALL_ERROR => UNKNOWN_CALL_ERROR_ENCODED, - DECODING_FAILED_ERROR => DECODING_FAILED_ERROR_ENCODED, - _ => { - let mut encoded_error = error.encode(); - // Resize the encoded value to 4 bytes in order to decode the value in a u32 (4 bytes). - encoded_error.resize(4, 0); - encoded_error.try_into().expect("qed, resized to 4 bytes line above") - }, - }; - match version { - // If an unknown variant of the `DispatchError` is detected the error needs to be converted - // into the encoded value of `Error::Other`. This conversion is performed by shifting the bytes one - // position forward (discarding the last byte as it is not used) and setting the first byte to the - // encoded value of `Other` (0u8). This ensures the error is correctly categorized as an `Other` - // variant which provides all the necessary information to debug which error occurred in the runtime. - // - // Byte layout explanation: - // - Byte 0: index of the variant within `Error` - // - Byte 1: - // - Must be zero for `UNIT_ERRORS`. - // - Represents the nested error in `SINGLE_NESTED_ERRORS`. - // - Represents the first level of nesting in `DOUBLE_NESTED_ERRORS`. - // - Byte 2: - // - Represents the second level of nesting in `DOUBLE_NESTED_ERRORS`. - // - Byte 3: - // - Unused or represents further nested information. - 0 => v0::handle_unknown_error(&mut encoded_error), - _ => encoded_error = UNKNOWN_CALL_ERROR_ENCODED, - } - u32::from_le_bytes(encoded_error) -} - -/// Function identifiers used in the Pop API. -/// -/// The `FuncId` specifies the available functions that can be called through the Pop API. Each -/// variant corresponds to a specific functionality provided by the API, facilitating the -/// interaction between smart contracts and the runtime. -/// -/// - `Dispatch`: Represents a function call to dispatch a runtime call. -/// - `ReadState`: Represents a function call to read the state from the runtime. -/// - `SendXcm`: Represents a function call to send an XCM message. -#[derive(Debug)] -pub enum FuncId { - Dispatch, - ReadState, -} - -impl TryFrom for FuncId { - type Error = DispatchError; - - /// Attempts to convert a `u8` value to its corresponding `FuncId` variant. - /// - /// If the `u8` value does not match any known function identifier, it returns a - /// `DispatchError::Other` indicating an unknown function ID. - fn try_from(func_id: u8) -> Result { - let id = match func_id { - 0 => Self::Dispatch, - 1 => Self::ReadState, - _ => { - return Err(UNKNOWN_CALL_ERROR); - }, - }; - Ok(id) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{Assets, Runtime, System}; - use sp_runtime::BuildStorage; - - // Test ensuring `func_id()` and `ext_id()` work as expected, i.e. extracting the first two - // bytes and the last two bytes, respectively, from a 4 byte array. - #[test] - fn test_byte_extraction() { - use rand::Rng; - - // Helper functions - fn func_id(id: u32) -> u16 { - (id & 0x0000FFFF) as u16 - } - fn ext_id(id: u32) -> u16 { - (id >> 16) as u16 - } - - // Number of test iterations - let test_iterations = 1_000_000; - - // Create a random number generator - let mut rng = rand::thread_rng(); - - // Run the test for a large number of random 4-byte arrays - for _ in 0..test_iterations { - // Generate a random 4-byte array - let bytes: [u8; 4] = rng.gen(); - - // Convert the 4-byte array to a u32 value - let value = u32::from_le_bytes(bytes); - - // Extract the first two bytes (least significant 2 bytes) - let first_two_bytes = func_id(value); - - // Extract the last two bytes (most significant 2 bytes) - let last_two_bytes = ext_id(value); - - // Check if the first two bytes match the expected value - assert_eq!([bytes[0], bytes[1]], first_two_bytes.to_le_bytes()); - - // Check if the last two bytes match the expected value - assert_eq!([bytes[2], bytes[3]], last_two_bytes.to_le_bytes()); - } - } - - // Test showing all the different type of variants and its encoding. - #[test] - fn encoding_of_enum() { - #[derive(Debug, PartialEq, Encode, Decode)] - enum ComprehensiveEnum { - SimpleVariant, - DataVariant(u8), - NamedFields { w: u8 }, - NestedEnum(InnerEnum), - OptionVariant(Option), - VecVariant(Vec), - TupleVariant(u8, u8), - NestedStructVariant(NestedStruct), - NestedEnumStructVariant(NestedEnumStruct), - } - - #[derive(Debug, PartialEq, Encode, Decode)] - enum InnerEnum { - A, - B { inner_data: u8 }, - C(u8), - } - - #[derive(Debug, PartialEq, Encode, Decode)] - struct NestedStruct { - x: u8, - y: u8, - } - - #[derive(Debug, PartialEq, Encode, Decode)] - struct NestedEnumStruct { - inner_enum: InnerEnum, - } - - // Creating each possible variant for an enum. - let enum_simple = ComprehensiveEnum::SimpleVariant; - let enum_data = ComprehensiveEnum::DataVariant(42); - let enum_named = ComprehensiveEnum::NamedFields { w: 42 }; - let enum_nested = ComprehensiveEnum::NestedEnum(InnerEnum::B { inner_data: 42 }); - let enum_option = ComprehensiveEnum::OptionVariant(Some(42)); - let enum_vec = ComprehensiveEnum::VecVariant(vec![1, 2, 3, 4, 5]); - let enum_tuple = ComprehensiveEnum::TupleVariant(42, 42); - let enum_nested_struct = - ComprehensiveEnum::NestedStructVariant(NestedStruct { x: 42, y: 42 }); - let enum_nested_enum_struct = - ComprehensiveEnum::NestedEnumStructVariant(NestedEnumStruct { - inner_enum: InnerEnum::C(42), - }); - - // Encode and print each variant individually to see their encoded values. - println!("{:?} -> {:?}", enum_simple, enum_simple.encode()); - println!("{:?} -> {:?}", enum_data, enum_data.encode()); - println!("{:?} -> {:?}", enum_named, enum_named.encode()); - println!("{:?} -> {:?}", enum_nested, enum_nested.encode()); - println!("{:?} -> {:?}", enum_option, enum_option.encode()); - println!("{:?} -> {:?}", enum_vec, enum_vec.encode()); - println!("{:?} -> {:?}", enum_tuple, enum_tuple.encode()); - println!("{:?} -> {:?}", enum_nested_struct, enum_nested_struct.encode()); - println!("{:?} -> {:?}", enum_nested_enum_struct, enum_nested_enum_struct.encode()); - } - - fn new_test_ext() -> sp_io::TestExternalities { - let t = frame_system::GenesisConfig::::default() - .build_storage() - .expect("Frame system builds valid default genesis config"); - let mut ext = sp_io::TestExternalities::new(t); - ext.execute_with(|| System::set_block_number(1)); - ext - } - - #[test] - fn encoding_decoding_dispatch_error() { - use sp_runtime::{ArithmeticError, DispatchError, ModuleError, TokenError}; - - new_test_ext().execute_with(|| { - let error = DispatchError::Module(ModuleError { - index: 255, - error: [2, 0, 0, 0], - message: Some("error message"), - }); - let encoded = error.encode(); - let decoded = DispatchError::decode(&mut &encoded[..]).unwrap(); - assert_eq!(encoded, vec![3, 255, 2, 0, 0, 0]); - assert_eq!( - decoded, - // `message` is skipped for encoding. - DispatchError::Module(ModuleError { - index: 255, - error: [2, 0, 0, 0], - message: None - }) - ); - - // Example pallet assets Error into ModuleError. - let index = <::PalletInfo as frame_support::traits::PalletInfo>::index::< - Assets, - >() - .expect("Every active module has an index in the runtime; qed") as u8; - let mut error = - pallet_assets::Error::NotFrozen::.encode(); - error.resize(MAX_MODULE_ERROR_ENCODED_SIZE, 0); - let error = DispatchError::Module(ModuleError { - index, - error: TryInto::try_into(error).expect("should work"), - message: None, - }); - let encoded = error.encode(); - let decoded = DispatchError::decode(&mut &encoded[..]).unwrap(); - assert_eq!(encoded, vec![3, 52, 18, 0, 0, 0]); - assert_eq!( - decoded, - DispatchError::Module(ModuleError { - index: 52, - error: [18, 0, 0, 0], - message: None - }) - ); - - // Example DispatchError::Token - let error = DispatchError::Token(TokenError::UnknownAsset); - let encoded = error.encode(); - let decoded = DispatchError::decode(&mut &encoded[..]).unwrap(); - assert_eq!(encoded, vec![7, 4]); - assert_eq!(decoded, error); - - // Example DispatchError::Arithmetic - let error = DispatchError::Arithmetic(ArithmeticError::Overflow); - let encoded = error.encode(); - let decoded = DispatchError::decode(&mut &encoded[..]).unwrap(); - assert_eq!(encoded, vec![8, 1]); - assert_eq!(decoded, error); - }); - } -} diff --git a/runtime/devnet/src/lib.rs b/runtime/devnet/src/lib.rs index 130fbb9a..4256ac01 100644 --- a/runtime/devnet/src/lib.rs +++ b/runtime/devnet/src/lib.rs @@ -8,7 +8,6 @@ include!(concat!(env!("OUT_DIR"), "/wasm_binary.rs")); // Public due to integration tests crate. pub mod config; -mod extensions; mod weights; use cumulus_pallet_parachain_system::RelayNumberMonotonicallyIncreases;