Unverified Commit 1a19f937 authored by Michael Müller's avatar Michael Müller Committed by GitHub
Browse files

Implement MVP for new off-chain testing engine (#712)

* Add `engine` crate

* Add `env_types` crate

* Adapt `env`, `lang` and `storage`

* Adapt examples

* Adapt CI

* Symlink license and readme

* Throw `TypedEncoded` out of `engine`

* Improve Erc20

* Bump versions to rc3

* Fix clippy error: Manual implementation of `Option::map` (#717)

* Implement comments

* Fix yml

* Improve structure

* Add tests

* Fix function signature

* Get rid of `engine`s singleton

* Revert instantiate stuff

* Implement review comments

* Make `Storage` non-generic

* Improve API for emmitted events

* Migrate to `panic_any`

* Clean up import

* Import `panic_any`

* Implement comments

* Fix param

* Use type

* Store balances in chain storage

* Fix tests

* Use individual storage per contract

* Implement comments
parent 19200ddc
Pipeline #137593 passed with stages
in 28 minutes and 53 seconds
......@@ -124,6 +124,12 @@ test:
- cargo test --verbose --all-features --no-fail-fast --workspace
- cargo test --verbose --all-features --no-fail-fast --workspace --doc
# Just needed as long as we have the `ink-experimental-engine` feature.
# We do not invoke `--all-features` here -- this would imply the feature
# `ink-experimental-engine`. So in order to still run the tests without the
# experimental engine feature we need this command.
- cargo test --verbose --features std --no-fail-fast --workspace
docs:
stage: workspace
<<: *docker-env
......@@ -179,6 +185,13 @@ codecov:
# RUSTFLAGS are the cause target cache can't be used here
- cargo build --verbose --all-features --workspace
- cargo test --verbose --all-features --no-fail-fast --workspace
# Just needed as long as we have the `ink-experimental-engine` feature.
# We must additionally run the coverage without `--all-features` here -- this
# would imply the feature `ink-experimental-engine`. So in order to still run
# the tests without the experimental engine feature we need this command.
- cargo test --verbose --features std --no-fail-fast --workspace
# coverage with branches
- grcov . --source-dir . --output-type lcov --llvm --branch --ignore-not-existing
--ignore "/*" --ignore "tests/*" --output-path lcov-w-branch.info
......@@ -240,6 +253,19 @@ examples-test:
cargo test --verbose --manifest-path examples/delegator/${contract}/Cargo.toml;
done
examples-test-experimental-engine:
stage: examples
<<: *docker-env
needs:
- job: clippy-std
artifacts: false
script:
# We test only the examples for which the tests have already been migrated to
# use the experimental engine.
- cargo test --no-default-features --features std, ink-experimental-engine --verbose --manifest-path examples/erc20/Cargo.toml
- cargo test --no-default-features --features std, ink-experimental-engine --verbose --manifest-path examples/contract-terminate/Cargo.toml
- cargo test --no-default-features --features std, ink-experimental-engine --verbose --manifest-path examples/contract-transfer/Cargo.toml
examples-fmt:
stage: examples
<<: *docker-env
......
[package]
name = "ink_engine"
version = "3.0.0-rc3"
authors = ["Parity Technologies <admin@parity.io>", "Michael Müller <michi@parity.io>"]
edition = "2018"
license = "Apache-2.0"
readme = "README.md"
repository = "https://github.com/paritytech/ink"
documentation = "https://docs.rs/ink_engine/"
homepage = "https://www.parity.io/"
description = "[ink!] Experimental off-chain environment for testing."
keywords = ["wasm", "parity", "webassembly", "blockchain", "edsl"]
categories = ["no-std", "embedded"]
include = ["Cargo.toml", "src/**/*.rs", "README.md", "LICENSE"]
[dependencies]
scale = { package = "parity-scale-codec", version = "2.0", default-features = false, features = ["derive", "full"] }
derive_more = { version = "0.99", default-features = false, features = ["from", "display"] }
sha2 = { version = "0.9" }
sha3 = { version = "0.9" }
blake2 = { version = "0.9" }
[features]
default = ["std"]
std = [
"scale/std",
]
../../LICENSE
\ No newline at end of file
../../README.md
\ No newline at end of file
// Copyright 2018-2021 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 crate::types::Balance;
use scale::KeyedVec;
use std::collections::HashMap;
const BALANCE_OF: &[u8] = b"balance:";
const STORAGE_OF: &[u8] = b"contract-storage:";
/// Returns the database key under which to find the balance for account `who`.
pub fn balance_of_key(who: &[u8]) -> [u8; 32] {
let keyed = who.to_vec().to_keyed_vec(BALANCE_OF);
let mut hashed_key: [u8; 32] = [0; 32];
super::hashing::blake2b_256(&keyed[..], &mut hashed_key);
hashed_key
}
/// Returns the database key under which to find the balance for account `who`.
pub fn storage_of_contract_key(who: &[u8], key: &[u8]) -> [u8; 32] {
let keyed = who.to_vec().to_keyed_vec(key).to_keyed_vec(STORAGE_OF);
let mut hashed_key: [u8; 32] = [0; 32];
super::hashing::blake2b_256(&keyed[..], &mut hashed_key);
hashed_key
}
/// The chain database.
///
/// Everything is stored in here: accounts, balances, contract storage, etc..
/// Just like in Substrate a prefix hash is computed for every contract.
#[derive(Default)]
pub struct Database {
hmap: HashMap<Vec<u8>, Vec<u8>>,
}
impl Database {
/// Creates a new database instance.
pub fn new() -> Self {
Database {
hmap: HashMap::new(),
}
}
/// Returns the amount of entries in the database.
#[cfg(test)]
fn len(&self) -> usize {
self.hmap.len()
}
/// Returns a reference to the value corresponding to the key.
fn get(&self, key: &[u8]) -> Option<&Vec<u8>> {
self.hmap.get(key)
}
/// Returns a reference to the value corresponding to the key.
pub fn get_from_contract_storage(
&self,
account_id: &[u8],
key: &[u8],
) -> Option<&Vec<u8>> {
let hashed_key = storage_of_contract_key(&account_id, key);
self.hmap.get(&hashed_key.to_vec())
}
/// Inserts `value` into the contract storage of `account_id` at storage key `key`.
pub fn insert_into_contract_storage(
&mut self,
account_id: &[u8],
key: &[u8],
value: Vec<u8>,
) -> Option<Vec<u8>> {
let hashed_key = storage_of_contract_key(&account_id, key);
self.hmap.insert(hashed_key.to_vec(), value)
}
/// Removes the value at the contract storage of `account_id` at storage key `key`.
pub fn remove_contract_storage(
&mut self,
account_id: &[u8],
key: &[u8],
) -> Option<Vec<u8>> {
let hashed_key = storage_of_contract_key(&account_id, key);
self.hmap.remove(&hashed_key.to_vec())
}
/// Removes a key from the storage, returning the value at the key if the key
/// was previously in storage.
pub fn remove(&mut self, key: &[u8]) -> Option<Vec<u8>> {
self.hmap.remove(key)
}
/// Sets the value of the entry, and returns the entry's old value.
pub fn insert(&mut self, key: Vec<u8>, value: Vec<u8>) -> Option<Vec<u8>> {
self.hmap.insert(key, value)
}
/// Clears the database, removing all key-value pairs.
pub fn clear(&mut self) {
self.hmap.clear();
}
/// Returns the balance of `account_id`, if available.
pub fn get_balance(&self, account_id: &[u8]) -> Option<Balance> {
let hashed_key = balance_of_key(&account_id);
self.get(&hashed_key).map(|encoded_balance| {
scale::Decode::decode(&mut &encoded_balance[..])
.expect("unable to decode balance from database")
})
}
/// Sets the balance of `account_id` to `new_balance`.
pub fn set_balance(&mut self, account_id: &[u8], new_balance: Balance) {
let hashed_key = balance_of_key(&account_id);
let encoded_balance = scale::Encode::encode(&new_balance);
self.hmap
.entry(hashed_key.to_vec())
.and_modify(|v| *v = encoded_balance.clone())
.or_insert(encoded_balance);
}
}
#[cfg(test)]
mod tests {
use super::Database;
#[test]
fn basic_operations() {
let mut database = Database::new();
let key1 = vec![42];
let key2 = vec![43];
let val1 = vec![44];
let val2 = vec![45];
let val3 = vec![46];
assert_eq!(database.len(), 0);
assert_eq!(database.get(&key1), None);
assert_eq!(database.insert(key1.clone(), val1.clone()), None);
assert_eq!(database.get(&key1), Some(&val1));
assert_eq!(
database.insert(key1.clone(), val2.clone()),
Some(val1.clone())
);
assert_eq!(database.get(&key1), Some(&val2));
assert_eq!(database.insert(key2.clone(), val3.clone()), None);
assert_eq!(database.len(), 2);
assert_eq!(database.remove(&key2), Some(val3));
assert_eq!(database.len(), 1);
database.clear();
assert_eq!(database.len(), 0);
}
#[test]
fn contract_storage() {
let account_id = vec![1; 32];
let mut storage = Database::new();
let key1 = vec![42];
let key2 = vec![43];
let val1 = vec![44];
let val2 = vec![45];
let val3 = vec![46];
assert_eq!(storage.len(), 0);
assert_eq!(storage.get_from_contract_storage(&account_id, &key1), None);
assert_eq!(
storage.insert_into_contract_storage(&account_id, &key1, val1.clone()),
None
);
assert_eq!(
storage.get_from_contract_storage(&account_id, &key1),
Some(&val1)
);
assert_eq!(
storage.insert_into_contract_storage(&account_id, &key1, val2.clone()),
Some(val1.clone())
);
assert_eq!(
storage.get_from_contract_storage(&account_id, &key1),
Some(&val2)
);
assert_eq!(
storage.insert_into_contract_storage(&account_id, &key2, val3.clone()),
None
);
assert_eq!(storage.len(), 2);
assert_eq!(
storage.remove_contract_storage(&account_id, &key2),
Some(val3)
);
assert_eq!(storage.len(), 1);
assert_eq!(
storage.remove_contract_storage(&account_id, &key1),
Some(val2)
);
assert_eq!(storage.len(), 0);
}
}
// Copyright 2018-2021 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::types::{
AccountId,
Balance,
};
/// The context of a contract execution.
#[cfg_attr(test, derive(Debug, PartialEq, Eq))]
pub struct ExecContext {
/// The caller of the contract execution. Might be user or another contract.
///
/// We don't know the specifics of the AccountId ‒ like how many bytes or what
/// type of default `AccountId` makes sense ‒ they are left to be initialized
/// by the crate which uses the `engine`. Methods which require a caller might
/// panic when it has not been set.
pub caller: Option<AccountId>,
/// The callee of the contract execution. Might be user or another contract.
///
/// We don't know the specifics of the AccountId ‒ like how many bytes or what
/// type of default `AccountId` makes sense ‒ they are left to be initialized
/// by the crate which uses the `engine`. Methods which require a callee might
/// panic when it has not been set.
pub callee: Option<AccountId>,
/// The value transferred to the contract as part of the call.
pub value_transferred: Balance,
}
#[allow(clippy::new_without_default)]
impl ExecContext {
/// Creates a new execution context.
pub fn new() -> Self {
Self {
caller: None,
callee: None,
value_transferred: 0,
}
}
/// Returns the callee.
pub fn callee(&self) -> Vec<u8> {
self.callee
.as_ref()
.expect("no callee has been set")
.as_bytes()
.into()
}
/// Resets the execution context
pub fn reset(&mut self) {
self.caller = None;
self.callee = None;
self.value_transferred = Default::default();
}
}
#[cfg(test)]
mod tests {
use super::{
AccountId,
ExecContext,
};
#[test]
fn basic_operations() {
let mut exec_cont = ExecContext::new();
exec_cont.callee = Some(AccountId::from_bytes(&[13]));
exec_cont.caller = Some(AccountId::from_bytes(&[14]));
exec_cont.value_transferred = 15;
assert_eq!(exec_cont.callee(), vec![13]);
exec_cont.reset();
assert_eq!(exec_cont, ExecContext::new());
}
}
// Copyright 2018-2021 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.
//! Provides the same interface as Substrate's FRAME `contract` module.
//!
//! See [the documentation for the `contract` module](https://docs.rs/crate/pallet-contracts)
//! for more information.
use crate::{
database::Database,
exec_context::ExecContext,
test_api::{
DebugInfo,
EmittedEvent,
},
types::{
AccountId,
Key,
},
};
use std::panic::panic_any;
type Result = core::result::Result<(), Error>;
macro_rules! define_error_codes {
(
$(
$( #[$attr:meta] )*
$name:ident = $discr:literal,
)*
) => {
/// Every error that can be returned to a contract when it calls any of the host functions.
#[cfg_attr(test, derive(PartialEq, Eq))]
#[derive(Debug)]
#[repr(u32)]
pub enum Error {
$(
$( #[$attr] )*
$name = $discr,
)*
/// Returns if an unknown error was received from the host module.
UnknownError,
}
impl From<ReturnCode> for Result {
#[inline]
fn from(return_code: ReturnCode) -> Self {
match return_code.0 {
0 => Ok(()),
$(
$discr => Err(Error::$name),
)*
_ => Err(Error::UnknownError),
}
}
}
};
}
define_error_codes! {
/// The called function trapped and has its state changes reverted.
/// In this case no output buffer is returned.
/// Can only be returned from `seal_call` and `seal_instantiate`.
CalleeTrapped = 1,
/// The called function ran to completion but decided to revert its state.
/// An output buffer is returned when one was supplied.
/// Can only be returned from `seal_call` and `seal_instantiate`.
CalleeReverted = 2,
/// The passed key does not exist in storage.
KeyNotFound = 3,
/// Transfer failed because it would have brought the sender's total balance
/// below the subsistence threshold.
BelowSubsistenceThreshold = 4,
/// Transfer failed for other not further specified reason. Most probably
/// reserved or locked balance of the sender that was preventing the transfer.
TransferFailed = 5,
/// The newly created contract is below the subsistence threshold after executing
/// its constructor so no usable contract instance will be created.
NewContractNotFunded = 6,
/// No code could be found at the supplied code hash.
CodeNotFound = 7,
/// The account that was called is either no contract (e.g. user account) or is a tombstone.
NotCallable = 8,
}
/// The raw return code returned by the host side.
#[repr(transparent)]
pub struct ReturnCode(u32);
impl ReturnCode {
/// Returns the raw underlying `u32` representation.
pub fn into_u32(self) -> u32 {
self.0
}
}
/// The off-chain engine.
pub struct Engine {
/// The environment database.
pub database: Database,
/// The current execution context.
pub exec_context: ExecContext,
/// Recorder for relevant interactions with the engine.
/// This is specifically about debug info. This info is
/// not available in the `contracts` pallet.
pub(crate) debug_info: DebugInfo,
}
impl Engine {
// Creates a new `Engine instance.
pub fn new() -> Self {
Self {
database: Database::new(),
exec_context: ExecContext::new(),
debug_info: DebugInfo::new(),
}
}
}
impl Default for Engine {
fn default() -> Self {
Self::new()
}
}
impl Engine {
/// Transfers value from the contract to the destination account.
pub fn transfer(&mut self, account_id: &[u8], mut value: &[u8]) -> Result {
// Note that a transfer of `0` is allowed here
let increment = <u128 as scale::Decode>::decode(&mut value)
.map_err(|_| Error::TransferFailed)?;
let dest = account_id.to_vec();
// Note that the destination account does not have to exist
let dest_old_balance = self.get_balance(dest.clone()).unwrap_or_default();
let contract = self.get_callee();
let contract_old_balance = self
.get_balance(contract.clone())
.map_err(|_| Error::TransferFailed)?;
self.database
.set_balance(&contract, contract_old_balance - increment);
self.database
.set_balance(&dest, dest_old_balance + increment);
Ok(())
}
/// Deposits an event identified by the supplied topics and data.
pub fn deposit_event(&mut self, topics: &[u8], data: &[u8]) {
// The first byte contains the number of topics in the slice
let topics_count: scale::Compact<u32> = scale::Decode::decode(&mut &topics[0..1])
.expect("decoding number of topics failed");
let topics_count = topics_count.0 as usize;
let topics_vec = if topics_count > 0 {
// The rest of the slice contains the topics
let topics = &topics[1..];
let bytes_per_topic = topics.len() / topics_count;
let topics_vec: Vec<Vec<u8>> = topics
.chunks(bytes_per_topic)
.map(|chunk| chunk.to_vec())
.collect();
assert_eq!(topics_count, topics_vec.len());
topics_vec
} else {
Vec::new()
};
self.debug_info.record_event(EmittedEvent {
topics: topics_vec,
data: data.to_vec(),
});
}
/// Writes the encoded value into the storage at the given key.
pub fn set_storage(&mut self, key: &[u8; 32], encoded_value: &[u8]) {
let callee = self.get_callee();
let account_id = AccountId::from_bytes(&callee[..]);
self.debug_info.inc_writes(account_id.clone());
self.debug_info
.record_cell_for_account(account_id, key.to_vec());
// We ignore if storage is already set for this key
let _ = self.database.insert_into_contract_storage(
&callee,
key,
encoded_value.to_vec(),
);
}
/// Returns the decoded contract storage at the key if any.
pub fn get_storage(&mut self, key: &[u8; 32], output: &mut &mut [u8]) -> Result {
let callee = self.get_callee();
let account_id = AccountId::from_bytes(&callee[..]);
self.debug_info.inc_reads(account_id);
match self.database.get_from_contract_storage(&callee, key) {
Some(val) => {
set_output(output, val);
Ok(())
}
None => Err(Error::KeyNotFound),
}
}
/// Removes the storage entries at the given key.
pub fn clear_storage(&mut self, key: &[u8; 32]) {
let callee = self.get_callee();
let account_id = AccountId::from_bytes(&callee[..]);
self.debug_info.inc_writes(account_id.clone());
let _ = self
.debug_info
.remove_cell_for_account(account_id, key.to_vec());
let _ = self.database.remove_contract_storage(&callee, key);
}
/// Remove the calling account and transfer remaining balance.
///
/// This function never returns. Either the termination was successful and the