From f8604bdb77d60a85332130f6eeb00e96f41ec646 Mon Sep 17 00:00:00 2001 From: Michael Mueller Date: Tue, 1 Mar 2022 07:04:20 +0100 Subject: [PATCH 1/2] Implement chain extension testing in experimental off-chain env --- crates/engine/src/chain_extension.rs | 94 +++++++++++++++++++ crates/engine/src/ext.rs | 30 ++++-- crates/engine/src/lib.rs | 2 + .../engine/experimental_off_chain/impls.rs | 18 ++-- .../engine/experimental_off_chain/test_api.rs | 15 +++ examples/rand-extension/Cargo.toml | 1 + examples/rand-extension/lib.rs | 49 ++++++++++ 7 files changed, 196 insertions(+), 13 deletions(-) create mode 100644 crates/engine/src/chain_extension.rs diff --git a/crates/engine/src/chain_extension.rs b/crates/engine/src/chain_extension.rs new file mode 100644 index 00000000000..c097914d2b0 --- /dev/null +++ b/crates/engine/src/chain_extension.rs @@ -0,0 +1,94 @@ +// Copyright 2018-2022 Parity Technologies (UK) Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use super::Error; +use derive_more::From; +use std::collections::{ + hash_map::Entry, + HashMap, +}; + +/// Chain extension registry. +/// +/// Allows to register chain extension methods and call them. +pub struct ChainExtensionHandler { + /// The currently registered runtime call handler. + registered: HashMap>, + /// The output buffer used and reused for chain extension method call results. + output: Vec, +} + +/// The unique ID of the registered chain extension method. +#[derive( + Debug, From, scale::Encode, scale::Decode, PartialEq, Eq, PartialOrd, Ord, Hash, +)] +pub struct ExtensionId(u32); + +/// Types implementing this trait can be used as chain extensions. +/// +/// This trait is only useful for testing contract via the off-chain environment. +pub trait ChainExtension { + /// The static function ID of the chain extension. + /// + /// # Note + /// + /// This is expected to return a constant value. + fn func_id(&self) -> u32; + + /// Calls the chain extension with the given input. + /// + /// Returns an error code and may fill the `output` buffer with a SCALE encoded result. + #[allow(clippy::ptr_arg)] + fn call(&mut self, input: &[u8], output: &mut Vec) -> u32; +} + +impl ChainExtensionHandler { + /// Creates a new chain extension handler. + /// + /// Initialized with an empty set of chain extensions. + pub fn new() -> Self { + Self { + registered: HashMap::new(), + output: Vec::new(), + } + } + + /// Resets the chain extension handler to uninitialized state. + pub fn reset(&mut self) { + self.registered.clear(); + self.output.clear(); + } + + /// Register a new chain extension. + pub fn register(&mut self, extension: Box) { + let func_id = extension.func_id(); + self.registered + .insert(ExtensionId::from(func_id), extension); + } + + /// Evaluates the chain extension with the given parameters. + /// + /// Upon success returns the values returned by the evaluated chain extension. + pub fn eval(&mut self, func_id: u32, input: &[u8]) -> Result<(u32, &[u8]), Error> { + self.output.clear(); + let extension_id = ExtensionId::from(func_id); + match self.registered.entry(extension_id) { + Entry::Occupied(occupied) => { + let status_code = occupied.into_mut().call(input, &mut self.output); + Ok((status_code, &mut self.output)) + } + Entry::Vacant(_vacant) => Err(Error::UnregisteredChainExtension.into()), + } + } +} diff --git a/crates/engine/src/ext.rs b/crates/engine/src/ext.rs index 60bba97f687..3279ffb2874 100644 --- a/crates/engine/src/ext.rs +++ b/crates/engine/src/ext.rs @@ -18,6 +18,7 @@ //! for more information. use crate::{ + chain_extension::ChainExtensionHandler, database::Database, exec_context::ExecContext, test_api::{ @@ -125,6 +126,8 @@ pub struct Engine { pub(crate) debug_info: DebugInfo, /// The chain specification. pub chain_spec: ChainSpec, + /// Handler for registered chain extensions. + pub chain_extension_handler: ChainExtensionHandler, } /// The chain specification. @@ -165,6 +168,7 @@ impl Engine { exec_context: ExecContext::new(), debug_info: DebugInfo::new(), chain_spec: ChainSpec::default(), + chain_extension_handler: ChainExtensionHandler::new(), } } } @@ -445,15 +449,27 @@ impl Engine { set_output(output, &rng_bytes[..]) } + /// Calls the chain extension method registered at `func_id` with `input`. pub fn call_chain_extension( &mut self, - _func_id: u32, - _input: &[u8], - _output: &mut &mut [u8], - ) -> u32 { - unimplemented!( - "off-chain environment does not yet support `call_chain_extension`" - ); + func_id: u32, + input: &[u8], + output: &mut &mut [u8], + ) { + let encoded_input = input.encode(); + let (status_code, out) = self + .chain_extension_handler + .eval(func_id, &encoded_input) + .unwrap_or_else(|error| { + panic!( + "Encountered unexpected missing chain extension method: {:?}", + error + ); + }); + let signature = + let res = (status_code, out); + let decoded: Vec = scale::Encode::encode(&res); + set_output(output, &decoded[..]) } /// Recovers the compressed ECDSA public key for given `signature` and `message_hash`, diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index 36971aba74a..f786371d1ef 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -15,6 +15,7 @@ pub mod ext; pub mod test_api; +mod chain_extension; mod database; mod exec_context; mod hashing; @@ -23,6 +24,7 @@ mod types; #[cfg(test)] mod tests; +pub use chain_extension::ChainExtension; pub use types::AccountError; use derive_more::From; diff --git a/crates/env/src/engine/experimental_off_chain/impls.rs b/crates/env/src/engine/experimental_off_chain/impls.rs index 7eb83397d3f..f335fe31513 100644 --- a/crates/env/src/engine/experimental_off_chain/impls.rs +++ b/crates/env/src/engine/experimental_off_chain/impls.rs @@ -305,12 +305,18 @@ impl EnvBackend for EnvInstance { let enc_input = &scale::Encode::encode(input)[..]; let mut output: [u8; BUFFER_SIZE] = [0; BUFFER_SIZE]; - status_to_result(self.engine.call_chain_extension( - func_id, - enc_input, - &mut &mut output[..], - ))?; - let decoded = decode_to_result(&output[..])?; + self.engine + .call_chain_extension(func_id, enc_input, &mut &mut output[..]); + let (status, out): (u32, Vec) = scale::Decode::decode(&mut &output[..]) + .unwrap_or_else(|error| { + panic!( + "could not decode `call_chain_extension` output: {:?}", + error + ) + }); + + status_to_result(status)?; + let decoded = decode_to_result(&out[..])?; Ok(decoded) } } diff --git a/crates/env/src/engine/experimental_off_chain/test_api.rs b/crates/env/src/engine/experimental_off_chain/test_api.rs index 6d827e30fa0..525bf572139 100644 --- a/crates/env/src/engine/experimental_off_chain/test_api.rs +++ b/crates/env/src/engine/experimental_off_chain/test_api.rs @@ -26,6 +26,8 @@ use core::fmt::Debug; use ink_engine::test_api::RecordedDebugMessages; use std::panic::UnwindSafe; +pub use ink_engine::ChainExtension; + /// Record for an emitted event. #[derive(Clone)] pub struct EmittedEvent { @@ -82,6 +84,19 @@ where }) } +/// Registers a new chain extension. +pub fn register_chain_extension(extension: E) +where + E: ink_engine::ChainExtension + 'static, +{ + ::on_instance(|instance| { + instance + .engine + .chain_extension_handler + .register(Box::new(extension)); + }) +} + /// Set the entropy hash of the current block. /// /// # Note diff --git a/examples/rand-extension/Cargo.toml b/examples/rand-extension/Cargo.toml index fb87b6569be..e2e176173fc 100755 --- a/examples/rand-extension/Cargo.toml +++ b/examples/rand-extension/Cargo.toml @@ -30,3 +30,4 @@ std = [ "scale-info/std", ] ink-as-dependency = [] +ink-experimental-engine = ["ink_env/ink-experimental-engine"] diff --git a/examples/rand-extension/lib.rs b/examples/rand-extension/lib.rs index bf6740937ae..720b8754769 100755 --- a/examples/rand-extension/lib.rs +++ b/examples/rand-extension/lib.rs @@ -153,4 +153,53 @@ mod rand_extension { assert_eq!(rand_extension.get(), [1; 32]); } } + + /// Unit tests in Rust are normally defined within such a `#[cfg(test)]` + #[cfg(test)] + #[cfg(feature = "ink-experimental-engine")] + mod tests_experimental_engine { + /// Imports all the definitions from the outer scope so we can use them here. + use super::*; + use ink_lang as ink; + + /// We test if the default constructor does its job. + #[ink::test] + fn default_works() { + let rand_extension = RandExtension::default(); + assert_eq!(rand_extension.get(), [0; 32]); + } + + #[ink::test] + fn chain_extension_works() { + // given + struct MockedExtension; + impl ink_env::test::ChainExtension for MockedExtension { + /// The static function id of the chain extension. + fn func_id(&self) -> u32 { + 1101 + } + + /// The chain extension is called with the given input. + /// + /// Returns an error code and may fill the `output` buffer with a + /// SCALE encoded result. The error code is taken from the + /// `ink_env::chain_extension::FromStatusCode` implementation for + /// `RandomReadErr`. + fn call(&mut self, _input: &[u8], output: &mut Vec) -> u32 { + let ret: [u8; 32] = [1; 32]; + scale::Encode::encode_to(&ret, output); + 0 + } + } + ink_env::test::register_chain_extension(MockedExtension); + let mut rand_extension = RandExtension::default(); + assert_eq!(rand_extension.get(), [0; 32]); + + // when + rand_extension.update([0_u8; 32]).expect("update must work"); + + // then + assert_eq!(rand_extension.get(), [1; 32]); + } + } } From 4745a461542a3d7451bbc3c7df753b78d327a525 Mon Sep 17 00:00:00 2001 From: Michael Mueller Date: Tue, 1 Mar 2022 07:52:13 +0100 Subject: [PATCH 2/2] Make `clippy` happy --- crates/engine/src/chain_extension.rs | 8 +++++++- crates/engine/src/ext.rs | 1 - 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/crates/engine/src/chain_extension.rs b/crates/engine/src/chain_extension.rs index c097914d2b0..9a32a6e3bc9 100644 --- a/crates/engine/src/chain_extension.rs +++ b/crates/engine/src/chain_extension.rs @@ -53,6 +53,12 @@ pub trait ChainExtension { fn call(&mut self, input: &[u8], output: &mut Vec) -> u32; } +impl Default for ChainExtensionHandler { + fn default() -> Self { + ChainExtensionHandler::new() + } +} + impl ChainExtensionHandler { /// Creates a new chain extension handler. /// @@ -88,7 +94,7 @@ impl ChainExtensionHandler { let status_code = occupied.into_mut().call(input, &mut self.output); Ok((status_code, &mut self.output)) } - Entry::Vacant(_vacant) => Err(Error::UnregisteredChainExtension.into()), + Entry::Vacant(_vacant) => Err(Error::UnregisteredChainExtension), } } } diff --git a/crates/engine/src/ext.rs b/crates/engine/src/ext.rs index 3279ffb2874..4d0a3318b3d 100644 --- a/crates/engine/src/ext.rs +++ b/crates/engine/src/ext.rs @@ -466,7 +466,6 @@ impl Engine { error ); }); - let signature = let res = (status_code, out); let decoded: Vec = scale::Encode::encode(&res); set_output(output, &decoded[..])