diff --git a/crates/env/src/api.rs b/crates/env/src/api.rs index 581d7dabb4f..a7ecb52b40e 100644 --- a/crates/env/src/api.rs +++ b/crates/env/src/api.rs @@ -250,8 +250,12 @@ where Args: scale::Encode, R: scale::Decode, { + let call = ::on_instance(|instance| { + TypedEnvBackend::invoke_contract_begin::(instance, params) + })?; + call(); ::on_instance(|instance| { - TypedEnvBackend::invoke_contract::(instance, params) + TypedEnvBackend::invoke_contract_end::(instance) }) } @@ -303,8 +307,12 @@ where Args: scale::Encode, Salt: AsRef<[u8]>, { + let deploy = ::on_instance(|instance| { + TypedEnvBackend::instantiate_contract_begin::(instance, params) + })?; + deploy(); ::on_instance(|instance| { - TypedEnvBackend::instantiate_contract::(instance, params) + TypedEnvBackend::instantiate_contract_end::(instance) }) } @@ -387,7 +395,7 @@ where /// # Note /// /// This function stops the execution of the contract immediately. -pub fn return_value(return_flags: ReturnFlags, return_value: &R) -> ! +pub fn return_value(return_flags: ReturnFlags, return_value: &R) where R: scale::Encode, { diff --git a/crates/env/src/backend.rs b/crates/env/src/backend.rs index 43ee3252332..4ce32c1a51f 100644 --- a/crates/env/src/backend.rs +++ b/crates/env/src/backend.rs @@ -30,7 +30,7 @@ use crate::{ use ink_primitives::Key; /// The flags to indicate further information about the end of a contract execution. -#[derive(Default)] +#[derive(Default, Debug, Clone)] pub struct ReturnFlags { value: u32, } @@ -46,6 +46,11 @@ impl ReturnFlags { self } + /// Returns if the flag is set to reverted. + pub fn reverted(&self) -> bool { + self.value > 0 + } + /// Returns the underlying `u32` representation. #[cfg(not(feature = "std"))] pub(crate) fn into_u32(self) -> u32 { @@ -219,7 +224,7 @@ pub trait EnvBackend { /// /// The `flags` parameter can be used to revert the state changes of the /// entire execution if necessary. - fn return_value(&mut self, flags: ReturnFlags, return_value: &R) -> ! + fn return_value(&mut self, flags: ReturnFlags, return_value: &R) where R: scale::Encode; @@ -386,15 +391,27 @@ pub trait TypedEnvBackend: EnvBackend { /// # Note /// /// For more details visit: [`invoke_contract`][`crate::invoke_contract`] - fn invoke_contract( + fn invoke_contract_begin( &mut self, call_data: &CallParams, Args, R>, - ) -> Result + ) -> Result where E: Environment, Args: scale::Encode, R: scale::Decode; + /// Invokes a contract message and returns its result. + /// + /// # Note + /// + /// For more details visit: [`invoke_contract`][`crate::invoke_contract`] + fn invoke_contract_end( + &mut self, + ) -> Result + where + E: Environment, + R: scale::Decode; + /// Invokes a contract message via delegate call and returns its result. /// /// # Note @@ -414,15 +431,26 @@ pub trait TypedEnvBackend: EnvBackend { /// # Note /// /// For more details visit: [`instantiate_contract`][`crate::instantiate_contract`] - fn instantiate_contract( + fn instantiate_contract_begin( &mut self, params: &CreateParams, - ) -> Result + ) -> Result where E: Environment, Args: scale::Encode, Salt: AsRef<[u8]>; + /// Instantiates another contract. + /// + /// # Note + /// + /// For more details visit: [`instantiate_contract`][`crate::instantiate_contract`] + fn instantiate_contract_end( + &mut self, + ) -> Result + where + E: Environment; + /// Terminates a smart contract. /// /// # Note diff --git a/crates/env/src/engine/off_chain/impls.rs b/crates/env/src/engine/off_chain/impls.rs index 26e1e3b4f1a..a1f2c96a5a1 100644 --- a/crates/env/src/engine/off_chain/impls.rs +++ b/crates/env/src/engine/off_chain/impls.rs @@ -218,14 +218,18 @@ impl EnvBackend for EnvInstance { where T: scale::Decode, { - unimplemented!("the off-chain env does not implement `seal_input`") + scale::Decode::decode(&mut &self.stack.peek().input[..]) + .map_err(|err| Error::Decode(err)) } - fn return_value(&mut self, _flags: ReturnFlags, _return_value: &R) -> ! + fn return_value(&mut self, flags: ReturnFlags, return_value: &R) where R: scale::Encode, { - unimplemented!("the off-chain env does not implement `seal_return_value`") + if flags.reverted() { + panic!("reverted."); + } + self.stack.set_return_value(flags, return_value.encode()); } fn debug_message(&mut self, message: &str) { @@ -337,8 +341,12 @@ impl EnvBackend for EnvInstance { Ok(decoded) } - fn set_code_hash(&mut self, _code_hash: &[u8]) -> Result<()> { - unimplemented!("off-chain environment does not support `set_code_hash`") + fn set_code_hash(&mut self, code_hash: &[u8]) -> Result<()> { + let me = self.stack.peek().callee; + let hash: crate::Hash = scale::Decode::decode(&mut &code_hash[..]) + .map_err(|err| Error::Decode(err))?; + self.contracts.update_code(me, hash); + Ok(()) } } @@ -410,23 +418,55 @@ impl TypedEnvBackend for EnvInstance { self.engine.deposit_event(&enc_topics[..], enc_data); } - fn invoke_contract( + fn invoke_contract_begin( &mut self, params: &CallParams, Args, R>, - ) -> Result + ) -> Result where E: Environment, Args: scale::Encode, R: scale::Decode, { let _gas_limit = params.gas_limit(); - let _callee = params.callee(); + let callee = params.callee(); let _call_flags = params.call_flags().into_u32(); - let _transferred_value = params.transferred_value(); - let _input = params.exec_input(); - unimplemented!("off-chain environment does not support contract invocation") + let transferred_value = params.transferred_value(); + if *transferred_value != num_traits::Zero::zero() { + unimplemented!("off-chain environment does not support value trnsfer when calling contracts") + } + let input = params.exec_input(); + let input = scale::Encode::encode(&input); + let callee = from_env::account_id::(callee); + + self.push_frame(&callee, input.clone()); + let (_deploy, call) = self.contracts.entrypoints(&callee) + .ok_or(Error::NotCallable)?; + // TODO: snapshot the db + // TODO: unwind panic? + Ok(call) } + fn invoke_contract_end(&mut self) -> Result + where + E: Environment, + R: scale::Decode, + { + // Read return value & process revert + let frame = self.pop_frame().expect("frame exists; qed."); + let data = if let Some((flags, data)) = frame.return_value { + if flags.reverted() { + // TODO: revert the db snapshot + return Err(Error::CalleeReverted) + } + data + } else { + Default::default() + }; + scale::Decode::decode(&mut &data[..]) + .map_err(|err| Error::Decode(err)) + } + + fn invoke_contract_delegate( &mut self, params: &CallParams, Args, R>, @@ -442,21 +482,44 @@ impl TypedEnvBackend for EnvInstance { ) } - fn instantiate_contract( + fn instantiate_contract_begin( &mut self, params: &CreateParams, - ) -> Result + ) -> Result where E: Environment, Args: scale::Encode, Salt: AsRef<[u8]>, { - let _code_hash = params.code_hash(); + let code_hash = params.code_hash(); let _gas_limit = params.gas_limit(); - let _endowment = params.endowment(); - let _input = params.exec_input(); - let _salt_bytes = params.salt_bytes(); - unimplemented!("off-chain environment does not support contract instantiation") + let endowment = params.endowment(); // TODO: not considered yet + let input = params.exec_input(); + let _salt_bytes = params.salt_bytes(); // TODO: not considered yet + + if *endowment != num_traits::Zero::zero() { + unimplemented!("off-chain environment does not support value trnsfer when instantiating contracts") + } + + let hash = from_env::hash::(code_hash); + let input = scale::Encode::encode(&input); + + // gen address (hash [n]) + let account = self.contracts.next_address_of(&hash); + self.contracts.register_contract(hash, account.clone()); + + self.push_frame(&account, input.clone()); + let (deploy, _call) = self.contracts.entrypoints(&account) + .ok_or(Error::NotCallable)?; + Ok(deploy) + } + + fn instantiate_contract_end(&mut self) -> Result + where + E: Environment, + { + let frame = self.pop_frame().expect("frame must exist; qed."); + Ok(to_env::account_id::(&frame.callee)) } fn terminate_contract(&mut self, beneficiary: E::AccountId) -> ! @@ -495,31 +558,62 @@ impl TypedEnvBackend for EnvInstance { scale::Decode::decode(&mut &output[..]).map_err(Into::into) } - fn is_contract(&mut self, _account: &E::AccountId) -> bool + fn is_contract(&mut self, account: &E::AccountId) -> bool where E: Environment, { - unimplemented!("off-chain environment does not support contract instantiation") + self.contracts.is_contract(&from_env::account_id::(account)) } fn caller_is_origin(&mut self) -> bool where E: Environment, { - unimplemented!("off-chain environment does not support cross-contract calls") + let origin = self.stack.origin(); + let ctx = self.stack.peek(); + assert!(ctx.level > 0, "should never reach when there's no running contract"); + ctx.caller.expect("contract has caller; qed.") == origin } - fn code_hash(&mut self, _account: &E::AccountId) -> Result + fn code_hash(&mut self, account: &E::AccountId) -> Result where E: Environment, { - unimplemented!("off-chain environment does not support `code_hash`") + self + .contracts + .code_hash(&from_env::account_id::(account)) + .map(|h| to_env::hash::(&h)) + .ok_or(Error::CodeNotFound) } fn own_code_hash(&mut self) -> Result where E: Environment, { - unimplemented!("off-chain environment does not support `own_code_hash`") + let me = self.stack.peek().callee; + let hash = self.contracts.code_hash(&me) + .expect("contract must has code hash"); + Ok(to_env::hash::(&hash)) + } +} + +mod from_env { + pub fn account_id(account: &E::AccountId) -> crate::AccountId { + crate::AccountId::try_from(account.as_ref()).unwrap() + } + pub fn hash(hash: &E::Hash) -> crate::Hash { + crate::Hash::try_from(hash.as_ref()).unwrap() + } +} + +mod to_env { + use scale::{Encode, Decode}; + pub fn account_id(account: &crate::AccountId) -> E::AccountId { + let raw = account.encode(); + Decode::decode(&mut &raw[..]).unwrap() + } + pub fn hash(hash: &crate::Hash) -> E::Hash { + let raw = hash.encode(); + Decode::decode(&mut &raw[..]).unwrap() } } diff --git a/crates/env/src/engine/off_chain/mod.rs b/crates/env/src/engine/off_chain/mod.rs index 2d2d0be6e62..4df553732b5 100644 --- a/crates/env/src/engine/off_chain/mod.rs +++ b/crates/env/src/engine/off_chain/mod.rs @@ -16,6 +16,7 @@ mod call_data; mod impls; pub mod test_api; mod types; +mod stack; #[cfg(test)] mod tests; @@ -24,6 +25,7 @@ pub use call_data::CallData; use super::OnInstance; use crate::Error; +use stack::{Stack, Frame, ContractStore}; use derive_more::From; use ink_engine::ext::Engine; @@ -31,6 +33,34 @@ use ink_engine::ext::Engine; /// The off-chain environment. pub struct EnvInstance { engine: Engine, + stack: Stack, + pub contracts: ContractStore, +} + +impl EnvInstance { + fn sync_stack(&mut self) { + use scale::Encode; + + let ctx = self.stack.peek(); + if let Some(caller) = ctx.caller { + self.engine.set_caller(caller.encode()); + } + self.engine.set_callee(ctx.callee.encode()); + } + + fn push_frame(&mut self, callee: &crate::AccountId, input: Vec) { + self.stack.push(callee, input); + self.sync_stack(); + } + + fn pop_frame(&mut self) -> Option { + let ctx = self.stack.pop(); + if ctx.is_some() { + self.sync_stack(); + } + ctx + } + } impl OnInstance for EnvInstance { @@ -42,7 +72,9 @@ impl OnInstance for EnvInstance { thread_local!( static INSTANCE: RefCell = RefCell::new( EnvInstance { - engine: Engine::new() + engine: Engine::new(), + stack: Stack::new(crate::AccountId::from([1u8; 32])), + contracts: Default::default(), } ) ); diff --git a/crates/env/src/engine/off_chain/stack.rs b/crates/env/src/engine/off_chain/stack.rs new file mode 100644 index 00000000000..2331304859e --- /dev/null +++ b/crates/env/src/engine/off_chain/stack.rs @@ -0,0 +1,129 @@ +use std::vec::Vec; +use std::collections::BTreeMap; + +use crate::{AccountId, Hash, account_id, ReturnFlags}; + + +/// A frame in the call stack +#[derive(Clone, Debug)] +pub struct Frame { + pub level: u32, + pub caller: Option, + pub callee: AccountId, + pub input: Vec, + pub return_value: Option<(ReturnFlags, Vec)> +} + +pub struct Stack { + pub stack: Vec, +} + +impl Stack { + /// Crates a call stack with the default `account` + pub fn new(account: AccountId) -> Self { + Self { + stack: vec![Frame { + level: 0, + caller: None, + callee: account, + input: Default::default(), + return_value: None, + }], + } + } + + /// Changes the caller account + /// + /// Only allowed outside any contract call (when the stack is empty). + pub fn switch_account(&mut self, account: AccountId) -> Result<(), ()> { + let stack = &mut self.stack; + if stack.len() != 1 { + return Err(()) + } + let ctx = stack.get_mut(0).ok_or(())?; + ctx.callee = account; + Ok(()) + } + + + /// Pushes a new call frame + pub fn push(&mut self, callee: &AccountId, input: Vec) { + let parent_ctx = self.peek(); + self.stack.push(Frame { + level: parent_ctx.level + 1, + caller: Some(parent_ctx.callee), + callee: callee.clone(), + input, + return_value: None, + }); + self.sync_to_ink(); + } + + /// Pops the call frame and returns the frame + pub fn pop(&mut self) -> Option { + if self.stack.len() > 1 { + let ctx = self.stack.pop(); + self.sync_to_ink(); + ctx + } else { + None + } + } + + /// Peeks the current call frame + pub fn peek(&self) -> Frame { + self.stack.last().cloned().expect("stack is never empty; qed.") + } + + pub fn set_return_value(&mut self, flags: ReturnFlags, value: Vec) { + let cur = self.stack.last_mut().expect("stack is never empty; qed."); + cur.return_value = Some((flags, value)); + } + + pub fn origin(&self) -> AccountId { + self.stack.first().expect("stack is never empty; qed").callee + } + + /// Syncs the top call frame to ink testing environment + pub fn sync_to_ink(&self) {} +} + +#[derive(Default)] +pub struct ContractStore { + code: BTreeMap, + fns: BTreeMap, +} + +impl ContractStore { + pub fn register_contract(&mut self, code: Hash, id: AccountId) { + self.code.insert(id, code); + } + pub fn register_entrypoints(&mut self, code: Hash, deploy: fn(), call: fn()) + { + self.fns.insert(code, (deploy, call)); + } + pub fn entrypoints(&self, account: &AccountId) -> Option<(fn(), fn())> { + let code = self.code.get(account)?; + self.fns.get(code).cloned() + } + pub fn is_contract(&self, account: &AccountId) -> bool { + self.code.contains_key(account) + } + pub fn code_hash(&self, account: &AccountId) -> Option { + self.code.get(account).cloned() + } + pub fn update_code(&mut self, account: AccountId, code: Hash) { + // account can be non-existing? + self.code.insert(account, code); + } + pub fn next_address_of(&self, code: &Hash) -> AccountId { + let count = self.code + .iter() + .filter(|(_k, v)| *v == code) + .count(); + let mut raw = [0u8; 32]; + raw.copy_from_slice(code.as_ref()); + raw[raw.len() - 1] = count as u8; + AccountId::from(raw) + } +} diff --git a/crates/env/src/engine/off_chain/test_api.rs b/crates/env/src/engine/off_chain/test_api.rs index 4d73be03ac3..29d0c31fce0 100644 --- a/crates/env/src/engine/off_chain/test_api.rs +++ b/crates/env/src/engine/off_chain/test_api.rs @@ -355,3 +355,18 @@ pub fn assert_contract_termination( assert_eq!(value_transferred, expected_value_transferred_to_beneficiary); assert_eq!(beneficiary, expected_beneficiary); } + +/// TODO +pub fn register_contract(code_hash: crate::Hash) +where + T: Environment, + C: crate::reflect::ContractEntrypoints + 'static, +{ + ::on_instance(|instance| { + let deploy = ::deploy; + let call = ::call; + instance + .contracts + .register_entrypoints(code_hash, deploy, call); + }) +} diff --git a/crates/env/src/engine/on_chain/impls.rs b/crates/env/src/engine/on_chain/impls.rs index e8a98d71c6f..c38fdd30b7c 100644 --- a/crates/env/src/engine/on_chain/impls.rs +++ b/crates/env/src/engine/on_chain/impls.rs @@ -256,7 +256,7 @@ impl EnvBackend for EnvInstance { self.get_property::(ext::input) } - fn return_value(&mut self, flags: ReturnFlags, return_value: &R) -> ! + fn return_value(&mut self, flags: ReturnFlags, return_value: &R) where R: scale::Encode, { diff --git a/crates/env/src/lib.rs b/crates/env/src/lib.rs index 306b6ea774e..674c4f3902a 100644 --- a/crates/env/src/lib.rs +++ b/crates/env/src/lib.rs @@ -110,6 +110,17 @@ pub use self::{ }, }; +/// TODO +pub mod reflect { + /// TODO + pub trait ContractEntrypoints { + /// TODO + fn call(); + /// TODO + fn deploy(); + } +} + cfg_if::cfg_if! { if #[cfg(any(feature = "ink-debug", feature = "std"))] { /// Required by the `debug_print*` macros below, because there is no guarantee that diff --git a/crates/lang/codegen/src/generator/dispatch.rs b/crates/lang/codegen/src/generator/dispatch.rs index 6ea44b7a950..b8e6f16054c 100644 --- a/crates/lang/codegen/src/generator/dispatch.rs +++ b/crates/lang/codegen/src/generator/dispatch.rs @@ -79,8 +79,8 @@ impl GenerateCode for Dispatch<'_> { #constructor_decoder_type #message_decoder_type - #[cfg(not(test))] - #[cfg(not(feature = "ink-as-dependency"))] + //#[cfg(not(test))] + //#[cfg(not(feature = "ink-as-dependency"))] const _: () = { #entry_points }; @@ -401,8 +401,7 @@ impl Dispatch<'_> { let any_message_accept_payment = self.any_message_accepts_payment_expr(message_spans); quote_spanned!(span=> - #[cfg(not(test))] - #[no_mangle] + #[cfg_attr(all(not(feature = "ink-as-dependency"), not(test)), no_mangle)] #[allow(clippy::nonminimal_bool)] fn deploy() { if !#any_constructor_accept_payment { @@ -422,8 +421,7 @@ impl Dispatch<'_> { }) } - #[cfg(not(test))] - #[no_mangle] + #[cfg_attr(all(not(feature = "ink-as-dependency"), not(test)), no_mangle)] #[allow(clippy::nonminimal_bool)] fn call() { if !#any_message_accept_payment { @@ -442,6 +440,15 @@ impl Dispatch<'_> { ::core::panic!("dispatching ink! message failed: {}", error) }) } + + impl ::ink_lang::reflect::ContractEntrypoints for #storage_ident { + fn deploy() { + deploy(); + } + fn call() { + call(); + } + } ) } diff --git a/crates/lang/src/codegen/dispatch/execution.rs b/crates/lang/src/codegen/dispatch/execution.rs index 749b1d841dc..db915611c90 100644 --- a/crates/lang/src/codegen/dispatch/execution.rs +++ b/crates/lang/src/codegen/dispatch/execution.rs @@ -97,7 +97,8 @@ where >( ReturnFlags::default().set_reverted(true), result.return_value(), - ) + ); + panic!("execute_constructor reverted"); } } } diff --git a/crates/lang/src/reflect/dispatch.rs b/crates/lang/src/reflect/dispatch.rs index e18e5212214..a350a2d7956 100644 --- a/crates/lang/src/reflect/dispatch.rs +++ b/crates/lang/src/reflect/dispatch.rs @@ -653,3 +653,5 @@ impl From for scale::Error { pub trait DecodeDispatch: scale::Decode { fn decode_dispatch(input: &mut I) -> Result; } + +pub use ink_env::reflect::ContractEntrypoints; diff --git a/crates/lang/src/reflect/mod.rs b/crates/lang/src/reflect/mod.rs index 3f59becff34..8f54e6cef4f 100644 --- a/crates/lang/src/reflect/mod.rs +++ b/crates/lang/src/reflect/mod.rs @@ -45,6 +45,7 @@ pub use self::{ DispatchableConstructorInfo, DispatchableMessageInfo, ExecuteDispatchable, + ContractEntrypoints, }, event::ContractEventBase, trait_def::{ diff --git a/examples/delegator/adder/lib.rs b/examples/delegator/adder/lib.rs index 5996ed1a787..e87767f38dd 100644 --- a/examples/delegator/adder/lib.rs +++ b/examples/delegator/adder/lib.rs @@ -32,3 +32,39 @@ mod adder { } } } + +#[cfg(test)] +mod test { + #[test] + fn it_works() { + use super::*; + use accumulator::{Accumulator, AccumulatorRef}; + + // register Accumulator & Adder + let hash1 = ink_env::Hash::try_from([10u8; 32]).unwrap(); + let hash2 = ink_env::Hash::try_from([20u8; 32]).unwrap(); + ink_env::test::register_contract::( + hash1.clone() + ); + ink_env::test::register_contract::( + hash2.clone() + ); + + let acc = AccumulatorRef::new(0) + .code_hash(hash1.clone()) + .endowment(0) + .salt_bytes([0u8; 0]) + .instantiate() + .expect("failed at instantiating the `AccumulatorRef` contract"); + let mut adder = AdderRef::new(acc.clone()) + .code_hash(hash2.clone()) + .endowment(0) + .salt_bytes([0u8; 0]) + .instantiate() + .expect("failed at instantiating the `AdderRef` contract"); + + assert_eq!(acc.get(), 0); + adder.inc(1); + assert_eq!(acc.get(), 1); + } +}