From 05a5d5b49dc0db3f7de8fdb3129ea2254b1f780b Mon Sep 17 00:00:00 2001 From: Tin Chung <56880684+chungquantin@users.noreply.github.com> Date: Thu, 5 Sep 2024 21:36:12 +0700 Subject: [PATCH] test: generic extension unit testing (#240) --- extension/src/decoding.rs | 250 ++++++++++- extension/src/functions.rs | 433 ++++++++++++++++++-- extension/src/lib.rs | 248 +++++------ extension/src/matching.rs | 54 ++- extension/src/mock.rs | 197 +++++++-- extension/src/tests.rs | 349 +++++++--------- pallets/api/src/extension.rs | 12 +- pallets/api/src/fungibles/mod.rs | 16 +- pallets/api/src/lib.rs | 3 +- primitives/src/lib.rs | 28 +- runtime/devnet/src/config/api/mod.rs | 32 +- runtime/devnet/src/config/api/versioning.rs | 12 +- 12 files changed, 1189 insertions(+), 445 deletions(-) diff --git a/extension/src/decoding.rs b/extension/src/decoding.rs index 6264952f..567f74dd 100644 --- a/extension/src/decoding.rs +++ b/extension/src/decoding.rs @@ -6,7 +6,8 @@ use sp_std::vec::Vec; pub trait Decode { /// The output type to be decoded. type Output: codec::Decode; - /// An optional processor, for performing any additional processing on data read from the contract before decoding. + /// An optional processor, for performing any additional processing on data read from the + /// contract before decoding. type Processor: Processor>; /// The error to return if decoding fails. type Error: Get; @@ -19,8 +20,8 @@ pub trait Decode { /// # Parameters /// - `env` - The current execution environment. fn decode(env: &mut E) -> Result { - // Charge appropriate weight for copying from contract, based on input length, prior to decoding. - // reference: https://github.com/paritytech/polkadot-sdk/pull/4233/files#:~:text=CopyToContract(len)%20%3D%3E%20T%3A%3AWeightInfo%3A%3Aseal_return(len)%2C + // Charge appropriate weight for copying from contract, based on input length, prior to + // decoding. reference: https://github.com/paritytech/polkadot-sdk/pull/4233/files#:~:text=CopyToContract(len)%20%3D%3E%20T%3A%3AWeightInfo%3A%3Aseal_return(len)%2C let len = env.in_len(); let weight = ContractWeights::::seal_return(len); let charged = env.charge_weight(weight)?; @@ -28,19 +29,46 @@ pub trait Decode { // Read encoded input supplied by contract for buffer. let mut input = env.read(len)?; log::debug!(target: Self::LOG_TARGET, "input read: input={input:?}"); - // Perform any additional processing required. Any implementation is expected to charge weight as appropriate. + // Perform any additional processing required. Any implementation is expected to charge + // weight as appropriate. input = Self::Processor::process(input, env); // Finally decode and return. Self::Output::decode(&mut &input[..]).map_err(|_| { log::error!(target: Self::LOG_TARGET, "decoding failed: unable to decode input into output type. input={input:?}"); - // TODO: should we standardise on pallet_contracts::Error::DecodingFailed to simplify rather than allow customisation? Self::Error::get() }) } } +/// Trait for processing a value based on additional information available from the environment. +pub trait Processor { + /// The type of value to be processed. + type Value; + + /// The log target. + const LOG_TARGET: &'static str; + + /// Processes the provided value. + /// + /// # Parameters + /// - `value` - The value to be processed. + /// - `env` - The current execution environment. + fn process(value: Self::Value, env: &impl Environment) -> Self::Value; +} + +/// Default processor implementation which just passes through the value unchanged. +pub struct Identity(PhantomData); +impl Processor for Identity { + type Value = Value; + const LOG_TARGET: &'static str = ""; + + fn process(value: Self::Value, _env: &impl Environment) -> Self::Value { + value + } +} + /// Default implementation for decoding data read from contract memory. -pub struct Decodes(PhantomData<(O, E, P, L)>); +pub struct Decodes>, L = ()>(PhantomData<(O, E, P, L)>); impl< Output: codec::Decode, Error: Get, @@ -54,13 +82,209 @@ impl< const LOG_TARGET: &'static str = Logger::LOG_TARGET; } -/// Default processor implementation which just passes through the value unchanged. -pub struct IdentityProcessor; -impl Processor for IdentityProcessor { - type Value = Vec; - const LOG_TARGET: &'static str = ""; +/// Error to be returned when decoding fails. +pub struct DecodingFailed(PhantomData); +impl Get for DecodingFailed { + fn get() -> DispatchError { + pallet_contracts::Error::::DecodingFailed.into() + } +} - fn process(value: Self::Value, _env: &impl Environment) -> Self::Value { - value +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + extension::read_from_buffer_weight, + mock::{MockEnvironment, RemoveFirstByte, Test}, + }; + use codec::{Decode as OriginalDecode, Encode}; + use frame_support::assert_ok; + + type EnumDecodes = Decodes>; + + #[test] + fn identity_processor_works() { + let env = MockEnvironment::default(); + assert_eq!(Identity::process(42, &env), 42); + assert_eq!(Identity::process(vec![0, 1, 2, 3, 4], &env), vec![0, 1, 2, 3, 4]); + } + + #[test] + fn remove_first_byte_processor_works() { + let env = MockEnvironment::default(); + let result = RemoveFirstByte::process(vec![0, 1, 2, 3, 4], &env); + assert_eq!(result, vec![1, 2, 3, 4]) + } + + #[test] + fn decode_works() { + test_cases().into_iter().for_each(|t| { + let (input, output) = (t.0, t.1); + println!("input: {:?} -> output: {:?}", input, output); + let mut env = MockEnvironment::new(0, input); + // Decode `input` to `output`. + assert_eq!(EnumDecodes::decode(&mut env), Ok(output)); + }); + } + + #[test] + fn decode_charges_weight() { + test_cases().into_iter().for_each(|t| { + let (input, output) = (t.0, t.1); + println!("input: {:?} -> output: {:?}", input, output); + let mut env = MockEnvironment::new(0, input.clone()); + // Decode `input` to `output`. + assert_ok!(EnumDecodes::decode(&mut env)); + // Decode charges weight based on the length of the input. + assert_eq!(env.charged(), read_from_buffer_weight(input.len() as u32)); + }); + } + + #[test] + fn decoding_failed_error_type_works() { + assert_eq!( + DecodingFailed::::get(), + pallet_contracts::Error::::DecodingFailed.into() + ) + } + + #[test] + fn decode_failure_returns_decoding_failed_error() { + let input = vec![100]; + let mut env = MockEnvironment::new(0, input.clone()); + let result = EnumDecodes::decode(&mut env); + assert_eq!(result, Err(pallet_contracts::Error::::DecodingFailed.into())); + } + + #[test] + fn decode_failure_charges_weight() { + let input = vec![100]; + let mut env = MockEnvironment::new(0, input.clone()); + assert!(EnumDecodes::decode(&mut env).is_err()); + // Decode charges weight based on the length of the input, also when decoding fails. + assert_eq!(env.charged(), ContractWeights::::seal_return(input.len() as u32)); + } + + #[derive(Debug, Clone, PartialEq, Encode, OriginalDecode)] + enum ComprehensiveEnum { + SimpleVariant, + DataVariant(u8), + NamedFields { w: u8 }, + NestedEnum(InnerEnum), + OptionVariant(Option), + VecVariant(Vec), + TupleVariant(u8, u8), + NestedStructVariant(NestedStruct), + NestedEnumStructVariant(NestedEnumStruct), + } + + #[derive(Debug, Clone, PartialEq, Encode, OriginalDecode)] + enum InnerEnum { + A, + B { inner_data: u8 }, + C(u8), + } + + #[derive(Debug, Clone, PartialEq, Encode, OriginalDecode)] + struct NestedStruct { + x: u8, + y: u8, + } + + #[derive(Debug, Clone, PartialEq, Encode, OriginalDecode)] + struct NestedEnumStruct { + inner_enum: InnerEnum, + } + + // Creating a set of byte data input and the decoded enum variant. + fn test_cases() -> Vec<(Vec, ComprehensiveEnum)> { + vec![ + (vec![0, 0, 0, 0], ComprehensiveEnum::SimpleVariant), + (vec![1, 42, 0, 0], ComprehensiveEnum::DataVariant(42)), + (vec![2, 42, 0, 0], ComprehensiveEnum::NamedFields { w: 42 }), + (vec![3, 0, 0, 0], ComprehensiveEnum::NestedEnum(InnerEnum::A)), + (vec![3, 1, 42, 0], ComprehensiveEnum::NestedEnum(InnerEnum::B { inner_data: 42 })), + (vec![3, 2, 42, 0], ComprehensiveEnum::NestedEnum(InnerEnum::C(42))), + (vec![4, 1, 42, 0], ComprehensiveEnum::OptionVariant(Some(42))), + (vec![4, 0, 0, 0], ComprehensiveEnum::OptionVariant(None)), + (vec![5, 12, 1, 2, 3], ComprehensiveEnum::VecVariant(vec![1, 2, 3])), + (vec![5, 16, 1, 2, 3, 4], ComprehensiveEnum::VecVariant(vec![1, 2, 3, 4])), + (vec![5, 20, 1, 2, 3, 4, 5], ComprehensiveEnum::VecVariant(vec![1, 2, 3, 4, 5])), + (vec![6, 42, 43, 0], ComprehensiveEnum::TupleVariant(42, 43)), + ( + vec![7, 42, 43, 0], + ComprehensiveEnum::NestedStructVariant(NestedStruct { x: 42, y: 43 }), + ), + ( + vec![8, 1, 42, 0], + ComprehensiveEnum::NestedEnumStructVariant(NestedEnumStruct { + inner_enum: InnerEnum::B { inner_data: 42 }, + }), + ), + ] + } + + // Test showing all the different type of variants and its encoding. + #[test] + fn encoding_of_enum() { + // 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(); + // DispatchError::Module index is 3 + 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/extension/src/functions.rs b/extension/src/functions.rs index 6a353626..3719b9d0 100644 --- a/extension/src/functions.rs +++ b/extension/src/functions.rs @@ -17,30 +17,6 @@ pub trait Function { ) -> Result; } -// Support tuples of at least one function (required for type resolution) and a maximum of ten. -#[impl_trait_for_tuples::impl_for_tuples(1, 10)] -#[tuple_types_custom_trait_bound(Function + Matches)] -impl Function for Tuple { - for_tuples!( where #( Tuple: Function )* ); - type Config = Runtime; - type Error = (); - - fn execute( - env: &mut (impl Environment + BufIn + BufOut), - ) -> Result { - // Attempts to match a specified extension/function identifier to its corresponding function, as configured by the runtime. - for_tuples!( #( - if Tuple::matches(env) { - return Tuple::execute(env) - } - )* ); - - // Otherwise returns error indicating an unmatched request. - log::error!("no function could be matched"); - Err(pallet_contracts::Error::::DecodingFailed.into()) - } -} - /// A function for dispatching a runtime call. pub struct DispatchCall(PhantomData<(M, C, D, F, E, L)>); impl< @@ -66,7 +42,6 @@ impl< /// - `env` - The current execution environment. fn execute(env: &mut (impl Environment + BufIn)) -> Result { // Decode runtime call. - // TODO: should the error returned from decoding failure be converted into a versioned error, or always be pallet_contracts::Error::DecodingFailed? let call = Decoder::decode(env)?.into(); log::debug!(target: Logger::LOG_TARGET, "decoded: call={call:?}"); // Charge weight before dispatch. @@ -125,8 +100,7 @@ impl< /// # Parameters /// - `env` - The current execution environment. fn execute(env: &mut (impl Environment + BufIn + BufOut)) -> Result { - // Decode runtime read - // TODO: should the error returned from decoding failure be converted into a versioned error, or always be pallet_contracts::Error::DecodingFailed? + // Decode runtime state read let read = Decoder::decode(env)?.into(); log::debug!(target: Logger::LOG_TARGET, "decoded: read={read:?}"); // Charge weight before read @@ -137,15 +111,15 @@ impl< ensure!(Filter::contains(&read), frame_system::Error::::CallFiltered); let result = read.read(); log::debug!(target: Logger::LOG_TARGET, "read: result={result:?}"); - // Perform any final conversion. Any implementation is expected to charge weight as appropriate. + // Perform any final conversion. Any implementation is expected to charge weight as + // appropriate. let result = ResultConverter::convert(result, env).into(); - // Charge weight before read - let weight = ContractWeights::::seal_input_per_byte(1); // use unit weight as write function handles multiplication - log::trace!(target: Logger::LOG_TARGET, "return result to contract: weight_per_byte={weight}"); - // Charge appropriate weight for writing to contract, based on input length, prior to decoding. - // TODO: check parameters (allow_skip, weight_per_byte) - // TODO: confirm whether potential error from writing to the buffer needs to be converted to a versioned error (suspect not) - env.write(&result, false, Some(weight))?; + log::debug!(target: Logger::LOG_TARGET, "converted: result={result:?}"); + // Charge appropriate weight for writing to contract, based on result length. + let weight = ContractWeights::::seal_input(result.len() as u32); + let charged = env.charge_weight(weight)?; + log::trace!(target: Logger::LOG_TARGET, "return result to contract: weight={weight}, charged={charged:?}"); + env.write(&result, false, None)?; // weight charged above Ok(Converging(0)) } } @@ -156,6 +130,36 @@ impl Matches for ReadState Weight; + + /// Performs the read and returns the result. + fn read(self) -> Self::Result; +} + +/// Trait for converting a value based on additional information available from the environment. +pub trait Converter { + /// The type of value to be converted. + type Source; + /// The target type. + type Target; + /// The log target. + const LOG_TARGET: &'static str; + + /// Converts the provided value. + /// + /// # Parameters + /// - `value` - The value to be converted. + /// - `env` - The current execution environment. + fn convert(value: Self::Source, env: &impl Environment) -> Self::Target; +} + /// A default converter, for converting (encoding) from some type into a byte array. pub struct DefaultConverter(PhantomData); impl>> Converter for DefaultConverter { @@ -167,3 +171,362 @@ impl>> Converter for DefaultConverter { value.into() } } + +/// Trait for error conversion. +pub trait ErrorConverter { + /// The log target. + const LOG_TARGET: &'static str; + + /// Converts the provided error. + /// + /// # Parameters + /// - `error` - The error to be converted. + /// - `env` - The current execution environment. + fn convert(error: DispatchError, env: &impl Environment) -> Result; +} + +impl ErrorConverter for () { + const LOG_TARGET: &'static str = "pop-chain-extension::converters::error"; + + fn convert(error: DispatchError, _env: &impl Environment) -> Result { + Err(error) + } +} + +// Support tuples of at least one function (required for type resolution) and a maximum of ten. +#[impl_trait_for_tuples::impl_for_tuples(1, 10)] +#[tuple_types_custom_trait_bound(Function + Matches)] +impl Function for Tuple { + for_tuples!( where #( Tuple: Function )* ); + type Config = Runtime; + type Error = (); + + fn execute( + env: &mut (impl Environment + BufIn + BufOut), + ) -> Result { + // Attempts to match a specified extension/function identifier to its corresponding + // function, as configured by the runtime. + for_tuples!( #( + if Tuple::matches(env) { + return Tuple::execute(env) + } + )* ); + + // Otherwise returns error indicating an unmatched request. + log::error!("no function could be matched"); + Err(pallet_contracts::Error::::DecodingFailed.into()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + extension::{read_from_buffer_weight, write_to_contract_weight}, + matching::WithFuncId, + mock::{Noop, NoopFuncId, INVALID_FUNC_ID}, + }; + use codec::Encode; + use frame_support::traits::{Everything, Nothing}; + use frame_system::Call; + use mock::{new_test_ext, Functions, MockEnvironment, RuntimeCall, RuntimeRead, Test}; + use sp_core::ConstU32; + + type FuncId = ConstU32<42>; + + enum AtLeastOneByte {} + impl Contains> for AtLeastOneByte { + fn contains(input: &Vec) -> bool { + input.len() > 0 + } + } + + enum LargerThan100 {} + impl Contains for LargerThan100 { + fn contains(input: &u8) -> bool { + *input > 100 + } + } + + enum MustBeEven {} + impl Contains for MustBeEven { + fn contains(input: &u8) -> bool { + *input % 2 == 0 + } + } + + #[test] + fn contains_works() { + fn contains, T>(input: T, expected: bool) { + assert_eq!(C::contains(&input), expected); + } + contains::(42, true); + contains::>(vec![1, 2, 3, 4], true); + contains::(42, false); + contains::>(vec![1, 2, 3, 4], false); + contains::>(vec![], false); + contains::>(vec![1], true); + contains::>(vec![1, 2, 3, 4], true); + contains::(100, false); + contains::(101, true); + contains::(100, true); + contains::(101, false); + } + + mod dispatch_call { + use super::*; + + type DispatchCall = DispatchCallWithFilter; + type DispatchCallWithFilter = super::DispatchCall< + WithFuncId, + Test, + Decodes>, + Filter, + >; + + #[test] + fn dispatch_call_filtering_works() { + let call = + RuntimeCall::System(Call::remark_with_event { remark: "pop".as_bytes().to_vec() }); + let mut env = MockEnvironment::new(FuncId::get(), call.encode()); + let error = frame_system::Error::::CallFiltered.into(); + let expected = <() as ErrorConverter>::convert(error, &mut env).err(); + assert_eq!(DispatchCallWithFilter::::execute(&mut env).err(), expected); + } + + #[test] + fn dispatch_call_filtered_charges_weight() { + let call = + RuntimeCall::System(Call::remark_with_event { remark: "pop".as_bytes().to_vec() }); + let encoded_call = call.encode(); + let mut env = MockEnvironment::new(FuncId::get(), encoded_call.clone()); + assert!(DispatchCallWithFilter::::execute(&mut env).is_err()); + assert_eq!( + env.charged(), + read_from_buffer_weight(encoded_call.len() as u32) + + call.get_dispatch_info().weight + ); + } + + #[test] + fn dispatch_call_works() { + new_test_ext().execute_with(|| { + let call = RuntimeCall::System(Call::remark_with_event { + remark: "pop".as_bytes().to_vec(), + }); + let mut env = MockEnvironment::new(FuncId::get(), call.encode()); + assert!(matches!(DispatchCall::execute(&mut env), Ok(Converging(0)))); + }) + } + + #[test] + fn dispatch_call_returns_error() { + new_test_ext().execute_with(|| { + let call = RuntimeCall::System(Call::set_code { code: "pop".as_bytes().to_vec() }); + let mut env = MockEnvironment::new(FuncId::get(), call.encode()); + let error = DispatchCall::execute(&mut env).err(); + let expected = + <() as ErrorConverter>::convert(DispatchError::BadOrigin, &env).err(); + assert_eq!(error, expected); + }) + } + + #[test] + fn dispatch_call_charges_weight() { + new_test_ext().execute_with(|| { + let call = RuntimeCall::System(Call::remark_with_event { + remark: "pop".as_bytes().to_vec(), + }); + let encoded_call = call.encode(); + let mut env = MockEnvironment::new(FuncId::get(), encoded_call.clone()); + assert!(DispatchCall::execute(&mut env).is_ok()); + assert_eq!( + env.charged(), + read_from_buffer_weight(encoded_call.len() as u32) + + call.get_dispatch_info().weight + ); + }) + } + + #[test] + fn dispatch_call_adjusts_weight() { + new_test_ext().execute_with(|| { + // Attempt to perform non-existent migration with additional weight limit specified. + let weight_limit = Weight::from_parts(123456789, 12345); + let call = RuntimeCall::Contracts(pallet_contracts::Call::migrate { weight_limit }); + let encoded_call = call.encode(); + let mut env = MockEnvironment::new(FuncId::get(), encoded_call.clone()); + let expected: DispatchError = + pallet_contracts::Error::::NoMigrationPerformed.into(); + assert_eq!(DispatchCall::execute(&mut env).err().unwrap(), expected); + assert_eq!( + env.charged(), + read_from_buffer_weight(encoded_call.len() as u32) + + // Weight limit subtracted from pre-dispatch weight charged on failure. + call.get_dispatch_info().weight - + weight_limit + ); + }) + } + + #[test] + fn dispatch_call_with_invalid_input_returns_error() { + // Invalid encoded runtime call. + let input = vec![0, 99]; + let mut env = MockEnvironment::new(FuncId::get(), input.clone()); + let error = pallet_contracts::Error::::DecodingFailed.into(); + let expected = <() as ErrorConverter>::convert(error, &mut env).err(); + assert_eq!(DispatchCall::execute(&mut env).err(), expected); + } + + #[test] + fn dispatch_call_with_invalid_input_charges_weight() { + // Invalid encoded runtime call. + let input = vec![0, 99]; + let mut env = MockEnvironment::new(FuncId::get(), input.clone()); + assert!(DispatchCall::execute(&mut env).is_err()); + assert_eq!(env.charged(), read_from_buffer_weight(input.len() as u32,)); + } + } + + mod read_state { + use super::*; + use crate::mock::{RuntimeResult, UppercaseConverter}; + + type ReadState = ReadStateWithFilter; + type ReadStateWithFilter = super::ReadState< + WithFuncId, + Test, + RuntimeRead, + Decodes>, + Filter, + >; + type ReadStateWithResultConverter = super::ReadState< + WithFuncId, + Test, + RuntimeRead, + Decodes>, + Everything, + ResultConverter, + >; + + #[test] + fn read_state_filtering_works() { + let read = RuntimeRead::Ping; + let mut env = MockEnvironment::new(FuncId::get(), read.encode()); + let error = frame_system::Error::::CallFiltered.into(); + let expected = <() as ErrorConverter>::convert(error, &mut env).err(); + assert_eq!(ReadStateWithFilter::::execute(&mut env).err(), expected); + } + + #[test] + fn read_state_filtered_charges_weight() { + let read = RuntimeRead::Ping; + let encoded_read = read.encode(); + let mut env = MockEnvironment::new(FuncId::get(), encoded_read.clone()); + assert!(ReadStateWithFilter::::execute(&mut env).is_err()); + assert_eq!( + env.charged(), + read_from_buffer_weight(encoded_read.len() as u32) + read.weight() + ); + } + + #[test] + fn read_state_works() { + let read = RuntimeRead::Ping; + let expected = "pop".as_bytes().encode(); + let mut env = MockEnvironment::new(FuncId::get(), read.encode()); + assert!(matches!(ReadState::execute(&mut env), Ok(Converging(0)))); + // Check if the contract environment buffer is written correctly. + assert_eq!(env.buffer, expected); + } + + #[test] + fn read_state_result_conversion_works() { + let read = RuntimeRead::Ping; + let expected = RuntimeResult::Pong("pop".to_string()); + let mut env = MockEnvironment::new(FuncId::get(), read.encode()); + assert!(matches!( + ReadStateWithResultConverter::::execute(&mut env), + Ok(Converging(0)) + )); + // Check if the contract environment buffer is written correctly. + assert_eq!(env.buffer, UppercaseConverter::convert(expected, &env)); + } + + #[test] + fn read_state_charges_weight() { + let read = RuntimeRead::Ping; + let encoded_read = read.encode(); + let mut env = MockEnvironment::new(FuncId::get(), encoded_read.clone()); + assert!(ReadState::execute(&mut env).is_ok()); + let expected = "pop".as_bytes().encode(); + assert_eq!( + env.charged(), + read_from_buffer_weight(encoded_read.len() as u32) + + read.weight() + write_to_contract_weight(expected.len() as u32) + ); + } + + #[test] + fn read_state_with_invalid_input_returns_error() { + // Invalid encoded runtime state read. + let input = vec![0]; + let mut env = MockEnvironment::new(FuncId::get(), input.clone()); + let error = pallet_contracts::Error::::DecodingFailed.into(); + let expected = <() as ErrorConverter>::convert(error, &mut env).err(); + assert_eq!(ReadState::execute(&mut env).err(), expected); + } + + #[test] + fn read_state_with_invalid_input_charges_weight() { + // Invalid encoded runtime state read. + let input = vec![0]; + let mut env = MockEnvironment::new(FuncId::get(), input.clone()); + assert!(ReadState::execute(&mut env).is_err()); + assert_eq!(env.charged(), read_from_buffer_weight(input.len() as u32)); + } + } + + #[test] + fn execute_tuple_matches_and_executes_function() { + type Functions = (Noop, Test>,); + let mut env = MockEnvironment::new(NoopFuncId::get(), vec![]); + assert!(matches!(Functions::execute(&mut env), Ok(Converging(0)))); + } + + #[test] + fn execute_tuple_with_invalid_function_fails() { + let input = vec![]; + let mut env = MockEnvironment::new(INVALID_FUNC_ID, input.clone()); + let error = pallet_contracts::Error::::DecodingFailed.into(); + let expected = <() as ErrorConverter>::convert(error, &mut env).err(); + assert_eq!(Functions::execute(&mut env).err(), expected); + } + + #[test] + fn execute_tuple_with_invalid_function_does_not_charge_weight() { + let input = vec![]; + let mut env = MockEnvironment::new(INVALID_FUNC_ID, input.clone()); + assert!(Functions::execute(&mut env).is_err()); + // No weight charged as no function in the `Functions` tuple is matched to charge weight. + // See extension tests for extension call weight charges. + assert_eq!(env.charged(), Weight::default()); + } + + #[test] + fn default_error_conversion_works() { + let env = MockEnvironment::default(); + assert!(matches!( + <() as ErrorConverter>::convert(DispatchError::BadOrigin, &env), + Err(DispatchError::BadOrigin) + )); + } + + #[test] + fn default_conversion_works() { + let env = MockEnvironment::default(); + let source = "pop".to_string(); + assert_eq!(DefaultConverter::convert(source.clone(), &env), source.as_bytes()); + } +} diff --git a/extension/src/lib.rs b/extension/src/lib.rs index 1bbd13d0..d9192f93 100644 --- a/extension/src/lib.rs +++ b/extension/src/lib.rs @@ -1,8 +1,8 @@ #![cfg_attr(not(feature = "std"), no_std)] use codec::Decode as _; -use core::{fmt::Debug, marker::PhantomData}; -pub use decoding::{Decode, Decodes, IdentityProcessor}; +use core::marker::PhantomData; +pub use decoding::{Decode, Decodes, DecodingFailed, Identity, Processor}; pub use environment::{BufIn, BufOut, Environment, Ext}; use frame_support::{ dispatch::{GetDispatchInfo, PostDispatchInfo, RawOrigin}, @@ -10,11 +10,15 @@ use frame_support::{ traits::{Contains, OriginTrait}, weights::Weight, }; -pub use functions::{DispatchCall, Function, ReadState}; +pub use functions::{ + Converter, DefaultConverter, DispatchCall, ErrorConverter, Function, ReadState, Readable, +}; pub use matching::{Equals, FunctionId, Matches}; -use pallet_contracts::chain_extension::{ChainExtension, InitState, RetVal::Converging}; pub use pallet_contracts::chain_extension::{Result, RetVal, State}; -use pallet_contracts::WeightInfo; +use pallet_contracts::{ + chain_extension::{ChainExtension, InitState, RetVal::Converging}, + WeightInfo, +}; use sp_core::Get; use sp_runtime::{traits::Dispatchable, DispatchError}; use sp_std::vec::Vec; @@ -23,14 +27,17 @@ mod decoding; mod environment; mod functions; mod matching; +// Mock runtime/environment for unit/integration testing. #[cfg(test)] mod mock; +// Integration tests using proxy contract and mock runtime. #[cfg(test)] mod tests; type ContractWeights = ::WeightInfo; -/// Encoded version of `pallet_contracts::Error::DecodingFailed`, as found within `DispatchError::ModuleError`. +/// Encoded version of `pallet_contracts::Error::DecodingFailed`, as found within +/// `DispatchError::ModuleError`. pub const DECODING_FAILED_ERROR: [u8; 4] = [11, 0, 0, 0]; /// A configurable chain extension. @@ -68,8 +75,8 @@ impl< ) -> Result { log::trace!(target: Config::LOG_TARGET, "extension called"); // 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/polkadot-sdk/pull/4233/files#:~:text=DebugMessage(len)%20%3D%3E%20T%3A%3AWeightInfo%3A%3Aseal_debug_message(len)%2C + // `debug_message` weight is a good approximation of the additional overhead of going from + // contract layer to substrate layer. reference: https://github.com/paritytech/polkadot-sdk/pull/4233/files#:~:text=DebugMessage(len)%20%3D%3E%20T%3A%3AWeightInfo%3A%3Aseal_debug_message(len)%2C let len = env.in_len(); let overhead = ContractWeights::::seal_debug_message(len); let charged = env.charge_weight(overhead)?; @@ -88,18 +95,6 @@ pub trait Config { const LOG_TARGET: &'static str; } -/// Trait to be implemented for a type handling a read of runtime state. -pub trait Readable { - /// The corresponding type carrying the result of the runtime state read. - type Result: Debug; - - /// Determines the weight of the read, used to charge the appropriate weight before the read is performed. - fn weight(&self) -> Weight; - - /// Performs the read and returns the result. - fn read(self) -> Self::Result; -} - /// Trait to enable specification of a log target. pub trait LogTarget { /// The log target. @@ -110,120 +105,127 @@ impl LogTarget for () { const LOG_TARGET: &'static str = "pop-chain-extension"; } -/// Trait for error conversion. -pub trait ErrorConverter { - /// The log target. - const LOG_TARGET: &'static str; - - /// Converts the provided error. - /// - /// # Parameters - /// - `error` - The error to be converted. - /// - `env` - The current execution environment. - fn convert(error: DispatchError, env: &impl Environment) -> Result; +#[test] +fn default_log_target_works() { + assert!(matches!(<() as LogTarget>::LOG_TARGET, "pop-chain-extension")); } -impl ErrorConverter for () { - const LOG_TARGET: &'static str = "pop-chain-extension::converters::error"; - - fn convert(error: DispatchError, _env: &impl Environment) -> Result { - Err(error) +#[cfg(test)] +mod extension { + use super::*; + use crate::mock::{ + new_test_ext, DispatchExtFuncId, MockEnvironment, NoopFuncId, ReadExtFuncId, RuntimeCall, + RuntimeRead, Test, INVALID_FUNC_ID, + }; + use codec::Encode; + use frame_system::Call; + + #[test] + fn call_works() { + let input = vec![2, 2]; + let mut env = MockEnvironment::new(NoopFuncId::get(), input.clone()); + let mut extension = Extension::::default(); + assert!(matches!(extension.call(&mut env), Ok(Converging(0)))); + // Charges weight. + assert_eq!(env.charged(), overhead_weight(input.len() as u32)) } -} -/// Error to be returned when decoding fails. -pub struct DecodingFailed(PhantomData); -impl Get for DecodingFailed { - fn get() -> DispatchError { - pallet_contracts::Error::::DecodingFailed.into() + #[test] + fn calling_unknown_function_fails() { + let input = vec![2, 2]; + // No function registered for id 0. + let mut env = MockEnvironment::new(INVALID_FUNC_ID, input.clone()); + let mut extension = Extension::::default(); + assert!(matches!( + extension.call(&mut env), + Err(error) if error == pallet_contracts::Error::::DecodingFailed.into() + )); + // Charges weight. + assert_eq!(env.charged(), overhead_weight(input.len() as u32)) } -} - -/// Trait for processing a value based on additional information available from the environment. -pub trait Processor { - /// The type of value to be processed. - type Value; - - /// The log target. - const LOG_TARGET: &'static str; - - /// Processes the provided value. - /// - /// # Parameters - /// - `value` - The value to be processed. - /// - `env` - The current execution environment. - fn process(value: Self::Value, env: &impl Environment) -> Self::Value; -} -impl Processor for () { - type Value = (); - const LOG_TARGET: &'static str = ""; - fn process(value: Self::Value, _env: &impl Environment) -> Self::Value { - value + #[test] + fn dispatch_call_works() { + new_test_ext().execute_with(|| { + let call = + RuntimeCall::System(Call::remark_with_event { remark: "pop".as_bytes().to_vec() }); + let encoded_call = call.encode(); + let mut env = MockEnvironment::new(DispatchExtFuncId::get(), encoded_call.clone()); + let mut extension = Extension::::default(); + assert!(matches!(extension.call(&mut env), Ok(Converging(0)))); + // Charges weight. + assert_eq!( + env.charged(), + overhead_weight(encoded_call.len() as u32) + + read_from_buffer_weight(encoded_call.len() as u32) + + call.get_dispatch_info().weight + ); + }); } -} - -/// Trait for converting a value based on additional information available from the environment. -pub trait Converter { - /// The type of value to be converted. - type Source; - /// The target type. - type Target; - /// The log target. - const LOG_TARGET: &'static str; - /// Converts the provided value. - /// - /// # Parameters - /// - `value` - The value to be converted. - /// - `env` - The current execution environment. - fn convert(value: Self::Source, env: &impl Environment) -> Self::Target; -} + #[test] + fn dispatch_call_with_invalid_input_returns_error() { + // Invalid encoded runtime call. + let input = vec![0u8, 99]; + let mut env = MockEnvironment::new(DispatchExtFuncId::get(), input.clone()); + let mut extension = Extension::::default(); + assert!(extension.call(&mut env).is_err()); + // Charges weight. + assert_eq!( + env.charged(), + overhead_weight(input.len() as u32) + read_from_buffer_weight(input.len() as u32) + ); + } -#[test] -fn extension_call_works() { - let mut env = - mock::Environment::new(mock::NoopFuncId::get(), Vec::default(), mock::Ext::default()); - let mut extension = Extension::::default(); - assert!(matches!(extension.call(&mut env), Ok(Converging(0)))); -} + #[test] + fn read_state_works() { + let read = RuntimeRead::Ping; + let encoded_read = read.encode(); + let expected = "pop".as_bytes().encode(); + let mut env = MockEnvironment::new(ReadExtFuncId::get(), encoded_read.clone()); + let mut extension = Extension::::default(); + assert!(matches!(extension.call(&mut env), Ok(Converging(0)))); + // Charges weight. + assert_eq!( + env.charged(), + overhead_weight(encoded_read.len() as u32) + + read_from_buffer_weight(encoded_read.len() as u32) + + read.weight() + + write_to_contract_weight(expected.len() as u32) + ); + // Check if the contract environment buffer is written correctly. + assert_eq!(env.buffer, expected); + } -#[test] -fn extension_returns_decoding_failed_for_unknown_function() { - // no function registered for id 0 - let mut env = mock::Environment::new(0, Vec::default(), mock::Ext::default()); - let mut extension = Extension::::default(); - assert!(matches!( - extension.call(&mut env), - Err(error) if error == pallet_contracts::Error::::DecodingFailed.into() - )); -} + #[test] + fn read_state_with_invalid_input_returns_error() { + let input = vec![0u8, 99]; + let mut env = MockEnvironment::new( + ReadExtFuncId::get(), + // Invalid runtime state read. + input.clone(), + ); + let mut extension = Extension::::default(); + assert!(extension.call(&mut env).is_err()); + // Charges weight. + assert_eq!( + env.charged(), + overhead_weight(input.len() as u32) + read_from_buffer_weight(input.len() as u32) + ); + } -#[test] -fn extension_call_charges_weight() { - // specify invalid function - let mut env = mock::Environment::new(0, [0u8; 42].to_vec(), mock::Ext::default()); - let mut extension = Extension::::default(); - assert!(extension.call(&mut env).is_err()); - assert_eq!(env.charged(), ContractWeights::::seal_debug_message(42)) -} + // Weight charged for calling into the runtime from a contract. + fn overhead_weight(input_len: u32) -> Weight { + ContractWeights::::seal_debug_message(input_len) + } -#[test] -fn decoding_failed_error_type_works() { - assert_eq!( - DecodingFailed::::get(), - pallet_contracts::Error::::DecodingFailed.into() - ) -} + // Weight charged for reading function call input from buffer. + pub(crate) fn read_from_buffer_weight(input_len: u32) -> Weight { + ContractWeights::::seal_return(input_len) + } -#[test] -fn default_error_conversion_works() { - let env = mock::Environment::new(0, [0u8; 42].to_vec(), mock::Ext::default()); - assert!(matches!( - <() as ErrorConverter>::convert( - DispatchError::BadOrigin, - &env - ), - Err(error) if error == DispatchError::BadOrigin - )); + // Weight charged for writing to contract memory. + pub(crate) fn write_to_contract_weight(len: u32) -> Weight { + ContractWeights::::seal_input(len) + } } diff --git a/extension/src/matching.rs b/extension/src/matching.rs index bfc48014..5966b9ff 100644 --- a/extension/src/matching.rs +++ b/extension/src/matching.rs @@ -9,7 +9,7 @@ pub trait Matches { fn matches(env: &impl Environment) -> bool; } -/// Matches on an extension and function identifier. +/// Matches an extension and function identifier. pub struct Equals(PhantomData<(E, F)>); impl, FuncId: Get> Matches for Equals { fn matches(env: &impl Environment) -> bool { @@ -17,7 +17,7 @@ impl, FuncId: Get> Matches for Equals { } } -/// Matches on a function identifier only. +/// Matches a function identifier only. pub struct FunctionId(PhantomData); impl> Matches for FunctionId { fn matches(env: &impl Environment) -> bool { @@ -25,7 +25,7 @@ impl> Matches for FunctionId { } } -/// Matches on a function identifier only. +/// Matches a `u32` function identifier. pub struct WithFuncId(PhantomData); impl> Matches for WithFuncId { fn matches(env: &impl Environment) -> bool { @@ -34,3 +34,51 @@ impl> Matches for WithFuncId { u32::from_le_bytes([func_id[0], func_id[1], ext_id[0], ext_id[1]]) == T::get() } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::mock::MockEnvironment; + use sp_core::{ConstU16, ConstU32}; + + #[test] + fn equals_matches() { + let env = MockEnvironment::new(u32::from_be_bytes([0u8, 1, 0, 2]), vec![]); + assert!(Equals::, ConstU16<2>>::matches(&env)); + } + + #[test] + fn equals_does_not_match() { + // Fails due to the invalid function id. + let env = MockEnvironment::new(u32::from_be_bytes([0u8, 1, 0, 3]), vec![]); + assert!(!Equals::, ConstU16<2>>::matches(&env)); + + // Fails due to the invalid extension id. + let env = MockEnvironment::new(u32::from_be_bytes([0u8, 2, 0, 2]), vec![]); + assert!(!Equals::, ConstU16<2>>::matches(&env)); + } + + #[test] + fn function_id_matches() { + let env = MockEnvironment::new(u32::from_be_bytes([0u8, 1, 0, 2]), vec![]); + assert!(FunctionId::>::matches(&env)); + } + + #[test] + fn function_id_does_not_match() { + let env = MockEnvironment::new(u32::from_be_bytes([0u8, 1, 0, 3]), vec![]); + assert!(!FunctionId::>::matches(&env)); + } + + #[test] + fn func_id_matches() { + let env = MockEnvironment::default(); + assert!(WithFuncId::>::matches(&env)); + } + + #[test] + fn func_id_does_not_match() { + let env = MockEnvironment::new(1, vec![]); + assert!(!WithFuncId::>::matches(&env)); + } +} diff --git a/extension/src/mock.rs b/extension/src/mock.rs index 481e29e2..aebd8116 100644 --- a/extension/src/mock.rs +++ b/extension/src/mock.rs @@ -1,14 +1,60 @@ use crate::{ - environment, matching::WithFuncId, Decodes, DecodingFailed, DispatchCall, Extension, Function, - Matches, Processor, + decoding::Identity, environment, matching::WithFuncId, Converter, Decodes, DecodingFailed, + DefaultConverter, DispatchCall, Extension, Function, Matches, Processor, ReadState, Readable, +}; +use codec::{Decode, Encode}; +use frame_support::{ + derive_impl, + pallet_prelude::Weight, + parameter_types, + traits::{fungible::Inspect, ConstU32, Everything, Nothing}, }; -use frame_support::pallet_prelude::Weight; -use frame_support::{derive_impl, parameter_types, traits::ConstU32, traits::Everything}; use frame_system::pallet_prelude::BlockNumberFor; use pallet_contracts::{chain_extension::RetVal, DefaultAddressGenerator, Frame, Schedule}; -use sp_runtime::Perbill; +use sp_runtime::{BuildStorage, Perbill}; use std::marker::PhantomData; +pub(crate) const ALICE: u64 = 1; +pub(crate) const DEBUG_OUTPUT: pallet_contracts::DebugInfo = + pallet_contracts::DebugInfo::UnsafeDebug; +pub(crate) const GAS_LIMIT: Weight = Weight::from_parts(500_000_000_000, 3 * 1024 * 1024); +pub(crate) const INIT_AMOUNT: ::Balance = 100_000_000; +pub(crate) const INVALID_FUNC_ID: u32 = 0; + +pub(crate) type AccountId = ::AccountId; +pub(crate) type Balance = <::Currency as Inspect< + ::AccountId, +>>::Balance; +type DispatchCallWith>> = DispatchCall< + // Registered with func id 1 + WithFuncId, + // Runtime config + Test, + // Decode inputs to the function as runtime calls + Decodes, Processor>, + // Accept any filterting + Filter, +>; +pub(crate) type EventRecord = frame_system::EventRecord< + ::RuntimeEvent, + ::Hash, +>; +pub(crate) type MockEnvironment = Environment; +type ReadStateWith>> = ReadState< + // Registered with func id 1 + WithFuncId, + // Runtime config + Test, + // The runtime state reads available. + RuntimeRead, + // Decode inputs to the function as runtime calls + Decodes, Processor>, + // Accept any filtering + Filter, + // Convert the result of a read into the expected result + DefaultConverter, +>; + frame_support::construct_runtime!( pub enum Test { System: frame_system, @@ -85,38 +131,93 @@ impl frame_support::traits::Randomness<::Hash, Blo } parameter_types! { - pub const DispatchCallFuncId : u32 = 1; - pub const ReadStateFuncId : u32 = 2; + // IDs for functions for extension tests. + pub const DispatchExtFuncId : u32 = 1; + pub const ReadExtFuncId : u32 = 2; + // IDs for functions for contract tests. + pub const DispatchContractFuncId : u32 = 3; + pub const ReadContractFuncId : u32 = 4; + // IDs for function for contract tests but do nothing. + pub const DispatchContractNoopFuncId : u32 = 5; + pub const ReadContractNoopFuncId : u32 = 6; + // ID for function that does nothing pub const NoopFuncId : u32 = u32::MAX; } +/// A query of mock runtime state. +#[derive(Encode, Decode, Debug)] +#[repr(u8)] +pub enum RuntimeRead { + #[codec(index = 1)] + Ping, +} +impl Readable for RuntimeRead { + /// The corresponding type carrying the result of the query for mock runtime state. + type Result = RuntimeResult; + + /// Determines the weight of the read, used to charge the appropriate weight before the read is + /// performed. + fn weight(&self) -> Weight { + match self { + RuntimeRead::Ping => Weight::from_parts(1_000u64, 1u64), + } + } + + /// Performs the read and returns the result. + fn read(self) -> Self::Result { + match self { + RuntimeRead::Ping => RuntimeResult::Pong("pop".to_string()), + } + } +} + +/// The result of a mock runtime state read. +#[derive(Debug, Decode, Encode)] +pub enum RuntimeResult { + #[codec(index = 1)] + Pong(String), +} + +impl Into> for RuntimeResult { + fn into(self) -> Vec { + match self { + RuntimeResult::Pong(value) => value.encode(), + } + } +} + +pub(crate) type Functions = ( + // Functions that allow everything for extension testing. + DispatchCallWith, + ReadStateWith, + // Functions that allow everything for contract testing. + DispatchCallWith, + ReadStateWith, + // Functions that allow nothing for contract testing. + DispatchCallWith, + ReadStateWith, + // Function that does nothing. + Noop, Test>, +); + #[derive(Default)] pub struct Config; impl super::Config for Config { - type Functions = ( - DispatchCall< - // Registered with func id 1 - WithFuncId, - // Runtime config - Test, - // Decode inputs to the function as runtime calls - Decodes, RemoveFirstByte>, - // Allow everything - Everything, - >, - Noop, Test>, - ); + type Functions = Functions; const LOG_TARGET: &'static str = "pop-chain-extension"; } -// Removes first bytes of the encoded call, added by the chain extension call within the proxy contract. +// Removes first bytes of the encoded call, added by the chain extension call within the proxy +// contract. pub struct RemoveFirstByte; impl Processor for RemoveFirstByte { type Value = Vec; const LOG_TARGET: &'static str = ""; fn process(mut value: Self::Value, _env: &impl crate::Environment) -> Self::Value { - value.remove(0); + if !value.is_empty() { + value.remove(0); + } value } } @@ -140,7 +241,7 @@ impl Matches for Noop { } /// A mocked chain extension environment. -pub(crate) struct Environment { +pub(crate) struct Environment { func_id: u16, ext_id: u16, charged: Vec, @@ -148,14 +249,20 @@ pub(crate) struct Environment { ext: E, } -impl Environment { - pub(crate) fn new(id: u32, buffer: Vec, ext: E) -> Self { +impl Default for Environment { + fn default() -> Self { + Self::new(0, [].to_vec()) + } +} + +impl Environment { + pub(crate) fn new(id: u32, buffer: Vec) -> Self { Self { func_id: (id & 0x0000FFFF) as u16, ext_id: (id >> 16) as u16, charged: Vec::new(), buffer, - ext, + ext: E::default(), } } @@ -215,23 +322,53 @@ impl environment::BufIn for Environment { impl environment::BufOut for Environment { fn write( &mut self, - _buffer: &[u8], + buffer: &[u8], _allow_skip: bool, _weight_per_byte: Option, ) -> pallet_contracts::chain_extension::Result<()> { - todo!() + self.buffer = buffer.to_vec(); + Ok(()) } } /// A mocked smart contract environment. #[derive(Clone, Default)] -pub(crate) struct Ext { +pub(crate) struct MockExt { pub(crate) address: ::AccountId, } -impl environment::Ext for Ext { +impl environment::Ext for MockExt { type Config = Test; fn address(&self) -> &::AccountId { &self.address } } + +/// Test externalities. +pub(crate) fn new_test_ext() -> sp_io::TestExternalities { + let _ = env_logger::try_init(); + + let mut t = frame_system::GenesisConfig::::default().build_storage().unwrap(); + + pallet_balances::GenesisConfig:: { balances: vec![(ALICE, INIT_AMOUNT)] } + .assimilate_storage(&mut t) + .unwrap(); + + let mut ext = sp_io::TestExternalities::new(t); + ext.execute_with(|| System::set_block_number(1)); + ext +} + +/// A converter for converting string results to uppercase. +pub(crate) struct UppercaseConverter; +impl Converter for UppercaseConverter { + type Source = RuntimeResult; + type Target = Vec; + const LOG_TARGET: &'static str = ""; + + fn convert(value: Self::Source, _env: &impl crate::Environment) -> Self::Target { + match value { + RuntimeResult::Pong(value) => value.to_uppercase().encode(), + } + } +} diff --git a/extension/src/tests.rs b/extension/src/tests.rs index b85261c0..8d527df6 100644 --- a/extension/src/tests.rs +++ b/extension/src/tests.rs @@ -1,78 +1,170 @@ -use crate::mock::{Test as Runtime, *}; -use codec::{Decode, Encode}; use core::fmt::Debug; -use frame_support::{pallet_prelude::Weight, traits::fungible::Inspect}; -use frame_system::Call; -use pallet_contracts::{Code, CollectEvents, ContractExecResult, Determinism}; -use sp_runtime::{BuildStorage, DispatchError}; use std::{path::Path, sync::LazyLock}; -type AccountId = ::AccountId; -type Balance = <::Currency as Inspect< - ::AccountId, ->>::Balance; -type EventRecord = frame_system::EventRecord< - ::RuntimeEvent, - ::Hash, ->; +use crate::{ + mock::{self, *}, + ErrorConverter, +}; +use codec::{Decode, Encode}; +use frame_support::weights::Weight; +use frame_system::Call; +use pallet_contracts::{Code, CollectEvents, ContractExecResult, Determinism, StorageDeposit}; +use sp_runtime::{ + DispatchError::{self, BadOrigin, Module}, + ModuleError, +}; + +static CONTRACT: LazyLock> = + LazyLock::new(|| initialize_contract("contract/target/ink/proxy.wasm")); -const ALICE: u64 = 1; -const DEBUG_OUTPUT: pallet_contracts::DebugInfo = pallet_contracts::DebugInfo::UnsafeDebug; -const GAS_LIMIT: Weight = Weight::from_parts(100_000_000_000, 3 * 1024 * 1024); -const INIT_AMOUNT: ::Balance = 100_000_000; -const INVALID_FUNC_ID: u32 = 0; +mod dispatch_call { + use super::*; + + #[test] + fn dispatch_call_works() { + new_test_ext().execute_with(|| { + // Instantiate a new contract. + let contract = instantiate(); + let dispatch_result = call( + contract, + DispatchContractFuncId::get(), + RuntimeCall::System(Call::remark_with_event { remark: "pop".as_bytes().to_vec() }), + GAS_LIMIT, + ); + // Successfully return data. + let return_value = dispatch_result.result.unwrap(); + let decoded = , u32>>::decode(&mut &return_value.data[..]).unwrap(); + assert!(decoded.unwrap().is_empty()); + // Successfully emit event. + assert!(dispatch_result.events.unwrap().iter().any(|e| matches!(e.event, + RuntimeEvent::System(frame_system::Event::::Remarked { sender, .. }) + if sender == contract))); + assert_eq!(dispatch_result.storage_deposit, StorageDeposit::Charge(0)); + }); + } + + #[test] + fn dispatch_call_filtering_works() { + new_test_ext().execute_with(|| { + // Instantiate a new contract. + let contract = instantiate(); + let dispatch_result = call( + contract, + DispatchContractNoopFuncId::get(), + RuntimeCall::System(Call::remark_with_event { remark: "pop".as_bytes().to_vec() }), + GAS_LIMIT, + ); + assert_eq!( + dispatch_result.result, + Err(Module(ModuleError { + index: 0, + error: [5, 0, 0, 0], + message: Some("CallFiltered") + })) + ); + }); + } + + #[test] + fn dispatch_call_returns_error() { + new_test_ext().execute_with(|| { + // Instantiate a new contract. + let contract = instantiate(); + let dispatch_result = call( + contract, + DispatchContractFuncId::get(), + // `set_code` requires root origin, expect throwing error. + RuntimeCall::System(Call::set_code { code: "pop".as_bytes().to_vec() }), + GAS_LIMIT, + ); + assert_eq!(dispatch_result.result.err(), Some(BadOrigin)) + }) + } +} + +mod read_state { + use super::*; + + #[test] + fn read_state_works() { + new_test_ext().execute_with(|| { + // Instantiate a new contract. + let contract = instantiate(); + // Successfully return data. + let read_result = + call(contract, ReadContractFuncId::get(), RuntimeRead::Ping, GAS_LIMIT); + let return_value = read_result.result.unwrap(); + let decoded = , u32>>::decode(&mut &return_value.data[1..]).unwrap(); + let result = Ok("pop".as_bytes().to_vec()); + assert_eq!(decoded, result); + }); + } + + #[test] + fn read_state_filtering_works() { + new_test_ext().execute_with(|| { + // Instantiate a new contract. + let contract = instantiate(); + // Successfully return data. + let read_result = + call(contract, ReadContractNoopFuncId::get(), RuntimeRead::Ping, GAS_LIMIT); + assert_eq!( + read_result.result, + Err(Module(ModuleError { + index: 0, + error: [5, 0, 0, 0], + message: Some("CallFiltered") + })) + ); + }); + } + + #[test] + fn read_state_with_invalid_input_returns_error() { + new_test_ext().execute_with(|| { + // Instantiate a new contract. + let contract = instantiate(); + let read_result = call(contract, ReadExtFuncId::get(), 99u8, GAS_LIMIT); + let expected: DispatchError = pallet_contracts::Error::::DecodingFailed.into(); + // Make sure the error is passed through the error converter. + let error = + <() as ErrorConverter>::convert(expected, &mock::MockEnvironment::default()).err(); + assert_eq!(read_result.result.err(), error); + }) + } +} #[test] -fn dispatch_call_works() { +fn noop_function_works() { new_test_ext().execute_with(|| { + // Instantiate a new contract. let contract = instantiate(); - - let call = call( - contract, - DispatchCallFuncId::get(), - RuntimeCall::System(Call::remark_with_event { remark: "pop".as_bytes().to_vec() }), - GAS_LIMIT, - ); - - let return_value = call.result.unwrap(); + let noop_result = call(contract, NoopFuncId::get(), (), GAS_LIMIT); + // Successfully return data. + let return_value = noop_result.result.unwrap(); let decoded = , u32>>::decode(&mut &return_value.data[..]).unwrap(); assert!(decoded.unwrap().is_empty()); - - assert!(call.events.unwrap().iter().any(|e| matches!(e.event, - RuntimeEvent::System(frame_system::Event::::Remarked { sender, .. }) - if sender == contract))); - }); + assert_eq!(noop_result.storage_deposit, StorageDeposit::Charge(0)); + }) } #[test] fn invalid_func_id_fails() { new_test_ext().execute_with(|| { + // Instantiate a new contract. let contract = instantiate(); - - let call = call(contract, INVALID_FUNC_ID, (), GAS_LIMIT); + let result = call(contract, INVALID_FUNC_ID, (), GAS_LIMIT); let expected: DispatchError = pallet_contracts::Error::::DecodingFailed.into(); - // TODO: assess whether this error should be passed through the error converter - i.e. is this error type considered 'stable'? - assert_eq!(call.result, Err(expected)) + // Make sure the error is passed through the error converter. + let error = + <() as ErrorConverter>::convert(expected, &mock::MockEnvironment::default()).err(); + assert_eq!(result.result.err(), error); }); } -fn new_test_ext() -> sp_io::TestExternalities { - let _ = env_logger::try_init(); - - let mut t = frame_system::GenesisConfig::::default().build_storage().unwrap(); - - pallet_balances::GenesisConfig:: { balances: vec![(ALICE, INIT_AMOUNT)] } - .assimilate_storage(&mut t) - .unwrap(); - - let mut ext = sp_io::TestExternalities::new(t); - ext.execute_with(|| System::set_block_number(1)); - ext -} - -static CONTRACT: LazyLock> = LazyLock::new(|| { - const CONTRACT: &str = "contract/target/ink/proxy.wasm"; - if !Path::new(CONTRACT).exists() { +/// Initializing a new contract file if it does not exist. +fn initialize_contract(contract_path: &str) -> Vec { + if !Path::new(contract_path).exists() { use contract_build::*; let manifest_path = ManifestPath::new("contract/Cargo.toml").unwrap(); let args = ExecuteArgs { @@ -86,9 +178,10 @@ static CONTRACT: LazyLock> = LazyLock::new(|| { }; execute(args).unwrap(); } - std::fs::read(CONTRACT).unwrap() -}); + std::fs::read(contract_path).unwrap() +} +/// Instantiating the contract. fn instantiate() -> AccountId { let result = Contracts::bare_instantiate( ALICE, @@ -107,6 +200,7 @@ fn instantiate() -> AccountId { result.account_id } +/// Perform a call to a specified contract. fn call( contract: AccountId, func_id: u32, @@ -130,146 +224,7 @@ fn call( result } +/// Construct the hashed bytes as a selector of function. fn function_selector(name: &str) -> Vec { sp_io::hashing::blake2_256(name.as_bytes())[0..4].to_vec() } - -mod encoding { - 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/pallets/api/src/extension.rs b/pallets/api/src/extension.rs index 57abb524..2021ff50 100644 --- a/pallets/api/src/extension.rs +++ b/pallets/api/src/extension.rs @@ -17,7 +17,8 @@ pub type Extension = pop_chain_extension::Extension; /// Decodes output by prepending bytes from ext_id() + func_id() pub type DecodesAs = Decodes; -/// Prepends bytes from ext_id() + func_id() to prefix the encoded input bytes to determine the versioned output +/// Prepends bytes from ext_id() + func_id() to prefix the encoded input bytes to determine the +/// versioned output pub struct Prepender; impl Processor for Prepender { /// The type of value to be processed. @@ -115,17 +116,20 @@ impl + Debug> Converter } fn func_id(env: &impl Environment) -> u8 { - // TODO: update once the encoding scheme order has been finalised: expected to be env.ext_id().to_le_bytes()[0] + // TODO: update once the encoding scheme order has been finalised: expected to be + // env.ext_id().to_le_bytes()[0] env.func_id().to_le_bytes()[1] } fn module_and_index(env: &impl Environment) -> (u8, u8) { - // TODO: update once the encoding scheme order has been finalised: expected to be env.func_id().to_le_bytes()[0..1] + // TODO: update once the encoding scheme order has been finalised: expected to be + // env.func_id().to_le_bytes()[0..1] let bytes = env.ext_id().to_le_bytes(); (bytes[0], bytes[1]) } fn version(env: &impl Environment) -> u8 { - // TODO: update once the encoding scheme order has been finalised: expected to be env.ext_id().to_le_bytes()[1] + // TODO: update once the encoding scheme order has been finalised: expected to be + // env.ext_id().to_le_bytes()[1] env.func_id().to_le_bytes()[0] } diff --git a/pallets/api/src/fungibles/mod.rs b/pallets/api/src/fungibles/mod.rs index ef5d15ca..dac002e9 100644 --- a/pallets/api/src/fungibles/mod.rs +++ b/pallets/api/src/fungibles/mod.rs @@ -420,7 +420,8 @@ pub mod pallet { AssetsOf::::clear_metadata(origin, asset.into()) } - /// Creates `value` amount of tokens and assigns them to `account`, increasing the total supply. + /// Creates `value` amount of tokens and assigns them to `account`, increasing the total + /// supply. /// /// # Parameters /// - `asset` - The asset to mint. @@ -481,7 +482,8 @@ pub mod pallet { /// The type or result returned. type Result = ReadResult; - /// Determines the weight of the requested read, used to charge the appropriate weight before the read is performed. + /// Determines the weight of the requested read, used to charge the appropriate weight + /// before the read is performed. /// /// # Parameters /// - `request` - The read request. @@ -498,12 +500,10 @@ pub mod pallet { use Read::*; match request { TotalSupply(asset) => ReadResult::TotalSupply(AssetsOf::::total_supply(asset)), - BalanceOf { asset, owner } => { - ReadResult::BalanceOf(AssetsOf::::balance(asset, owner)) - }, - Allowance { asset, owner, spender } => { - ReadResult::Allowance(AssetsOf::::allowance(asset, &owner, &spender)) - }, + BalanceOf { asset, owner } => + ReadResult::BalanceOf(AssetsOf::::balance(asset, owner)), + Allowance { asset, owner, spender } => + ReadResult::Allowance(AssetsOf::::allowance(asset, &owner, &spender)), TokenName(asset) => ReadResult::TokenName( as MetadataInspect< AccountIdOf, >>::name(asset)), diff --git a/pallets/api/src/lib.rs b/pallets/api/src/lib.rs index b3cc533e..d94d1978 100644 --- a/pallets/api/src/lib.rs +++ b/pallets/api/src/lib.rs @@ -15,7 +15,8 @@ pub trait Read { /// The type or result returned. type Result; - /// Determines the weight of the requested read, used to charge the appropriate weight before the read is performed. + /// Determines the weight of the requested read, used to charge the appropriate weight before + /// the read is performed. /// /// # Parameters /// - `request` - The read request. diff --git a/primitives/src/lib.rs b/primitives/src/lib.rs index 30187d8e..e15b142c 100644 --- a/primitives/src/lib.rs +++ b/primitives/src/lib.rs @@ -32,7 +32,8 @@ pub mod v0 { /// The pallet index. index: u8, /// The error within the pallet. - // Supports a single level of nested error only, due to status code type size constraints. + // Supports a single level of nested error only, due to status code type size + // constraints. error: [u8; 2], } = 3, /// At least one consumer is remaining so the account cannot be destroyed. @@ -45,28 +46,32 @@ pub mod v0 { Token(TokenError) = 7, /// An arithmetic error. Arithmetic(ArithmeticError) = 8, - /// The number of transactional layers has been reached, or we are not in a transactional - /// layer. + /// The number of transactional layers has been reached, or we are not in a + /// transactional layer. Transactional(TransactionalError) = 9, - /// Resources exhausted, e.g. attempt to read/write data which is too large to manipulate. + /// Resources exhausted, e.g. attempt to read/write data which is too large to + /// manipulate. Exhausted = 10, /// The state is corrupt; this is generally not going to fix itself. Corruption = 11, - /// Some resource (e.g. a preimage) is unavailable right now. This might fix itself later. + /// Some resource (e.g. a preimage) is unavailable right now. This might fix itself + /// later. Unavailable = 12, /// Root origin is not allowed. RootNotAllowed = 13, /// Decoding failed. DecodingFailed = 254, /// An unknown error occurred. This variant captures any unexpected errors that the - /// contract cannot specifically handle. It is useful for cases where there are breaking - /// changes in the runtime or when an error falls outside the predefined categories. + /// contract cannot specifically handle. It is useful for cases where there are + /// breaking changes in the runtime or when an error falls outside the predefined + /// categories. Unknown { /// The index within the `DispatchError`. dispatch_error_index: u8, /// The index within the `DispatchError` variant (e.g. a `TokenError`). error_index: u8, - /// The specific error code or sub-index, providing additional context (e.g. `error` in `ModuleError`). + /// The specific error code or sub-index, providing additional context (e.g. + /// `error` in `ModuleError`). error: u8, } = 255, } @@ -86,7 +91,8 @@ pub mod v0 { impl From for u32 { fn from(value: Error) -> Self { let mut encoded_error = value.encode(); - // Resize the encoded value to 4 bytes in order to decode the value into a u32 (4 bytes). + // Resize the encoded value to 4 bytes in order to decode the value into a u32 (4 + // bytes). encoded_error.resize(4, 0); u32::from_le_bytes( encoded_error.try_into().expect("qed, resized to 4 bytes line above"), @@ -100,8 +106,8 @@ pub mod v0 { pub enum TokenError { /// Funds are unavailable. FundsUnavailable, - /// Some part of the balance gives the only provider reference to the account and thus cannot - /// be (re)moved. + /// Some part of the balance gives the only provider reference to the account and thus + /// cannot be (re)moved. OnlyProvider, /// Account cannot exist with the funds that would be given. BelowMinimum, diff --git a/runtime/devnet/src/config/api/mod.rs b/runtime/devnet/src/config/api/mod.rs index 88ade75f..7fe23fc6 100644 --- a/runtime/devnet/src/config/api/mod.rs +++ b/runtime/devnet/src/config/api/mod.rs @@ -4,9 +4,8 @@ use crate::{ use codec::Decode; use cumulus_primitives_core::Weight; use frame_support::traits::Contains; -use pallet_api::extension::*; pub(crate) use pallet_api::Extension; -use pallet_api::Read; +use pallet_api::{extension::*, Read}; use sp_core::ConstU8; use sp_runtime::DispatchError; use sp_std::vec::Vec; @@ -31,7 +30,8 @@ impl Readable for RuntimeRead { /// The corresponding type carrying the result of the query for runtime state. type Result = RuntimeResult; - /// Determines the weight of the read, used to charge the appropriate weight before the read is performed. + /// Determines the weight of the read, used to charge the appropriate weight before the read is + /// performed. fn weight(&self) -> Weight { match self { RuntimeRead::Fungibles(key) => fungibles::Pallet::weight(key), @@ -125,14 +125,14 @@ impl Contains for Filter { matches!( c, RuntimeCall::Fungibles( - transfer { .. } - | transfer_from { .. } - | approve { .. } | increase_allowance { .. } - | decrease_allowance { .. } - | create { .. } | set_metadata { .. } - | start_destroy { .. } - | clear_metadata { .. } - | mint { .. } | burn { .. } + transfer { .. } | + transfer_from { .. } | + approve { .. } | increase_allowance { .. } | + decrease_allowance { .. } | + create { .. } | set_metadata { .. } | + start_destroy { .. } | + clear_metadata { .. } | + mint { .. } | burn { .. } ) ) } @@ -144,10 +144,12 @@ impl Contains for Filter { matches!( r, RuntimeRead::Fungibles( - TotalSupply(..) - | BalanceOf { .. } | Allowance { .. } - | TokenName(..) | TokenSymbol(..) - | TokenDecimals(..) | AssetExists(..) + TotalSupply(..) | + BalanceOf { .. } | + Allowance { .. } | + TokenName(..) | TokenSymbol(..) | + TokenDecimals(..) | + AssetExists(..) ) ) } diff --git a/runtime/devnet/src/config/api/versioning.rs b/runtime/devnet/src/config/api/versioning.rs index 7e07f31b..50041340 100644 --- a/runtime/devnet/src/config/api/versioning.rs +++ b/runtime/devnet/src/config/api/versioning.rs @@ -101,7 +101,8 @@ impl From for V0Error { // Mappings exist here to avoid taking a dependency of sp_runtime on pop-primitives Self(match error { Other(_message) => { - // Note: lossy conversion: message not used due to returned contract status code size limitation + // Note: lossy conversion: message not used due to returned contract status code + // size limitation Error::Other }, CannotLookup => Error::CannotLookup, @@ -110,13 +111,14 @@ impl From for V0Error { // Note: message not used let ModuleError { index, error, message: _message } = error; // Map `pallet-contracts::Error::DecodingFailed` to `Error::DecodingFailed` - if index as usize - == ::index() - && error == DECODING_FAILED_ERROR + if index as usize == + ::index() && + error == DECODING_FAILED_ERROR { Error::DecodingFailed } else { - // Note: lossy conversion of error value due to returned contract status code size limitation + // Note: lossy conversion of error value due to returned contract status code + // size limitation Error::Module { index, error: [error[0], error[1]] } } },