Skip to content

Commit

Permalink
Implement utility methods for experimental off-chain testing env (#1143)
Browse files Browse the repository at this point in the history
  • Loading branch information
cmichi authored Feb 26, 2022
1 parent 3527918 commit 34bd952
Show file tree
Hide file tree
Showing 6 changed files with 183 additions and 26 deletions.
2 changes: 2 additions & 0 deletions crates/engine/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ sha2 = { version = "0.10" }
sha3 = { version = "0.10" }
blake2 = { version = "0.10" }

rand = { version = "0.8" }

# ECDSA for the off-chain environment.
secp256k1 = { version = "0.21.2", features = ["recovery", "global-context"], optional = true }

Expand Down
38 changes: 30 additions & 8 deletions crates/engine/src/exec_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@
use super::types::{
AccountId,
Balance,
BlockNumber,
BlockTimestamp,
Hash,
};
use rand::Rng;

/// The context of a contract execution.
#[cfg_attr(test, derive(Debug, PartialEq, Eq))]
Expand All @@ -36,18 +40,34 @@ pub struct ExecContext {
pub callee: Option<AccountId>,
/// The value transferred to the contract as part of the call.
pub value_transferred: Balance,
/// The current block number.
pub block_number: BlockNumber,
/// The current block timestamp.
pub block_timestamp: BlockTimestamp,
/// The randomization entropy for a block.
pub entropy: Hash,
}

#[allow(clippy::new_without_default)]
impl ExecContext {
/// Creates a new execution context.
pub fn new() -> Self {
impl Default for ExecContext {
fn default() -> Self {
let mut entropy: [u8; 32] = Default::default();
rand::thread_rng().fill(entropy.as_mut());
Self {
caller: None,
callee: None,
value_transferred: 0,
block_number: 0,
block_timestamp: 0,
entropy,
}
}
}

impl ExecContext {
/// Creates a new execution context.
pub fn new() -> Self {
Default::default()
}

/// Returns the callee.
pub fn callee(&self) -> Vec<u8> {
Expand All @@ -60,9 +80,7 @@ impl ExecContext {

/// Resets the execution context
pub fn reset(&mut self) {
self.caller = None;
self.callee = None;
self.value_transferred = Default::default();
*self = Default::default();
}
}

Expand All @@ -83,6 +101,10 @@ mod tests {
assert_eq!(exec_cont.callee(), vec![13]);

exec_cont.reset();
assert_eq!(exec_cont, ExecContext::new());
exec_cont.entropy = Default::default();

let mut new_exec_cont = ExecContext::new();
new_exec_cont.entropy = Default::default();
assert_eq!(exec_cont, new_exec_cont);
}
}
101 changes: 90 additions & 11 deletions crates/engine/src/ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,17 @@ use crate::{
DebugInfo,
EmittedEvent,
},
types::AccountId,
types::{
AccountId,
Balance,
BlockTimestamp,
},
};
use rand::{
Rng,
SeedableRng,
};
use scale::Encode;
use std::panic::panic_any;

type Result = core::result::Result<(), Error>;
Expand Down Expand Up @@ -114,6 +123,38 @@ pub struct Engine {
/// This is specifically about debug info. This info is
/// not available in the `contracts` pallet.
pub(crate) debug_info: DebugInfo,
/// The chain specification.
pub chain_spec: ChainSpec,
}

/// The chain specification.
pub struct ChainSpec {
/// The current gas price.
pub gas_price: Balance,
/// The minimum value an account of the chain must have
/// (i.e. the chain's existential deposit).
pub minimum_balance: Balance,
/// The targeted block time.
pub block_time: BlockTimestamp,
}

/// The default values for the chain specification are:
///
/// * `gas_price`: 100
/// * `minimum_balance`: 42
/// * `block_time`: 6
///
/// There is no particular reason behind choosing them this way.
impl Default for ChainSpec {
fn default() -> Self {
// Those are the default values which were chosen in
// the original off-chain testing environment.
Self {
gas_price: 100,
minimum_balance: 42,
block_time: 6,
}
}
}

impl Engine {
Expand All @@ -123,6 +164,7 @@ impl Engine {
database: Database::new(),
exec_context: ExecContext::new(),
debug_info: DebugInfo::new(),
chain_spec: ChainSpec::default(),
}
}
}
Expand Down Expand Up @@ -317,20 +359,30 @@ impl Engine {
super::hashing::keccak_256(input, output);
}

pub fn block_number(&self, _output: &mut &mut [u8]) {
unimplemented!("off-chain environment does not yet support `block_number`");
/// Returns the current block number.
pub fn block_number(&self, output: &mut &mut [u8]) {
let block_number: Vec<u8> =
scale::Encode::encode(&self.exec_context.block_number);
set_output(output, &block_number[..])
}

pub fn block_timestamp(&self, _output: &mut &mut [u8]) {
unimplemented!("off-chain environment does not yet support `block_timestamp`");
/// Returns the timestamp of the current block.
pub fn block_timestamp(&self, output: &mut &mut [u8]) {
let block_timestamp: Vec<u8> =
scale::Encode::encode(&self.exec_context.block_timestamp);
set_output(output, &block_timestamp[..])
}

pub fn gas_left(&self, _output: &mut &mut [u8]) {
unimplemented!("off-chain environment does not yet support `gas_left`");
}

pub fn minimum_balance(&self, _output: &mut &mut [u8]) {
unimplemented!("off-chain environment does not yet support `minimum_balance`");
/// Returns the minimum balance that is required for creating an account
/// (i.e. the chain's existential deposit).
pub fn minimum_balance(&self, output: &mut &mut [u8]) {
let minimum_balance: Vec<u8> =
scale::Encode::encode(&self.chain_spec.minimum_balance);
set_output(output, &minimum_balance[..])
}

#[allow(clippy::too_many_arguments)]
Expand Down Expand Up @@ -358,12 +410,39 @@ impl Engine {
unimplemented!("off-chain environment does not yet support `call`");
}

pub fn weight_to_fee(&self, _gas: u64, _output: &mut &mut [u8]) {
unimplemented!("off-chain environment does not yet support `weight_to_fee`");
/// Emulates gas price calculation.
pub fn weight_to_fee(&self, gas: u64, output: &mut &mut [u8]) {
let fee = self.chain_spec.gas_price.saturating_mul(gas.into());
let fee: Vec<u8> = scale::Encode::encode(&fee);
set_output(output, &fee[..])
}

pub fn random(&self, _subject: &[u8], _output: &mut &mut [u8]) {
unimplemented!("off-chain environment does not yet support `random`");
/// Returns a randomized hash.
///
/// # Note
///
/// - This is the off-chain environment implementation of `random`.
/// It provides the same behavior in that it will likely yield the
/// same hash for the same subjects within the same block (or
/// execution context).
///
/// # Example
///
/// ```rust
/// let engine = ink_engine::ext::Engine::default();
/// let subject = [0u8; 32];
/// let mut output = [0u8; 32];
/// engine.random(&subject, &mut output.as_mut_slice());
/// ```
pub fn random(&self, subject: &[u8], output: &mut &mut [u8]) {
let seed = (self.exec_context.entropy, subject).encode();
let mut digest = [0u8; 32];
Engine::hash_blake2_256(&seed, &mut digest);

let mut rng = rand::rngs::StdRng::from_seed(digest);
let mut rng_bytes: [u8; 32] = Default::default();
rng.fill(&mut rng_bytes);
set_output(output, &rng_bytes[..])
}

pub fn call_chain_extension(
Expand Down
38 changes: 38 additions & 0 deletions crates/engine/src/test_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,16 @@ impl Engine {
(*reads, *writes)
}

/// Returns the total number of reads executed.
pub fn count_reads(&self) -> usize {
self.debug_info.count_reads.iter().map(|(_, v)| v).sum()
}

/// Returns the total number of writes executed.
pub fn count_writes(&self) -> usize {
self.debug_info.count_writes.iter().map(|(_, v)| v).sum()
}

/// Sets a caller for the next call.
pub fn set_caller(&mut self, caller: Vec<u8>) {
self.exec_context.caller = Some(caller.into());
Expand All @@ -210,6 +220,12 @@ impl Engine {
Ok(cells.len())
}

/// Advances the chain by a single block.
pub fn advance_block(&mut self) {
self.exec_context.block_number += 1;
self.exec_context.block_timestamp += self.chain_spec.block_time;
}

/// Returns the callee, i.e. the currently executing contract.
pub fn get_callee(&self) -> Vec<u8> {
self.exec_context.callee()
Expand Down Expand Up @@ -290,4 +306,26 @@ mod tests {
// then
assert_eq!(engine.count_used_storage_cells(&account_id), Ok(0));
}

#[test]
fn count_total_writes() {
// given
let mut engine = Engine::new();
let key: &[u8; 32] = &[0x42; 32];
let mut buf = [0_u8; 32];

// when
engine.set_callee(vec![1; 32]);
engine.set_storage(key, &[0x05_u8; 5]);
engine.set_storage(key, &[0x05_u8; 6]);
engine.get_storage(key, &mut &mut buf[..]).unwrap();

engine.set_callee(vec![2; 32]);
engine.set_storage(key, &[0x07_u8; 7]);
engine.get_storage(key, &mut &mut buf[..]).unwrap();

// then
assert_eq!(engine.count_writes(), 3);
assert_eq!(engine.count_reads(), 2);
}
}
20 changes: 13 additions & 7 deletions crates/engine/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,21 @@
// See the License for the specific language governing permissions and
// limitations under the License.

//! Right now the `engine` crate can only be used with the `ink_env::DefaultEnvironment`.
//! This is a known limitation that we want to address in the future.

use derive_more::From;

/// This is just a temporary solution for the MVP!
/// As a temporary solution we choose the same type as the default
/// `env` `Balance` type.
///
/// In the long-term this type should be `Vec<u8>` as well, as to not
/// be dependent on the specific off-chain environment type, so that
/// the `engine` crate can be used with an arbitrary `Environment` configuration.
/// Same type as the `DefaultEnvironment::Hash` type.
pub type Hash = [u8; 32];

/// Same type as the `DefaultEnvironment::BlockNumber` type.
pub type BlockNumber = u32;

/// Same type as the `DefaultEnvironment::BlockTimestamp` type.
pub type BlockTimestamp = u64;

/// Same type as the `DefaultEnvironment::Balance` type.
pub type Balance = u128;

/// The Account Id type used by this crate.
Expand Down
10 changes: 10 additions & 0 deletions crates/env/src/engine/experimental_off_chain/test_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,16 @@ pub fn set_clear_storage_disabled(_disable: bool) {
);
}

/// Advances the chain by a single block.
pub fn advance_block<T>()
where
T: Environment,
{
<EnvInstance as OnInstance>::on_instance(|instance| {
instance.engine.advance_block();
})
}

/// Sets a caller for the next call.
pub fn set_caller<T>(caller: T::AccountId)
where
Expand Down

0 comments on commit 34bd952

Please sign in to comment.