diff --git a/contracts/Cargo.toml b/contracts/Cargo.toml index d03b0729..a7cf2a5d 100644 --- a/contracts/Cargo.toml +++ b/contracts/Cargo.toml @@ -7,6 +7,8 @@ members = [ "initializer", "counter", "counter_float", + "counter_deployer", + "counter_deployer_template", "debugger", "double_counter", "empty_initializer", diff --git a/contracts/counter_deployer/Cargo.toml b/contracts/counter_deployer/Cargo.toml new file mode 100644 index 00000000..a1a8efa7 --- /dev/null +++ b/contracts/counter_deployer/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "counter_deployer" +version = "0.1.0" +authors = [ + "Demilade Sonuga ", +] +edition = "2018" + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +piecrust-uplink = { path = "../../piecrust-uplink", features = ["abi", "dlmalloc", "debug"] } +counter_deployer_template = { path = "../counter_deployer_template" } diff --git a/contracts/counter_deployer/src/lib.rs b/contracts/counter_deployer/src/lib.rs new file mode 100644 index 00000000..660623af --- /dev/null +++ b/contracts/counter_deployer/src/lib.rs @@ -0,0 +1,150 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + +//! Contract to test the contract deploy functionality. + +#![no_std] + +extern crate alloc; + +use alloc::vec::Vec; +use core::convert::TryInto; + +use piecrust_uplink::{self as uplink, ContractError}; +use uplink::ContractId; + +/// Struct that describes the state of the counter deployer contract +pub struct CounterDeployer<'a> { + bytecode: &'a [u8], +} + +/// State of the counter deployer contract +static mut STATE: CounterDeployer = CounterDeployer { + bytecode: include_bytes!("../../../target/wasm64-unknown-unknown/release/counter_deployer_template.wasm"), +}; + +impl<'a> CounterDeployer<'a> { + pub fn simple_deploy( + &self, + init_value: i32, + owner: Vec, + deploy_nonce: u64, + ) -> Result { + if owner.len() == 32 { + let vowner: [u8; 32] = owner.clone().try_into().unwrap(); + uplink::deploy(self.bytecode, Some(&(init_value, false, 0u32, 0u32, owner.clone())), vowner, deploy_nonce) + } else { + panic!("The owner must be 32 bytes in length"); + } + } + + pub fn simple_deploy_fail( + &self, + init_value: i32, + owner: Vec, + deploy_nonce: u64, + ) -> Result { + if owner.len() == 32 { + let vowner: [u8; 32] = owner.clone().try_into().unwrap(); + uplink::deploy(self.bytecode, Some(&(init_value, true, 0u32, 0u32, owner.clone())), vowner, deploy_nonce) + } else { + panic!("The owner must be 32 bytes in length"); + } + } + + pub fn multiple_deploy( + &self, + first_init_value: i32, + last_init_value: i32, + owner: Vec, + deploy_nonce: u64, + ) -> Result, ContractError> { + if first_init_value > last_init_value { + return Ok(Vec::new()); + } + let mut ids: Vec = uplink::call::<_, Result, ContractError>>( + uplink::self_id(), + "multiple_deploy", + &(first_init_value + 1, last_init_value, owner.clone(), deploy_nonce + 1) + )??; + let new_id = uplink::call::<_, Result>( + uplink::self_id(), + "simple_deploy", + &(first_init_value, owner, deploy_nonce) + )??; + ids.push(new_id); + Ok(ids) + } + + /// Mutually recursive function with the template counter's init. + /// + /// Works as follows: + /// - Deploys a contract + /// - If `additional_deploys` != 0, the contract's init function calls this + /// function to deploy another contract + /// - Process repeats until `additional_deploys` == 0 + /// + /// `fail_at` tells at what point and additional deploy triggered from the + /// contract's init function should fail. + /// `fail` tells whether or not the contract being deployed should panic in its + /// init function. + pub fn recursive_deploy_through_init( + &self, + init_value: i32, + fail: bool, + fail_at: u32, + additional_deploys: u32, + deploy_nonce: u64, + owner: Vec, + ) -> Result { + if owner.len() == 32 { + let vowner: [u8; 32] = owner.clone().try_into().unwrap(); + uplink::deploy( + self.bytecode, + Some(&(init_value, fail, fail_at, additional_deploys, owner)), + vowner, + deploy_nonce, + ) + } else { + panic!("The owner must be 32 bytes in length"); + } + } +} + +/// Expose `CounterDeployer::simple_deploy` to the host +#[no_mangle] +unsafe fn simple_deploy(arg_len: u32) -> u32 { + uplink::wrap_call(arg_len, |(init_value, owner, nonce)| STATE.simple_deploy(init_value, owner, nonce)) +} + +/// Expose `CounterDeployer::multiple_deploy` to the host +#[no_mangle] +unsafe fn multiple_deploy(arg_len: u32) -> u32 { + uplink::wrap_call(arg_len, |(first_init_value, last_init_value, owner, nonce)| STATE.multiple_deploy(first_init_value, last_init_value, owner, nonce)) +} + +/// Expose `CounterDeployer::simple_deploy_fail` to the host +#[no_mangle] +unsafe fn simple_deploy_fail(arg_len: u32) -> u32 { + uplink::wrap_call(arg_len, |(init_value, owner, deploy_nonce)| { + STATE.simple_deploy_fail(init_value, owner, deploy_nonce) + }) +} + +/// Expose `CounterDeployer::recursive_deploy_through_init` to the host +#[no_mangle] +unsafe fn recursive_deploy_through_init(arg_len: u32) -> u32 { + uplink::wrap_call(arg_len, |args| { + let (init_value, + fail, + fail_at, + additional_deploys, + deploy_nonce, + owner + ) = args; + STATE.recursive_deploy_through_init(init_value, fail, fail_at, additional_deploys, deploy_nonce, owner) + }) +} diff --git a/contracts/counter_deployer_template/Cargo.toml b/contracts/counter_deployer_template/Cargo.toml new file mode 100644 index 00000000..e66aff40 --- /dev/null +++ b/contracts/counter_deployer_template/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "counter_deployer_template" +version = "0.1.0" +authors = [ + "Demilade Sonuga ", +] +edition = "2021" + +license = "MPL-2.0" + +[dependencies] +piecrust-uplink = { path = "../../piecrust-uplink", features = ["abi", "dlmalloc"] } + +[lib] +crate-type = ["cdylib", "rlib"] diff --git a/contracts/counter_deployer_template/src/lib.rs b/contracts/counter_deployer_template/src/lib.rs new file mode 100644 index 00000000..17d8df59 --- /dev/null +++ b/contracts/counter_deployer_template/src/lib.rs @@ -0,0 +1,71 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + +//! Contract to act as a template for the counter deployer sample contract. + +#![no_std] + +extern crate alloc; + +use piecrust_uplink as uplink; +use uplink::{ContractError, ContractId}; +use alloc::string::ToString; +use alloc::vec::Vec; + +/// Struct that describes the state of the Counter contract +pub struct Counter { + value: i32, +} + +impl Counter { + pub fn init(&mut self, value: i32, fail: bool, fail_at: u32, additional_deploys: u32, owner: Vec) { + if fail { + panic!("Failed to deploy"); + } + self.value = value; + + if additional_deploys > 0 { + let deploy_nonce = additional_deploys as u64 + 100_000; + let fail = fail_at == additional_deploys; + let _ = uplink::call::<_, Result>( + ContractId::try_from("0101010101010101010101010101010101010101010101010101010101010101".to_string()).unwrap(), + "recursive_deploy_through_init", + &(value, fail, fail_at, additional_deploys - 1, deploy_nonce, owner.clone()) + ); + } + } +} + +/// State of the Counter contract +static mut STATE: Counter = Counter { value: 0 }; + +impl Counter { + pub fn read_value(&self) -> i32 { + self.value + } + + pub fn increment(&mut self) { + self.value += 1; + } +} + +/// Expose `Initializer::read_value()` to the host +#[no_mangle] +unsafe fn read_value(arg_len: u32) -> u32 { + uplink::wrap_call(arg_len, |_: ()| STATE.read_value()) +} + +/// Expose `Initializer::increment()` to the host +#[no_mangle] +unsafe fn increment(arg_len: u32) -> u32 { + uplink::wrap_call(arg_len, |_: ()| STATE.increment()) +} + +/// Expose `Initializer::init()` to the host +#[no_mangle] +unsafe fn init(arg_len: u32) -> u32 { + uplink::wrap_call(arg_len, |(arg, fail, fail_at, additional_deploys, owner)| STATE.init(arg, fail, fail_at, additional_deploys, owner)) +} diff --git a/piecrust-uplink/src/abi/state.rs b/piecrust-uplink/src/abi/state.rs index c5617952..cf98f12e 100644 --- a/piecrust-uplink/src/abi/state.rs +++ b/piecrust-uplink/src/abi/state.rs @@ -63,6 +63,16 @@ mod ext { pub fn spent() -> u64; pub fn owner(contract_id: *const u8) -> i32; pub fn self_id(); + pub fn deploy( + bytecode: *const u8, + bytecode_len: u64, + init_arg: *const u8, + init_arg_len: u32, + owner: *const u8, + owner_len: u32, + deploy_nonce: u64, + gas_limit: u64, + ) -> i32; } } @@ -219,6 +229,86 @@ pub fn call_raw_with_limit( }) } +/// Deploys the contract `bytecode` with init argument `init_arg` and +/// deploy nonce `deploy_nonce`, assigning `owner` as the owner. +/// +/// To specify the gas allowed to be spent by the called contract, use +/// [`deploy_with_limit`]. +pub fn deploy( + bytecode: &[u8], + init_arg: Option<&D>, + owner: [u8; N], + deploy_nonce: u64, +) -> Result +where + for<'a> D: Serialize>, +{ + deploy_with_limit(bytecode, init_arg, owner, deploy_nonce, 0) +} + +/// Deploys the contract `bytecode` with init argument `init_arg` and +/// deploy nonce `deploy_nonce`, assigning `owner` as the owner. +/// +/// A gas limit of `0` will use the default behavior - `93%` of the remaining +/// gas will be used to deploy the contract. If the gas limit given is above or +/// equal the remaining amount, the default behavior will be used instead. +/// +/// On invocation, the deploy charge, which is the length of the bytecode * gas +/// per deploy byte, will first be deducted. +/// The remaining gas after that deduction will be used to call the contract's +/// init function, if any. +/// If the gas is exhausted at any point, the entire gas limit used for +/// deployment will be consumed. +pub fn deploy_with_limit( + bytecode: &[u8], + init_arg: Option<&D>, + owner: [u8; N], + deploy_nonce: u64, + gas_limit: u64, +) -> Result +where + for<'a> D: Serialize>, +{ + let (init_arg, init_arg_len) = with_arg_buf(|buf| { + if let Some(init_arg) = init_arg { + let mut sbuf = [0u8; SCRATCH_BUF_BYTES]; + let scratch = BufferScratch::new(&mut sbuf); + let ptr = buf.as_ptr(); + let ser = BufferSerializer::new(buf); + let mut ser = CompositeSerializer::new(ser, scratch, Infallible); + ser.serialize_value(init_arg) + .expect("should not fail to serialize"); + let pos = ser.pos(); + (ptr, pos) + } else { + (ptr::null(), 0) + } + }); + + let ret = unsafe { + ext::deploy( + bytecode.as_ptr(), + bytecode.len() as u64, + init_arg, + init_arg_len as u32, + owner.as_ptr(), + owner.len() as u32, + deploy_nonce, + gas_limit, + ) + }; + + with_arg_buf(|buf| { + if ret == 0 { + let mut id = [0; CONTRACT_ID_BYTES]; + id.copy_from_slice(&buf[..CONTRACT_ID_BYTES]); + Ok(ContractId::from_bytes(id)) + } else { + Err(ContractError::from_parts(ret, buf)) + } + }) +} + /// Returns data made available by the host under the given name. The type `D` /// must be correctly specified, otherwise undefined behavior will occur. pub fn meta_data(name: &str) -> Option diff --git a/piecrust-uplink/src/error.rs b/piecrust-uplink/src/error.rs index 451f0e4b..e6380152 100644 --- a/piecrust-uplink/src/error.rs +++ b/piecrust-uplink/src/error.rs @@ -22,12 +22,13 @@ use core::str; // // The contract writer, however, is free to pass it around and react to it if it // wishes. -#[derive(Debug, Clone, Archive, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Archive, Serialize, Deserialize)] #[archive_attr(derive(CheckBytes))] pub enum ContractError { Panic(String), OutOfGas, DoesNotExist, + InitializationError(String), Unknown, } @@ -59,6 +60,7 @@ impl ContractError { -1 => Self::Panic(get_msg(slice)), -2 => Self::OutOfGas, -3 => Self::DoesNotExist, + -4 => Self::InitializationError(get_msg(slice)), i32::MIN => Self::Unknown, _ => unreachable!("The host must guarantee that the code is valid"), } @@ -84,6 +86,10 @@ impl ContractError { } Self::OutOfGas => -2, Self::DoesNotExist => -3, + Self::InitializationError(msg) => { + put_msg(msg, slice); + -4 + } Self::Unknown => i32::MIN, } } @@ -95,6 +101,7 @@ impl From for i32 { ContractError::Panic(_) => -1, ContractError::OutOfGas => -2, ContractError::DoesNotExist => -3, + ContractError::InitializationError(_) => -4, ContractError::Unknown => i32::MIN, } } @@ -108,6 +115,9 @@ impl Display for ContractError { ContractError::DoesNotExist => { write!(f, "Contract does not exist") } + ContractError::InitializationError(msg) => { + write!(f, "Initialization error: {msg}") + } ContractError::Unknown => write!(f, "Unknown"), } } diff --git a/piecrust/Cargo.toml b/piecrust/Cargo.toml index 5c529f14..a3a943a5 100644 --- a/piecrust/Cargo.toml +++ b/piecrust/Cargo.toml @@ -20,6 +20,7 @@ dusk-wasmtime = { version = "21.0.0-alpha", default-features = false, features = bytecheck = "0.6" rkyv = { version = "0.7", features = ["size_32", "validation"] } blake3 = "1" +blake2b_simd = { version = "1.0.2", default-features = false } memmap2 = "0.7" tempfile = "3" thiserror = "1" diff --git a/piecrust/src/error.rs b/piecrust/src/error.rs index 923e1779..8fdc6568 100644 --- a/piecrust/src/error.rs +++ b/piecrust/src/error.rs @@ -129,6 +129,9 @@ impl From for ContractError { Error::OutOfGas => Self::OutOfGas, Error::Panic(msg) => Self::Panic(msg), Error::ContractDoesNotExist(_) => Self::DoesNotExist, + Error::InitalizationError(msg) => { + Self::InitializationError(msg.to_string()) + } _ => Self::Unknown, } } diff --git a/piecrust/src/imports.rs b/piecrust/src/imports.rs index d9de5921..64382e9a 100644 --- a/piecrust/src/imports.rs +++ b/piecrust/src/imports.rs @@ -8,9 +8,11 @@ mod wasm32; mod wasm64; use std::any::Any; +use std::cmp::max; use std::sync::Arc; use crate::contract::ContractMetadata; +use blake2b_simd::Params; use dusk_wasmtime::{ Caller, Extern, Func, Module, Result as WasmtimeResult, Store, }; @@ -84,6 +86,10 @@ impl Imports { "self_id" => Func::wrap(store, self_id), #[cfg(feature = "debug")] "hdebug" => Func::wrap(store, hdebug), + "deploy" => match is_64 { + false => Func::wrap(store, wasm32::deploy), + true => Func::wrap(store, wasm64::deploy), + }, _ => return None, }) } @@ -217,6 +223,137 @@ pub(crate) fn hd( Ok(data.len() as u32) } +#[allow(clippy::too_many_arguments)] +pub(crate) fn deploy( + mut fenv: Caller, + bytecode_ofs: usize, + bytecode_len: u64, + init_arg_ofs: usize, + init_arg_len: u32, + owner_ofs: usize, + owner_len: u32, + deploy_nonce: u64, + gas_limit: u64, +) -> WasmtimeResult { + let env = fenv.data_mut(); + let instance = env.self_instance(); + + check_ptr(instance, init_arg_ofs, init_arg_len as usize)?; + check_ptr(instance, owner_ofs, owner_len as usize)?; + check_ptr(instance, init_arg_ofs, init_arg_len as usize)?; + check_ptr(instance, owner_ofs, owner_len as usize)?; + // Safe to cast `u64` to `usize` because wasmtime doesn't support 32-bit + // platforms https://docs.wasmtime.dev/stability-platform-support.html#compiler-support + check_ptr(instance, bytecode_ofs, bytecode_len as usize)?; + + let (deployer_gas_remaining, deployed_init_limit) = { + let gas_remaining = instance.get_remaining_gas(); + let deploy_gas_limit = + compute_gas_limit_for_callee(gas_remaining, gas_limit); + let deploy_charge = max( + bytecode_len * env.gas_per_deploy_byte(), + env.min_deploy_points(), + ); + if deploy_gas_limit < deploy_charge { + instance.set_remaining_gas(gas_remaining - deploy_gas_limit); + let err = ContractError::from(Error::OutOfGas); + instance.with_arg_buf_mut(|buf| { + err.to_parts(buf); + }); + return Ok(err.into()); + } + instance.set_remaining_gas(gas_remaining - deploy_charge); + ( + gas_remaining - deploy_charge, + deploy_gas_limit - deploy_charge, + ) + }; + + let deploy_result: Result<_, Error> = instance.with_memory(|mem| { + let bytecode = &mem[bytecode_ofs..bytecode_ofs + bytecode_len as usize]; + let owner = mem[owner_ofs..owner_ofs + owner_len as usize].to_vec(); + let contract_id = gen_contract_id(bytecode, deploy_nonce, &owner); + let init_arg = if init_arg_ofs == 0 { + None + } else { + Some( + mem[init_arg_ofs..init_arg_ofs + init_arg_len as usize] + .to_vec(), + ) + }; + env.deploy_raw( + Some(contract_id), + bytecode, + init_arg.clone(), + owner, + gas_limit, + false, + )?; + Ok((contract_id, init_arg)) + }); + + let (contract_id, init_arg) = match deploy_result { + Ok(val) => val, + Err(err) => { + let err = ContractError::from(err); + instance.with_arg_buf_mut(|buf| { + err.to_parts(buf); + }); + return Ok(err.into()); + } + }; + + let call_init_result: Result<_, Error> = (|| { + env.push_callstack(contract_id, gas_limit)?; + let deployed = env + .instance(&contract_id) + .expect("The contract just deployed should exist"); + init_arg.inspect(|arg| deployed.write_argument(arg)); + deployed + .call(INIT_METHOD, init_arg_len, deployed_init_limit) + .map_err(Error::normalize) + .map_err(|err| { + // On failure, charge the full gas limit for calling init and + // restore to previous state. + instance.set_remaining_gas( + deployer_gas_remaining - deployed_init_limit, + ); + env.remove_contract(&contract_id); + env.move_up_prune_call_tree(); + let revert_res = env.revert_callstack(); + if let Err(io_err) = revert_res { + Error::MemorySnapshotFailure { + reason: Some(Arc::new(err)), + io: Arc::new(io_err), + } + } else { + err + } + })?; + let spent_on_init = deployed_init_limit - deployed.get_remaining_gas(); + env.move_up_call_tree(spent_on_init); + instance.set_remaining_gas(deployer_gas_remaining - spent_on_init); + Ok(()) + })(); + + match call_init_result { + Ok(()) => { + instance.with_arg_buf_mut(|arg_buf| { + arg_buf[..CONTRACT_ID_BYTES] + .copy_from_slice(contract_id.as_bytes()); + }); + Ok(0) + } + Err(err) => { + let err = ContractError::from(err); + instance.with_arg_buf_mut(|buf| { + err.to_parts(buf); + }); + Ok(err.into()) + } + } +} + pub(crate) fn c( mut fenv: Caller, callee_ofs: usize, @@ -239,13 +376,8 @@ pub(crate) fn c( let caller_remaining = instance.get_remaining_gas(); - let callee_limit = if gas_limit > 0 && gas_limit < caller_remaining { - gas_limit - } else { - let div = caller_remaining / 100 * GAS_PASS_PCT; - let rem = caller_remaining % 100 * GAS_PASS_PCT / 100; - div + rem - }; + let callee_limit = + compute_gas_limit_for_callee(caller_remaining, gas_limit); enum WithMemoryError { BeforePush(Error), @@ -534,3 +666,35 @@ fn self_id(mut fenv: Caller) { env.self_instance() .with_arg_buf_mut(|arg| arg[..len].copy_from_slice(&slice)); } + +pub fn gen_contract_id( + bytes: impl AsRef<[u8]>, + nonce: u64, + owner: impl AsRef<[u8]>, +) -> ContractId { + let mut hasher = Params::new().hash_length(CONTRACT_ID_BYTES).to_state(); + hasher.update(bytes.as_ref()); + hasher.update(&nonce.to_le_bytes()[..]); + hasher.update(owner.as_ref()); + let hash_bytes: [u8; CONTRACT_ID_BYTES] = hasher + .finalize() + .as_bytes() + .try_into() + .expect("the hash result is exactly `CONTRACT_ID_BYTES` long"); + ContractId::from_bytes(hash_bytes) +} + +fn compute_gas_limit_for_callee( + caller_gas_left: u64, + preferred_callee_gas_limit: u64, +) -> u64 { + if preferred_callee_gas_limit > 0 + && preferred_callee_gas_limit < caller_gas_left + { + preferred_callee_gas_limit + } else { + let div = caller_gas_left / 100 * GAS_PASS_PCT; + let rem = caller_gas_left % 100 * GAS_PASS_PCT / 100; + div + rem + } +} diff --git a/piecrust/src/imports/wasm32.rs b/piecrust/src/imports/wasm32.rs index 4963f461..d399d9c3 100644 --- a/piecrust/src/imports/wasm32.rs +++ b/piecrust/src/imports/wasm32.rs @@ -56,3 +56,28 @@ pub(crate) fn emit( pub(crate) fn owner(fenv: Caller, mod_id_ofs: u32) -> WasmtimeResult { imports::owner(fenv, mod_id_ofs as usize) } + +#[allow(clippy::too_many_arguments)] +pub(crate) fn deploy( + fenv: Caller, + bytecode_ofs: u32, + bytecode_len: u64, + init_arg_ofs: u32, + init_arg_len: u32, + owner_ofs: u32, + owner_len: u32, + deploy_nonce: u64, + gas_limit: u64, +) -> WasmtimeResult { + imports::deploy( + fenv, + bytecode_ofs as usize, + bytecode_len, + init_arg_ofs as usize, + init_arg_len, + owner_ofs as usize, + owner_len, + deploy_nonce, + gas_limit, + ) +} diff --git a/piecrust/src/imports/wasm64.rs b/piecrust/src/imports/wasm64.rs index 06f729db..5ad2fc4c 100644 --- a/piecrust/src/imports/wasm64.rs +++ b/piecrust/src/imports/wasm64.rs @@ -56,3 +56,28 @@ pub(crate) fn emit( pub(crate) fn owner(fenv: Caller, mod_id_ofs: u64) -> WasmtimeResult { imports::owner(fenv, mod_id_ofs as usize) } + +#[allow(clippy::too_many_arguments)] +pub(crate) fn deploy( + fenv: Caller, + bytecode_ofs: u64, + bytecode_len: u64, + init_arg_ofs: u64, + init_arg_len: u32, + owner_ofs: u64, + owner_len: u32, + deploy_nonce: u64, + gas_limit: u64, +) -> WasmtimeResult { + imports::deploy( + fenv, + bytecode_ofs as usize, + bytecode_len, + init_arg_ofs as usize, + init_arg_len, + owner_ofs as usize, + owner_len, + deploy_nonce, + gas_limit, + ) +} diff --git a/piecrust/src/lib.rs b/piecrust/src/lib.rs index 0844ab97..828fddd4 100644 --- a/piecrust/src/lib.rs +++ b/piecrust/src/lib.rs @@ -115,6 +115,8 @@ //! [runtime docs]: dusk_wasmtime::Config::consume_fuel //! [`deletions`]: VM::delete_commit +#![feature(result_option_inspect)] + #[macro_use] mod bytecode_macro; mod call_tree; @@ -131,6 +133,7 @@ mod vm; pub use call_tree::{CallTree, CallTreeElem}; pub use contract::{ContractData, ContractDataBuilder}; pub use error::Error; +pub use imports::gen_contract_id; pub use session::{CallReceipt, Session, SessionData}; pub use store::PageOpening; pub use vm::{HostQuery, VM}; diff --git a/piecrust/src/session.rs b/piecrust/src/session.rs index 2d984fee..dd699a88 100644 --- a/piecrust/src/session.rs +++ b/piecrust/src/session.rs @@ -54,6 +54,7 @@ pub struct Session { engine: Engine, inner: &'static mut SessionInner, original: bool, + config: SessionConfig, } impl Debug for Session { @@ -138,6 +139,7 @@ impl Session { contract_session: ContractSession, host_queries: HostQueries, data: SessionData, + config: SessionConfig, ) -> Self { let inner = SessionInner { current: ContractId::from_bytes([0; CONTRACT_ID_BYTES]), @@ -159,6 +161,7 @@ impl Session { engine: engine.clone(), inner, original: true, + config, }; let mut config = engine.config().clone(); @@ -185,6 +188,7 @@ impl Session { engine: self.engine.clone(), inner: unsafe { &mut *inner }, original: false, + config: self.config.clone(), } } @@ -193,6 +197,14 @@ impl Session { &self.engine } + pub(crate) fn gas_per_deploy_byte(&self) -> u64 { + self.config.gas_per_deploy_byte + } + + pub(crate) fn min_deploy_points(&self) -> u64 { + self.config.min_deploy_points + } + /// Deploy a contract, returning its [`ContractId`]. The ID is computed /// using a `blake3` hash of the `bytecode`. Contracts using the `memory64` /// proposal are accepted in just the same way as 32-bit contracts, and @@ -247,6 +259,7 @@ impl Session { .owner .expect("Owner must be specified when deploying a contract"), gas_limit, + true, ) } @@ -275,12 +288,20 @@ impl Session { init_arg: Option>, owner: Vec, gas_limit: u64, + call_init: bool, ) -> Result { let contract_id = contract_id.unwrap_or({ let hash = blake3::hash(bytecode); ContractId::from_bytes(hash.into()) }); - self.do_deploy(contract_id, bytecode, init_arg, owner, gas_limit)?; + self.do_deploy( + contract_id, + bytecode, + init_arg, + owner, + gas_limit, + call_init, + )?; Ok(contract_id) } @@ -293,6 +314,7 @@ impl Session { arg: Option>, owner: Vec, gas_limit: u64, + call_init: bool, ) -> Result<(), Error> { if self.inner.contract_session.contract_deployed(contract_id) { return Err(InitalizationError( @@ -321,7 +343,7 @@ impl Session { let instance = self.instance(&contract_id).expect("instance should exist"); - if instance.is_function_exported(INIT_METHOD) { + if call_init && instance.is_function_exported(INIT_METHOD) { // If no argument was provided, we call the init method anyway, // but with an empty argument. The alternative is to panic, but // that assumes that the caller of `deploy` knows that the @@ -647,6 +669,10 @@ impl Session { self.inner.call_tree.call_ids() } + pub(crate) fn remove_contract(&mut self, contract_id: &ContractId) { + self.inner.contract_session.remove_contract(contract_id) + } + /// Creates a new instance of the given contract, returning its memory /// length. fn create_instance( @@ -969,3 +995,21 @@ impl SessionDataBuilder { } } } + +#[derive(Debug, Clone)] +pub(crate) struct SessionConfig { + gas_per_deploy_byte: u64, + min_deploy_points: u64, +} + +impl SessionConfig { + pub(crate) fn new( + gas_per_deploy_byte: Option, + min_deploy_points: Option, + ) -> Self { + Self { + gas_per_deploy_byte: gas_per_deploy_byte.unwrap_or(100), + min_deploy_points: min_deploy_points.unwrap_or(5_000_000), + } + } +} diff --git a/piecrust/src/vm.rs b/piecrust/src/vm.rs index 5d1b4a59..691f689e 100644 --- a/piecrust/src/vm.rs +++ b/piecrust/src/vm.rs @@ -13,18 +13,18 @@ use std::sync::Arc; use std::thread; use dusk_wasmtime::{ - Config, Engine, ModuleVersionStrategy, OperatorCost, OptLevel, Strategy, - WasmBacktraceDetails, + Config as WasmtimeConfig, Engine, ModuleVersionStrategy, OperatorCost, + OptLevel, Strategy, WasmBacktraceDetails, }; use tempfile::tempdir; use crate::config::BYTE_STORE_COST; -use crate::session::{Session, SessionData}; +use crate::session::{Session, SessionConfig, SessionData}; use crate::store::ContractStore; use crate::Error::{self, PersistenceError}; -fn config() -> Config { - let mut config = Config::new(); +fn config() -> WasmtimeConfig { + let mut config = WasmtimeConfig::new(); // Neither WASM backtrace, nor native unwind info. config.wasm_backtrace(false); @@ -116,6 +116,7 @@ pub struct VM { engine: Engine, host_queries: HostQueries, store: ContractStore, + session_config: SessionConfig, } impl Debug for VM { @@ -124,6 +125,7 @@ impl Debug for VM { .field("config", self.engine.config()) .field("host_queries", &self.host_queries) .field("store", &self.store) + .field("session_config", &self.session_config) .finish() } } @@ -137,7 +139,11 @@ impl VM { /// /// # Errors /// If the directory contains unparseable or inconsistent data. - pub fn new>(root_dir: P) -> Result { + pub fn new>( + root_dir: P, + gas_per_deploy_byte: Option, + min_deploy_points: Option, + ) -> Result { tracing::trace!("vm::new"); let config = config(); @@ -158,6 +164,10 @@ impl VM { engine, host_queries: HostQueries::default(), store, + session_config: SessionConfig::new( + gas_per_deploy_byte, + min_deploy_points, + ), }) } @@ -169,6 +179,13 @@ impl VM { /// # Errors /// If creating a temporary directory fails. pub fn ephemeral() -> Result { + Self::ephemeral_with_session_config(None, None) + } + + pub fn ephemeral_with_session_config( + gas_per_deploy_byte: Option, + min_deploy_points: Option, + ) -> Result { let tmp = tempdir().map_err(|err| PersistenceError(Arc::new(err)))?; let tmp = tmp.path().to_path_buf(); @@ -188,6 +205,10 @@ impl VM { engine, host_queries: HostQueries::default(), store, + session_config: SessionConfig::new( + gas_per_deploy_byte, + min_deploy_points, + ), }) } @@ -228,6 +249,7 @@ impl VM { contract_session, self.host_queries.clone(), data, + self.session_config.clone(), )) } diff --git a/piecrust/tests/cold-reboot/src/main.rs b/piecrust/tests/cold-reboot/src/main.rs index 9f9c6c44..6570a5a8 100644 --- a/piecrust/tests/cold-reboot/src/main.rs +++ b/piecrust/tests/cold-reboot/src/main.rs @@ -68,14 +68,14 @@ fn confirm_counter>( fn initialize>(vm_data_path: P) -> Result<(), piecrust::Error> { let commit_id_file_path = PathBuf::from(vm_data_path.as_ref()).join("commit_id"); - let vm = VM::new(vm_data_path.as_ref())?; + let vm = VM::new(vm_data_path.as_ref(), None, None)?; initialize_counter(&vm, commit_id_file_path) } fn confirm>(vm_data_path: P) -> Result<(), piecrust::Error> { let commit_id_file_path = PathBuf::from(vm_data_path.as_ref()).join("commit_id"); - let vm = VM::new(vm_data_path.as_ref())?; + let vm = VM::new(vm_data_path.as_ref(), None, None)?; confirm_counter(&vm, commit_id_file_path) } diff --git a/piecrust/tests/deploy.rs b/piecrust/tests/deploy.rs index 0c11c568..7b0d1eeb 100644 --- a/piecrust/tests/deploy.rs +++ b/piecrust/tests/deploy.rs @@ -5,12 +5,17 @@ // Copyright (c) DUSK NETWORK. All rights reserved. use piecrust::{ - contract_bytecode, ContractData, ContractError, ContractId, Error, - SessionData, VM, + contract_bytecode, gen_contract_id, ContractData, ContractError, + ContractId, Error, SessionData, VM, }; const OWNER: [u8; 32] = [0u8; 32]; const LIMIT: u64 = 1_000_000; +const CONTRACT_DEPLOY_CONTRACT_LIMIT: u64 = 7_500_000; +const GAS_PER_DEPLOY_BYTE: u64 = 100; +const CONTRACT_DEPLOYER_TEMPLATE_CODE: &[u8] = include_bytes!("../../target/wasm64-unknown-unknown/release/counter_deployer_template.wasm"); +const EXPECTED_DEPLOY_CHARGE: u64 = + CONTRACT_DEPLOYER_TEMPLATE_CODE.len() as u64 * GAS_PER_DEPLOY_BYTE; #[test] pub fn deploy_with_id() -> Result<(), Error> { @@ -84,3 +89,367 @@ fn call_non_deployed() -> Result<(), Error> { Ok(()) } + +#[test] +pub fn contract_deploy_contract_simple() -> Result<(), Error> { + let vm = + VM::ephemeral_with_session_config(Some(GAS_PER_DEPLOY_BYTE), None)?; + + let bytecode = contract_bytecode!("counter_deployer"); + let contract_id = ContractId::from([1; 32]); + let mut session = vm.session(SessionData::builder())?; + session.deploy( + bytecode, + ContractData::builder() + .owner(OWNER) + .contract_id(contract_id), + LIMIT, + )?; + + // Two separate counter contracts with different init args should + // successfully deploy. + let deploy_nonce1 = 0u64; + let deploy_nonce2 = 1u64; + let deployed_contract1_receipt = + session.call::<_, Result>( + contract_id, + "simple_deploy", + &(-64i32, OWNER.to_vec(), deploy_nonce1), + CONTRACT_DEPLOY_CONTRACT_LIMIT, + )?; + let deployed_contract2_receipt = + session.call::<_, Result>( + contract_id, + "simple_deploy", + &(1000i32, OWNER.to_vec(), deploy_nonce2), + CONTRACT_DEPLOY_CONTRACT_LIMIT, + )?; + + assert!(deployed_contract1_receipt.gas_spent > EXPECTED_DEPLOY_CHARGE); + assert!(deployed_contract2_receipt.gas_spent > EXPECTED_DEPLOY_CHARGE); + + let deployed_contract1 = deployed_contract1_receipt.data.unwrap(); + let deployed_contract2 = deployed_contract2_receipt.data.unwrap(); + + // Their IDs should be correctly generated. + assert_eq!( + deployed_contract1, + gen_contract_id(CONTRACT_DEPLOYER_TEMPLATE_CODE, deploy_nonce1, &OWNER) + ); + assert_eq!( + deployed_contract2, + gen_contract_id(CONTRACT_DEPLOYER_TEMPLATE_CODE, deploy_nonce2, &OWNER) + ); + + // They should work as expected. + assert_eq!( + session + .call::<_, i32>(deployed_contract1, "read_value", &(), LIMIT)? + .data, + -64 + ); + assert_eq!( + session + .call::<_, i32>(deployed_contract2, "read_value", &(), LIMIT)? + .data, + 1000 + ); + + session.call::<_, ()>(deployed_contract1, "increment", &(), LIMIT)?; + session.call::<_, ()>(deployed_contract2, "increment", &(), LIMIT)?; + + assert_eq!( + session + .call::<_, i32>(deployed_contract1, "read_value", &(), LIMIT)? + .data, + -63 + ); + assert_eq!( + session + .call::<_, i32>(deployed_contract2, "read_value", &(), LIMIT)? + .data, + 1001 + ); + + Ok(()) +} + +#[test] +pub fn contract_deploy_contract_insufficient_gas() -> Result<(), Error> { + let vm = + VM::ephemeral_with_session_config(Some(GAS_PER_DEPLOY_BYTE), None)?; + + let bytecode = contract_bytecode!("counter_deployer"); + let contract_id = ContractId::from([1; 32]); + let mut session = vm.session(SessionData::builder())?; + session.deploy( + bytecode, + ContractData::builder() + .owner(OWNER) + .contract_id(contract_id), + LIMIT, + )?; + + let deployed_contract1_receipt = + session.call::<_, Result>( + contract_id, + "simple_deploy", + &(-64i32, OWNER.to_vec(), 0u64), + EXPECTED_DEPLOY_CHARGE - 1, + )?; + + assert_eq!( + deployed_contract1_receipt.data, + Err(ContractError::OutOfGas) + ); + + Ok(()) +} + +#[test] +pub fn contract_deploy_contract_multiple() -> Result<(), Error> { + let vm = + VM::ephemeral_with_session_config(Some(GAS_PER_DEPLOY_BYTE), None)?; + + let bytecode = contract_bytecode!("counter_deployer"); + let contract_id = ContractId::from([1; 32]); + + let mut session = vm.session(SessionData::builder())?; + session.deploy( + bytecode, + ContractData::builder() + .owner(OWNER) + .contract_id(contract_id), + LIMIT, + )?; + + // Recursively deploying multiple contracts should succeed. + let deployed_contracts_receipt = + session.call::<_, Result, ContractError>>( + contract_id, + "multiple_deploy", + &(-2i32, 2i32, OWNER.to_vec(), 0u64), + CONTRACT_DEPLOY_CONTRACT_LIMIT * 5, + )?; + + assert!(deployed_contracts_receipt.gas_spent > EXPECTED_DEPLOY_CHARGE * 5); + assert!(deployed_contracts_receipt.data.is_ok()); + + // Those contracts should work as expected. + let deployed_contracts = deployed_contracts_receipt.data.unwrap(); + for ((contract, init_value), nonce) in deployed_contracts + .into_iter() + .zip([2, 1, 0, -1, -2]) + .zip([4, 3, 2, 1, 0]) + { + assert_eq!( + contract, + gen_contract_id(CONTRACT_DEPLOYER_TEMPLATE_CODE, nonce, &OWNER), + ); + + assert_eq!( + session + .call::<_, i32>(contract, "read_value", &(), LIMIT)? + .data, + init_value + ); + + session.call::<_, ()>(contract, "increment", &(), LIMIT)?; + + assert_eq!( + session + .call::<_, i32>(contract, "read_value", &(), LIMIT)? + .data, + init_value + 1 + ); + } + + Ok(()) +} + +#[test] +pub fn contract_deploy_already_deployed_contract() -> Result<(), Error> { + let vm = + VM::ephemeral_with_session_config(Some(GAS_PER_DEPLOY_BYTE), None)?; + + let bytecode = contract_bytecode!("counter_deployer"); + let contract_id = ContractId::from([1; 32]); + let mut session = vm.session(SessionData::builder())?; + session.deploy( + bytecode, + ContractData::builder() + .owner(OWNER) + .contract_id(contract_id), + LIMIT, + )?; + + let args = (-64i32, OWNER.to_vec(), 0u64); + let deployed_contract1_receipt = + session.call::<_, Result>( + contract_id, + "simple_deploy", + &args, + CONTRACT_DEPLOY_CONTRACT_LIMIT, + )?; + let deployed_contract2_receipt = + session.call::<_, Result>( + contract_id, + "simple_deploy", + &args, + CONTRACT_DEPLOY_CONTRACT_LIMIT, + )?; + + assert!(deployed_contract1_receipt.gas_spent > EXPECTED_DEPLOY_CHARGE); + assert!(deployed_contract2_receipt.gas_spent > EXPECTED_DEPLOY_CHARGE); + + assert_eq!( + deployed_contract2_receipt.data, + Err(ContractError::InitializationError( + "Deployed error already exists".to_string() + )) + ); + + Ok(()) +} + +#[test] +pub fn contract_deploy_contract_failed_init() -> Result<(), Error> { + let vm = + VM::ephemeral_with_session_config(Some(GAS_PER_DEPLOY_BYTE), None)?; + + let bytecode = contract_bytecode!("counter_deployer"); + + let contract_id = ContractId::from([1; 32]); + let mut session = vm.session(SessionData::builder())?; + session.deploy( + bytecode, + ContractData::builder() + .owner(OWNER) + .contract_id(contract_id), + LIMIT, + )?; + + let deploy_nonce = 1u64; + let deployed_contract_receipt = session + .call::<_, Result>( + contract_id, + "simple_deploy_fail", + &(-64i32, OWNER.to_vec(), deploy_nonce), + CONTRACT_DEPLOY_CONTRACT_LIMIT, + )?; + + assert!(deployed_contract_receipt.gas_spent > EXPECTED_DEPLOY_CHARGE); + assert!(deployed_contract_receipt.data.is_err()); + assert_eq!( + deployed_contract_receipt.data, + Err(ContractError::Panic("Failed to deploy".to_string())) + ); + + let call_result = session.call::<_, i32>( + gen_contract_id(CONTRACT_DEPLOYER_TEMPLATE_CODE, deploy_nonce, &OWNER), + "read_value", + &(), + LIMIT, + ); + assert!(matches!(call_result, Err(Error::ContractDoesNotExist(_))), "If a contract's init function fails during deployment, the deployment should be reversed"); + + Ok(()) +} + +#[test] +pub fn contract_deploy_contract_init_deploys() -> Result<(), Error> { + let vm = + VM::ephemeral_with_session_config(Some(GAS_PER_DEPLOY_BYTE), None)?; + + let bytecode = contract_bytecode!("counter_deployer"); + + let contract_id = ContractId::from([1; 32]); + let mut session = vm.session(SessionData::builder())?; + session.deploy( + bytecode, + ContractData::builder() + .owner(OWNER) + .contract_id(contract_id), + LIMIT, + )?; + + // Deploying a contract that deploys other contracts in its init function + // should succeed. + let (init_value, fail, fail_at, additional_deploys, deploy_nonce) = + (-64i32, false, 5u32, 10u32, 1u64); + let deployed_contract_receipt = session + .call::<_, Result>( + contract_id, + "recursive_deploy_through_init", + &( + init_value, + fail, + fail_at, + additional_deploys, + deploy_nonce, + OWNER.to_vec(), + ), + CONTRACT_DEPLOY_CONTRACT_LIMIT * 11, + )?; + + let expected_successful_deploys = 5; + let expected_failed_deploys = 1; + assert!( + deployed_contract_receipt.gas_spent + > EXPECTED_DEPLOY_CHARGE + * (expected_failed_deploys + expected_successful_deploys) + ); + + // The contracts that were successfully deployed should exist. + let (contract_ids_that_should_exist, contract_id_that_shouldnt_exist) = { + let mut ids = + vec![gen_contract_id(CONTRACT_DEPLOYER_TEMPLATE_CODE, 1, &OWNER)]; + let mut shouldnt_exist = vec![]; + let mut fails_from_here = false; + for deploy_no in (1..=additional_deploys).rev() { + let id = gen_contract_id( + CONTRACT_DEPLOYER_TEMPLATE_CODE, + deploy_no as u64 + 100_000, + &OWNER, + ); + if !fails_from_here { + fails_from_here = deploy_no == fail_at; + } + if !fails_from_here { + ids.push(id); + } else { + shouldnt_exist.push(id); + } + } + (ids, shouldnt_exist) + }; + + for contract in contract_ids_that_should_exist { + assert_eq!( + session + .call::<_, i32>(contract, "read_value", &(), LIMIT)? + .data, + init_value + ); + + session.call::<_, ()>(contract, "increment", &(), LIMIT)?; + + assert_eq!( + session + .call::<_, i32>(contract, "read_value", &(), LIMIT)? + .data, + init_value + 1 + ); + } + + // The contract whose init function failed should not exist. + // Since its init function failed, then all other contracts deployed + // from it should be reverted. + for contract in contract_id_that_shouldnt_exist { + let call_result = + session.call::<_, i32>(contract, "read_value", &(), LIMIT); + assert!(matches!(call_result, Err(Error::ContractDoesNotExist(_)))); + } + + Ok(()) +} diff --git a/piecrust/tests/persistence.rs b/piecrust/tests/persistence.rs index cd230719..2ea571b9 100644 --- a/piecrust/tests/persistence.rs +++ b/piecrust/tests/persistence.rs @@ -67,7 +67,7 @@ fn session_commits_persistence() -> Result<(), Error> { } { - let vm2 = VM::new(vm.root_dir())?; + let vm2 = VM::new(vm.root_dir(), None, None)?; let mut session = vm2.session(SessionData::builder().base(commit_1))?; // check if both contracts' state was restored @@ -84,7 +84,7 @@ fn session_commits_persistence() -> Result<(), Error> { } { - let vm3 = VM::new(vm.root_dir())?; + let vm3 = VM::new(vm.root_dir(), None, None)?; let mut session = vm3.session(SessionData::builder().base(commit_2))?; // check if both contracts' state was restored @@ -132,7 +132,7 @@ fn contracts_persistence() -> Result<(), Error> { let commit_1 = session.commit()?; - let vm2 = VM::new(vm.root_dir())?; + let vm2 = VM::new(vm.root_dir(), None, None)?; let mut session2 = vm2.session(SessionData::builder().base(commit_1))?; // check if both contracts' state was restored