From 27250bae0c9c3160cbbd165e199c25ae71035306 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20P=C5=82askonka?= Date: Tue, 14 Nov 2023 11:38:15 +0100 Subject: [PATCH] Envs in Odra Core (#243) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * New approach to the module building - using Env object. * New structure of crates. Co-authored-by: Maciej ZieliƄski --- .DS_Store | Bin 0 -> 8196 bytes .github/workflows/odra-ci.yml | 6 +- Cargo.toml | 9 +- core/src/lib.rs | 10 + core/src/test_utils.rs | 5 +- examples/resources/erc20_schema.json | 31 +- examples2/.gitignore | 3 + examples2/Cargo.toml | 22 + examples2/bin/build_contract.rs | 5 + examples2/bin/build_schema.rs | 12 + examples2/build.rs | 8 + examples2/src/counter.rs | 94 +++ examples2/src/counter_pack.rs | 283 +++++++++ examples2/src/erc20.rs | 596 ++++++++++++++++++ examples2/src/lib.rs | 11 + examples2/src/ownable.rs | 34 + justfile | 23 +- lang/codegen/src/generator/common.rs | 54 +- lang/codegen/src/generator/event_item/mod.rs | 24 +- lang/proc-macros/Cargo.toml | 4 +- mock-vm/backend/src/contract_env.rs | 3 +- mock-vm/backend/src/mock_vm.rs | 3 +- mock-vm/backend/src/test_env.rs | 6 +- modules/src/erc20.rs | 1 - odra-casper/backend/src/casper_env.rs | 2 +- odra-casper/backend/src/contract_env.rs | 3 +- .../livenet/resources/proxy_caller.wasm | Bin 40935 -> 41349 bytes odra-casper/livenet/src/contract_env.rs | 2 +- odra-casper/proxy-caller/src/lib.rs | 6 +- odra-casper/shared/src/consts.rs | 3 + .../resources/proxy_caller_with_return.wasm | Bin 42963 -> 43377 bytes .../test-env/src/dummy_contract_env.rs | 3 +- odra-casper/test-env/src/env.rs | 2 +- odra-casper/test-env/src/test_env.rs | 2 +- odra-casper/test-vm/Cargo.toml | 26 + odra-casper/test-vm/resources/chainspec.toml | 261 ++++++++ .../resources/proxy_caller_with_return.wasm | Bin 0 -> 43377 bytes odra-casper/test-vm/src/casper_host.rs | 83 +++ odra-casper/test-vm/src/lib.rs | 9 + odra-casper/test-vm/src/vm/casper_vm.rs | 426 +++++++++++++ odra-casper/test-vm/src/vm/mod.rs | 1 + odra-casper/wasm-env/Cargo.toml | 19 + odra-casper/wasm-env/src/consts.rs | 58 ++ odra-casper/wasm-env/src/host_functions.rs | 474 ++++++++++++++ odra-casper/wasm-env/src/lib.rs | 25 + odra-casper/wasm-env/src/wasm_contract_env.rs | 59 ++ odra-core/Cargo.toml | 8 + odra-core/src/contract_context.rs | 15 + odra-core/src/contract_env.rs | 104 +++ odra-core/src/entry_point_callback.rs | 22 + {types => odra-core}/src/event.rs | 18 +- odra-core/src/host_context.rs | 24 + odra-core/src/host_env.rs | 92 +++ odra-core/src/key_maker.rs | 38 ++ odra-core/src/lib.rs | 28 + odra-core/src/mapping.rs | 53 ++ odra-core/src/module.rs | 70 ++ odra-core/src/odra_result.rs | 3 + odra-core/src/prelude.rs | 38 ++ odra-core/src/utils.rs | 0 odra-core/src/variable.rs | 40 ++ odra-vm/Cargo.toml | 12 + odra-vm/src/lib.rs | 10 + odra-vm/src/odra_vm_contract_env.rs | 57 ++ odra-vm/src/odra_vm_host.rs | 77 +++ odra-vm/src/vm/balance.rs | 54 ++ odra-vm/src/vm/callstack.rs | 74 +++ odra-vm/src/vm/contract_container.rs | 247 ++++++++ odra-vm/src/vm/contract_register.rs | 45 ++ odra-vm/src/vm/mod.rs | 12 + odra-vm/src/vm/odra_vm.rs | 563 +++++++++++++++++ odra-vm/src/vm/odra_vm_state.rs | 244 +++++++ odra-vm/src/vm/storage.rs | 303 +++++++++ odra2/Cargo.toml | 20 + odra2/src/lib.rs | 48 ++ types/Cargo.toml | 1 + types/src/call_def.rs | 44 ++ types/src/contract_def.rs | 5 + types/src/error.rs | 5 + types/src/lib.rs | 15 +- 80 files changed, 5012 insertions(+), 88 deletions(-) create mode 100644 .DS_Store create mode 100644 examples2/.gitignore create mode 100644 examples2/Cargo.toml create mode 100644 examples2/bin/build_contract.rs create mode 100644 examples2/bin/build_schema.rs create mode 100644 examples2/build.rs create mode 100644 examples2/src/counter.rs create mode 100644 examples2/src/counter_pack.rs create mode 100644 examples2/src/erc20.rs create mode 100644 examples2/src/lib.rs create mode 100644 examples2/src/ownable.rs create mode 100644 odra-casper/test-vm/Cargo.toml create mode 100644 odra-casper/test-vm/resources/chainspec.toml create mode 100755 odra-casper/test-vm/resources/proxy_caller_with_return.wasm create mode 100644 odra-casper/test-vm/src/casper_host.rs create mode 100644 odra-casper/test-vm/src/lib.rs create mode 100644 odra-casper/test-vm/src/vm/casper_vm.rs create mode 100644 odra-casper/test-vm/src/vm/mod.rs create mode 100644 odra-casper/wasm-env/Cargo.toml create mode 100644 odra-casper/wasm-env/src/consts.rs create mode 100644 odra-casper/wasm-env/src/host_functions.rs create mode 100644 odra-casper/wasm-env/src/lib.rs create mode 100644 odra-casper/wasm-env/src/wasm_contract_env.rs create mode 100644 odra-core/Cargo.toml create mode 100644 odra-core/src/contract_context.rs create mode 100644 odra-core/src/contract_env.rs create mode 100644 odra-core/src/entry_point_callback.rs rename {types => odra-core}/src/event.rs (53%) create mode 100644 odra-core/src/host_context.rs create mode 100644 odra-core/src/host_env.rs create mode 100644 odra-core/src/key_maker.rs create mode 100644 odra-core/src/lib.rs create mode 100644 odra-core/src/mapping.rs create mode 100644 odra-core/src/module.rs create mode 100644 odra-core/src/odra_result.rs create mode 100644 odra-core/src/prelude.rs create mode 100644 odra-core/src/utils.rs create mode 100644 odra-core/src/variable.rs create mode 100644 odra-vm/Cargo.toml create mode 100644 odra-vm/src/lib.rs create mode 100644 odra-vm/src/odra_vm_contract_env.rs create mode 100644 odra-vm/src/odra_vm_host.rs create mode 100644 odra-vm/src/vm/balance.rs create mode 100644 odra-vm/src/vm/callstack.rs create mode 100644 odra-vm/src/vm/contract_container.rs create mode 100644 odra-vm/src/vm/contract_register.rs create mode 100644 odra-vm/src/vm/mod.rs create mode 100644 odra-vm/src/vm/odra_vm.rs create mode 100644 odra-vm/src/vm/odra_vm_state.rs create mode 100644 odra-vm/src/vm/storage.rs create mode 100644 odra2/Cargo.toml create mode 100644 odra2/src/lib.rs create mode 100644 types/src/call_def.rs diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..8b5658987d56dc9efe5fa994d8ffceb376310072 GIT binary patch literal 8196 zcmeHMQBTuQ6h0RQ>lnfwG~q#$CceT%R3hICYVi;(Q8f@3XDqTy~j%f&F&-x$y z3%>eG{29Lcq~E=_BfVYm(U|OB(tFz5@9XLL&gpIMAR<=pJ9mkeh{(XkZKaGZr}6WA z8*5X^$W2%VJ~jQYZTq2(Cn0$KsBfL1^&@INSkcQ$Xv74Lm{R`*%~t-wSo zz~=`O8@IK@4y6@S2Rg9?fGuEHH0+B{e^`?bz}6BwlolgsOj2Pbl_g6ImaH6@q~m-o z!4IXCbYj9Yu9J9X$qI!f3lC-rII)_vy4MP51;!QNYxe?G>4-dv)%833g~#jSw`hV` z9O0-#eX>>b)KLaKcZv3J^?*X^k*jiO6P%wJ4<2>N0>h;!gZ}n+v^|JUl3yRb+b6yk z#jC47*wl1kX0~V)jk58s)d>f!*oym6-RiyJM|)xD+oOHUe(AY`gUa=-Fo-QLaC?H_ zIW8b?UwMHO4(ee)a3aBN$rDD&C>>Om&d%00)|bt-hY!w|&9jZQ)n)Vkz4i0+l5ykq zokzO|$9^{mKg$HLWD=umnS^}7F;_XI!D*`(_+bEb`CLkKv;}o~&?f}WN8hI7j80IM zswq`fP7F?${0s-$Dx7)vd4l{rr^nQwr?dlG1{f=i2;L4@d$dbUs?$F9n&K*X zY#wj=P78=8r%re!DmVkx@ew+7=>!^#RSK`g4E_TuHlmmgp)D9{6ub`UYf7;KFcxa| zD1+lFrEx_l(?rA#MA)K+iWb_a=&sBct3}#R==3(()U^B*+gFV zMHTPS>%2P^GhUiCi>u3L)6LGMLdT1gjwN8-q<$^2pEuj388bkA5?Re4g<>kQ4=k_q zERyS!Mxx|&n^*;=W#mg`*v8er|4*zwI&iJPBq+cN+pX<7A{~8}W`pKyZ4=vjY`k&Y vP+CDjCzj)|VmS_b`42( contract_address: Address, at: i32 -) -> Result { +) -> Result { crate::test_env::get_event(contract_address, at) } diff --git a/examples/resources/erc20_schema.json b/examples/resources/erc20_schema.json index d49a0438..0390314d 100644 --- a/examples/resources/erc20_schema.json +++ b/examples/resources/erc20_schema.json @@ -163,5 +163,34 @@ } ] } - ] + ], + "types": [{ + "name": "Transfer", + "fields": [ + { + "name": "from", + "ty": { + "Option": "Key" + } + }, + { + "name": "to", + "ty": { + "Option": "Key" + } + }, + { + "name": "amount", + "ty": "U256" + } + ] + }], + "storage_layout": [{ + "key_name": "STATE", + "is_dictionary": true, + "dictionary_keys": [ + { "name": "0", "ty": "Key" }, + { "name": "1", "ty": "_#Key" } + ] + }] } \ No newline at end of file diff --git a/examples2/.gitignore b/examples2/.gitignore new file mode 100644 index 00000000..e0aef76c --- /dev/null +++ b/examples2/.gitignore @@ -0,0 +1,3 @@ +/target +/Cargo.lock +/wasm/* \ No newline at end of file diff --git a/examples2/Cargo.toml b/examples2/Cargo.toml new file mode 100644 index 00000000..c042b938 --- /dev/null +++ b/examples2/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "examples2" +version = "0.1.0" +edition = "2021" + +[dependencies] +odra2 = { path = "../odra2" } + +[[bin]] +name = "contract" +path = "bin/build_contract.rs" + +[[bin]] +name = "build_schema" +path = "bin/build_schema.rs" + +[profile.release] +codegen-units = 1 +lto = true + +[profile.dev.package."*"] +opt-level = 3 diff --git a/examples2/bin/build_contract.rs b/examples2/bin/build_contract.rs new file mode 100644 index 00000000..cf398246 --- /dev/null +++ b/examples2/bin/build_contract.rs @@ -0,0 +1,5 @@ +#![no_std] +#![no_main] + +#![allow(unused_imports)] +use examples2; diff --git a/examples2/bin/build_schema.rs b/examples2/bin/build_schema.rs new file mode 100644 index 00000000..e8e33d82 --- /dev/null +++ b/examples2/bin/build_schema.rs @@ -0,0 +1,12 @@ +#[allow(unused_imports)] +use examples2; +use odra2::types::contract_def::ContractBlueprint2; + +extern "Rust" { + fn module_schema() -> ContractBlueprint2; +} + +fn main() { + let schema = unsafe { module_schema() }; + println!("{:#?}", schema); +} diff --git a/examples2/build.rs b/examples2/build.rs new file mode 100644 index 00000000..7f61e3e8 --- /dev/null +++ b/examples2/build.rs @@ -0,0 +1,8 @@ +use std::env; + +pub fn main() { + println!("cargo:rerun-if-env-changed=ODRA_MODULE"); + let module = env::var("ODRA_MODULE").unwrap_or_else(|_| "".to_string()); + let msg = format!("cargo:rustc-cfg=odra_module=\"{}\"", module); + println!("{}", msg); +} diff --git a/examples2/src/counter.rs b/examples2/src/counter.rs new file mode 100644 index 00000000..a40bd05f --- /dev/null +++ b/examples2/src/counter.rs @@ -0,0 +1,94 @@ +use odra2::prelude::*; +use odra2::types::{runtime_args, FromBytes, RuntimeArgs}; +use odra2::{CallDef, ContractEnv, HostEnv, Mapping, Variable}; + +pub struct Counter { + env: Rc, + count0: Variable, + count1: Variable, + count2: Variable, + count3: Variable, + count4: Variable, + count5: Variable, + count6: Variable, + count7: Variable, + count8: Variable, + count9: Variable, + counts: Mapping +} + +impl Counter { + pub fn get_count(&self, index: u8) -> u32 { + match index { + 0 => self.count0.get_or_default(), + 1 => self.count1.get_or_default(), + 2 => self.count2.get_or_default(), + 3 => self.count3.get_or_default(), + 4 => self.count4.get_or_default(), + 5 => self.count5.get_or_default(), + 6 => self.count6.get_or_default(), + 7 => self.count7.get_or_default(), + 8 => self.count8.get_or_default(), + 9 => self.count9.get_or_default(), + _ => unreachable!() + } + } + + pub fn increment(&mut self, index: u8) { + match index { + 0 => increment(&mut self.count0), + 1 => increment(&mut self.count1), + 2 => increment(&mut self.count2), + 3 => increment(&mut self.count3), + 4 => increment(&mut self.count4), + 5 => increment(&mut self.count5), + 6 => increment(&mut self.count6), + 7 => increment(&mut self.count7), + 8 => increment(&mut self.count8), + 9 => increment(&mut self.count9), + _ => unreachable!() + }; + } +} + +fn increment(count: &mut Variable) { + let a = count.get_or_default(); + count.set(a + 1); +} + +mod __counter_pack_module { + use super::*; + impl odra2::module::Module for Counter { + fn new(env: Rc) -> Self { + let count0 = Variable::new(Rc::clone(&env), 0); + let count1 = Variable::new(Rc::clone(&env), 1); + let count2 = Variable::new(Rc::clone(&env), 2); + let count3 = Variable::new(Rc::clone(&env), 3); + let count4 = Variable::new(Rc::clone(&env), 4); + let count5 = Variable::new(Rc::clone(&env), 5); + let count6 = Variable::new(Rc::clone(&env), 6); + let count7 = Variable::new(Rc::clone(&env), 7); + let count8 = Variable::new(Rc::clone(&env), 8); + let count9 = Variable::new(Rc::clone(&env), 9); + let counts = Mapping::new(Rc::clone(&env), 10); + Self { + env, + count0, + count1, + count2, + count3, + count4, + count5, + count6, + count7, + count8, + count9, + counts + } + } + + fn env(&self) -> Rc { + self.env.clone() + } + } +} diff --git a/examples2/src/counter_pack.rs b/examples2/src/counter_pack.rs new file mode 100644 index 00000000..5f6f1fbe --- /dev/null +++ b/examples2/src/counter_pack.rs @@ -0,0 +1,283 @@ +use crate::counter::Counter; +use odra2::prelude::*; +use odra2::ContractEnv; +use odra2::Mapping; +use odra2::ModuleWrapper; + +pub struct CounterPack { + env: Rc, + counter0: ModuleWrapper, + counter1: ModuleWrapper, + counter2: ModuleWrapper, + counter3: ModuleWrapper, + counter4: ModuleWrapper, + counter5: ModuleWrapper, + counter6: ModuleWrapper, + counter7: ModuleWrapper, + counter8: ModuleWrapper, + counter9: ModuleWrapper, + counters: Mapping<(u8, u8), u32>, + counters_map: Mapping +} + +impl CounterPack { + pub fn get_count(&self, index_a: u8, index_b: u8) -> u32 { + match index_a { + 0 => self.counter0.get_count(index_b), + 1 => self.counter1.get_count(index_b), + 2 => self.counter2.get_count(index_b), + 3 => self.counter3.get_count(index_b), + 4 => self.counter4.get_count(index_b), + 5 => self.counter5.get_count(index_b), + 6 => self.counter6.get_count(index_b), + 7 => self.counter7.get_count(index_b), + 8 => self.counter8.get_count(index_b), + 9 => self.counter9.get_count(index_b), + _ => unreachable!() + } + // self.counters.get_or_default((index_a, index_b)) + // self.counters_map.module(index_a).get_count(index_b) + } + + pub fn increment(&mut self, index_a: u8, index_b: u8) { + match index_a { + 0 => self.counter0.increment(index_b), + 1 => self.counter1.increment(index_b), + 2 => self.counter2.increment(index_b), + 3 => self.counter3.increment(index_b), + 4 => self.counter4.increment(index_b), + 5 => self.counter5.increment(index_b), + 6 => self.counter6.increment(index_b), + 7 => self.counter7.increment(index_b), + 8 => self.counter8.increment(index_b), + 9 => self.counter9.increment(index_b), + _ => unreachable!() + }; + // let count = self.counters.get_or_default((index_a, index_b)); + // self.counters.set((index_a, index_b), count + 1); + // self.counters_map.module(index_a).increment(index_b); + } +} + +// autogenerated +mod odra_core_module { + use super::*; + + impl Module for CounterPack { + fn new(env: Rc) -> Self { + let counter0 = ModuleWrapper::new(Rc::clone(&env), 0); + let counter1 = ModuleWrapper::new(Rc::clone(&env), 1); + let counter2 = ModuleWrapper::new(Rc::clone(&env), 2); + let counter3 = ModuleWrapper::new(Rc::clone(&env), 3); + let counter4 = ModuleWrapper::new(Rc::clone(&env), 4); + let counter5 = ModuleWrapper::new(Rc::clone(&env), 5); + let counter6 = ModuleWrapper::new(Rc::clone(&env), 6); + let counter7 = ModuleWrapper::new(Rc::clone(&env), 7); + let counter8 = ModuleWrapper::new(Rc::clone(&env), 8); + let counter9 = ModuleWrapper::new(Rc::clone(&env), 9); + let counters = Mapping::new(Rc::clone(&env), 10); + let counters_map = Mapping::new(Rc::clone(&env), 11); + Self { + env, + counter0, + counter1, + counter2, + counter3, + counter4, + counter5, + counter6, + counter7, + counter8, + counter9, + counters, + counters_map + } + } + + fn env(&self) -> Rc { + self.env.clone() + } + } +} + +#[cfg(odra_module = "CounterPack")] +#[cfg(target_arch = "wasm32")] +mod __counter_pack_wasm_parts { + use odra2::casper_event_standard::Schemas; + use odra2::odra_casper_backend2; + use odra2::odra_casper_backend2::casper_contract::unwrap_or_revert::UnwrapOrRevert; + use odra2::odra_casper_backend2::WasmContractEnv; + use odra2::types::casper_types::{ + CLType, CLTyped, CLValue, EntryPoint, EntryPointAccess, EntryPointType, EntryPoints, Group, + Parameter, RuntimeArgs + }; + use odra2::types::{runtime_args, Address, U256}; + use odra2::{prelude::*, ContractEnv}; + use odra_casper_backend2::casper_contract::contract_api::runtime; + + use super::CounterPack; + + extern crate alloc; + + pub fn entry_points() -> EntryPoints { + let mut entry_points = EntryPoints::new(); + entry_points.add_entry_point(EntryPoint::new( + "get_count", + alloc::vec![ + Parameter::new("index_a", CLType::U8), + Parameter::new("index_b", CLType::U8), + ], + CLType::U32, + EntryPointAccess::Public, + EntryPointType::Contract + )); + entry_points.add_entry_point(EntryPoint::new( + "increment", + alloc::vec![ + Parameter::new("index_a", CLType::U8), + Parameter::new("index_b", CLType::U8), + ], + CLType::Unit, + EntryPointAccess::Public, + EntryPointType::Contract + )); + entry_points + } + + pub fn execute_call() { + odra_casper_backend2::wasm_host::install_contract(entry_points(), Schemas::new(), None); + } + + pub fn execute_get_count() { + let index_a: u8 = runtime::get_named_arg("index_a"); + let index_b: u8 = runtime::get_named_arg("index_b"); + let env = WasmContractEnv::new(); + let contract: CounterPack = CounterPack::new(Rc::new(env)); + let result = contract.get_count(index_a, index_b); + runtime::ret(CLValue::from_t(result).unwrap_or_revert()); + } + + pub fn execute_increment() { + let index_a: u8 = runtime::get_named_arg("index_a"); + let index_b: u8 = runtime::get_named_arg("index_b"); + let env = WasmContractEnv::new(); + let mut contract: CounterPack = CounterPack::new(Rc::new(env)); + contract.increment(index_a, index_b); + } + + #[no_mangle] + fn call() { + execute_call(); + } + + #[no_mangle] + fn get_count() { + execute_get_count(); + } + + #[no_mangle] + fn increment() { + execute_increment(); + } +} + +#[cfg(not(target_arch = "wasm32"))] +mod __counter_pack_test_parts { + use odra2::types::{runtime_args, Bytes, RuntimeArgs, ToBytes, U256, U512}; + use odra2::{prelude::*, EntryPointsCaller}; + use odra2::{types::Address, CallDef, HostEnv}; + + use crate::counter_pack::CounterPack; + + pub struct CounterPackHostRef { + address: Address, + env: HostEnv + } + + impl CounterPackHostRef { + pub fn get_count(&self, index_a: u8, index_b: u8) -> u32 { + self.env.call_contract( + &self.address, + CallDef::new( + String::from("get_count"), + runtime_args! { + "index_a" => index_a, + "index_b" => index_b + } + ) + ) + } + + pub fn increment(&self, index_a: u8, index_b: u8) { + self.env.call_contract( + &self.address, + CallDef::new( + String::from("increment"), + runtime_args! { + "index_a" => index_a, + "index_b" => index_b + } + ) + ) + } + } + + pub struct CounterPackDeployer; + + impl CounterPackDeployer { + pub fn init(env: &HostEnv) -> CounterPackHostRef { + let epc = EntryPointsCaller::new(env.clone(), |contract_env, call_def| { + use odra2::types::ToBytes; + let mut counter_pack = CounterPack::new(Rc::new(contract_env)); + match call_def.method() { + "get_count" => { + let index_a: u8 = call_def.get("index_a").unwrap(); + let index_b: u8 = call_def.get("index_b").unwrap(); + let result = counter_pack.get_count(index_a, index_b); + Bytes::from(result.to_bytes().unwrap()) + } + "increment" => { + let index_a: u8 = call_def.get("index_a").unwrap(); + let index_b: u8 = call_def.get("index_b").unwrap(); + let result = counter_pack.increment(index_a, index_b); + Bytes::from(result.to_bytes().unwrap()) + } + _ => panic!("Unknown method") + } + }); + + let address = env.new_contract("counter_pack", None, Some(epc)); + + CounterPackHostRef { + address, + env: env.clone() + } + } + } +} + +#[cfg(not(target_arch = "wasm32"))] +pub use __counter_pack_test_parts::*; + +#[cfg(test)] +mod tests { + pub use super::*; + + #[test] + fn counter_pack_works() { + let env = odra2::test_env(); + let counter_pack = CounterPackDeployer::init(&env); + + let n: u8 = 3; + let m: u8 = 3; + for i in 0..n { + for j in 0..m { + assert_eq!(counter_pack.get_count(i, j), 0); + counter_pack.increment(i, j); + assert_eq!(counter_pack.get_count(i, j), 1); + } + } + + env.print_gas_report(); + } +} diff --git a/examples2/src/erc20.rs b/examples2/src/erc20.rs new file mode 100644 index 00000000..15625a5b --- /dev/null +++ b/examples2/src/erc20.rs @@ -0,0 +1,596 @@ +use odra2::casper_event_standard; +use odra2::{prelude::*, CallDef, Event, ModuleWrapper}; +use odra2::{ + types::{Address, U256, U512}, + ContractEnv, Mapping, Variable +}; + +#[derive(Event, Eq, PartialEq, Debug)] +pub struct Transfer { + pub from: Option
, + pub to: Option
, + pub amount: U256 +} + +#[derive(Event, Eq, PartialEq, Debug)] +pub struct Approval { + pub owner: Address, + pub spender: Address, + pub value: U256 +} + +pub struct Erc20 { + env: Rc, + total_supply: Variable, + balances: Mapping +} + +impl Erc20 { + pub fn init(&mut self, total_supply: Option) { + if let Some(total_supply) = total_supply { + self.total_supply.set(total_supply); + self.balances.set(self.env().caller(), total_supply); + } + } + + pub fn total_supply(&self) -> U256 { + self.total_supply.get_or_default() + } + + pub fn balance_of(&self, owner: Address) -> U256 { + self.balances.get_or_default(owner) + } + + pub fn transfer(&mut self, to: Address, value: U256) { + let caller = self.env().caller(); + let balances = &mut self.balances; + let from_balance = balances.get_or_default(caller); + let to_balance = balances.get_or_default(to); + if from_balance < value { + self.env().revert(1); + } + balances.set(caller, from_balance.saturating_sub(value)); + balances.set(to, to_balance.saturating_add(value)); + self.env.emit_event(Transfer { + from: Some(caller), + to: Some(to), + amount: value + }); + } + + pub fn cross_total(&self, other: Address) -> U256 { + let other_erc20 = Erc20ContractRef { + address: other, + env: self.env() + }; + + self.total_supply() + other_erc20.total_supply() + } + + pub fn pay_to_mint(&mut self) { + let attached_value = self.env().attached_value(); + if attached_value.is_zero() { + self.env.revert(666); + } + let caller = self.env().caller(); + let caller_balance = self.balance_of(caller); + self.balances + .set(caller, caller_balance + U256::from(attached_value.as_u64())); + self.total_supply + .set(self.total_supply() + U256::from(attached_value.as_u64())); + } + + pub fn get_current_block_time(&self) -> u64 { + self.env().get_block_time() + } + + pub fn burn_and_get_paid(&mut self, amount: U256) { + let caller = self.env().caller(); + let caller_balance = self.balance_of(caller); + if amount > caller_balance { + self.env().revert(1); + } + + self.balances.set(caller, caller_balance - amount); + self.total_supply.set(self.total_supply() - amount); + self.env() + .transfer_tokens(&caller, &U512::from(amount.as_u64())); + } +} + +// autogenerated for general purpose module. +mod __erc20_module { + use super::Erc20; + use odra2::{module::Module, prelude::*, ContractEnv, Mapping, Variable}; + + impl Module for Erc20 { + fn new(env: Rc) -> Self { + let total_supply = Variable::new(Rc::clone(&env), 1); + let balances = Mapping::new(Rc::clone(&env), 2); + Self { + env, + total_supply, + balances + } + } + + fn env(&self) -> Rc { + self.env.clone() + } + } +} + +#[cfg(odra_module = "Erc20")] +mod __erc20_schema { + use odra2::{prelude::String, types::contract_def::ContractBlueprint2}; + + #[no_mangle] + fn module_schema() -> ContractBlueprint2 { + ContractBlueprint2 { + name: String::from("Erc20") + } + } +} + +// autogenerated for the WasmContractEnv. +#[cfg(odra_module = "Erc20")] +#[cfg(target_arch = "wasm32")] +mod __erc20_wasm_parts { + use super::{Approval, Erc20, Transfer}; + use odra2::casper_event_standard::Schemas; + use odra2::odra_casper_wasm_env; + use odra2::odra_casper_wasm_env::casper_contract::contract_api::runtime; + use odra2::odra_casper_wasm_env::casper_contract::unwrap_or_revert::UnwrapOrRevert; + use odra2::odra_casper_wasm_env::WasmContractEnv; + use odra2::types::casper_types::{ + CLType, CLTyped, CLValue, EntryPoint, EntryPointAccess, EntryPointType, EntryPoints, Group, + Parameter, RuntimeArgs + }; + use odra2::types::{runtime_args, Address, U256}; + use odra2::{prelude::*, ContractEnv}; + + extern crate alloc; + + fn entry_points() -> EntryPoints { + let mut entry_points = EntryPoints::new(); + entry_points.add_entry_point(EntryPoint::new( + "init", + alloc::vec![Parameter::new("total_supply", Option::::cl_type()),], + CLType::Unit, + EntryPointAccess::Groups(alloc::vec![Group::new("constructor_group")]), + EntryPointType::Contract + )); + entry_points.add_entry_point(EntryPoint::new( + "total_supply", + alloc::vec![], + U256::cl_type(), + EntryPointAccess::Public, + EntryPointType::Contract + )); + entry_points.add_entry_point(EntryPoint::new( + "balance_of", + alloc::vec![Parameter::new("owner", Address::cl_type()),], + U256::cl_type(), + EntryPointAccess::Public, + EntryPointType::Contract + )); + entry_points.add_entry_point(EntryPoint::new( + "transfer", + alloc::vec![ + Parameter::new("to", Address::cl_type()), + Parameter::new("value", U256::cl_type()), + ], + CLType::Unit, + EntryPointAccess::Public, + EntryPointType::Contract + )); + entry_points.add_entry_point(EntryPoint::new( + "cross_total", + alloc::vec![Parameter::new("other", Address::cl_type()),], + U256::cl_type(), + EntryPointAccess::Public, + EntryPointType::Contract + )); + entry_points.add_entry_point(EntryPoint::new( + "pay_to_mint", + alloc::vec![], + CLType::Unit, + EntryPointAccess::Public, + EntryPointType::Contract + )); + entry_points.add_entry_point(EntryPoint::new( + "get_current_block_time", + alloc::vec![], + u64::cl_type(), + EntryPointAccess::Public, + EntryPointType::Contract + )); + entry_points.add_entry_point(EntryPoint::new( + "burn_and_get_paid", + alloc::vec![Parameter::new("amount", U256::cl_type()),], + CLType::Unit, + EntryPointAccess::Public, + EntryPointType::Contract + )); + entry_points + } + + pub fn execute_call() { + let schemas = Schemas::new(); //.with::().with::(); + let total_supply: Option = runtime::get_named_arg("total_supply"); + let init_args = runtime_args! { + "total_supply" => total_supply + }; + odra2::odra_casper_wasm_env::host_functions::install_contract( + entry_points(), + schemas, + Some(init_args) + ); + } + + pub fn execute_init() { + let total_supply: Option = runtime::get_named_arg("total_supply"); + let env = WasmContractEnv::new(); + let mut contract: Erc20 = Erc20::new(Rc::new(env)); + contract.init(total_supply); + } + + pub fn execute_total_supply() { + let env = WasmContractEnv::new(); + let contract: Erc20 = Erc20::new(Rc::new(env)); + let result = contract.total_supply(); + runtime::ret(CLValue::from_t(result).unwrap_or_revert()) + } + + pub fn execute_balance_of() { + let owner: Address = runtime::get_named_arg("owner"); + let env = WasmContractEnv::new(); + let contract: Erc20 = Erc20::new(Rc::new(env)); + let result = contract.balance_of(owner); + runtime::ret(CLValue::from_t(result).unwrap_or_revert()) + } + + pub fn execute_transfer() { + let to: Address = runtime::get_named_arg("to"); + let value: U256 = runtime::get_named_arg("value"); + let env = WasmContractEnv::new(); + let mut contract: Erc20 = Erc20::new(Rc::new(env)); + contract.transfer(to, value); + } + + pub fn execute_cross_total() { + let other: Address = runtime::get_named_arg("other"); + let env = WasmContractEnv::new(); + let contract: Erc20 = Erc20::new(Rc::new(env)); + let result = contract.cross_total(other); + runtime::ret(CLValue::from_t(result).unwrap_or_revert()) + } + + pub fn execute_pay_to_mint() { + let env = WasmContractEnv::new(); + odra2::odra_casper_wasm_env::host_functions::handle_attached_value(); + let mut contract: Erc20 = Erc20::new(Rc::new(env)); + contract.pay_to_mint(); + odra2::odra_casper_wasm_env::host_functions::clear_attached_value(); + } + + pub fn execute_get_current_block_time() { + let env = WasmContractEnv::new(); + let contract: Erc20 = Erc20::new(Rc::new(env)); + let result = contract.get_current_block_time(); + runtime::ret(CLValue::from_t(result).unwrap_or_revert()) + } + + pub fn execute_burn_and_get_paid() { + let amount: U256 = runtime::get_named_arg("amount"); + let env = WasmContractEnv::new(); + let mut contract: Erc20 = Erc20::new(Rc::new(env)); + contract.burn_and_get_paid(amount); + } + + #[no_mangle] + fn call() { + execute_call(); + } + + #[no_mangle] + fn init() { + execute_init(); + } + + #[no_mangle] + fn total_supply() { + execute_total_supply(); + } + + #[no_mangle] + fn balance_of() { + execute_balance_of(); + } + + #[no_mangle] + fn transfer() { + execute_transfer(); + } + + #[no_mangle] + fn cross_total() { + execute_cross_total() + } + + #[no_mangle] + fn pay_to_mint() { + execute_pay_to_mint(); + } + + #[no_mangle] + fn get_current_block_time() { + execute_get_current_block_time(); + } + + #[no_mangle] + fn burn_and_get_paid() { + execute_burn_and_get_paid(); + } +} + +// #[cfg(not(target_arch = "wasm32"))] +mod __erc20_test_parts { + use crate::erc20::Erc20; + use odra2::prelude::*; + use odra2::types::casper_types::EntryPoints; + use odra2::types::{runtime_args, Address, Bytes, RuntimeArgs, ToBytes, U256, U512, FromBytes}; + use odra2::{CallDef, ContractEnv, EntryPointsCaller, HostEnv}; + use odra2::casper_event_standard::EventInstance; + use odra2::event::EventError; + + pub struct Erc20ContractRef { + pub address: Address, + pub env: Rc + } + + impl Erc20ContractRef { + pub fn total_supply(&self) -> U256 { + self.env.call_contract( + self.address, + CallDef::new(String::from("total_supply"), RuntimeArgs::new()) + ) + } + } + + pub struct Erc20HostRef { + pub address: Address, + pub env: HostEnv, + pub attached_value: U512 + } + + impl Erc20HostRef { + pub fn with_tokens(&self, tokens: U512) -> Self { + Self { + address: self.address, + env: self.env.clone(), + attached_value: tokens + } + } + + pub fn total_supply(&self) -> U256 { + self.env.call_contract( + &self.address, + CallDef::new(String::from("total_supply"), RuntimeArgs::new()) + ) + } + + pub fn balance_of(&self, owner: Address) -> U256 { + self.env.call_contract( + &self.address, + CallDef::new( + String::from("balance_of"), + runtime_args! { + "owner" => owner + } + ) + ) + } + + pub fn transfer(&self, to: Address, value: U256) { + self.env.call_contract( + &self.address, + CallDef::new( + String::from("transfer"), + runtime_args! { + "to" => to, + "value" => value + } + ) + ) + } + + pub fn cross_total(&self, other: Address) -> U256 { + self.env.call_contract( + &self.address, + CallDef::new( + String::from("cross_total"), + runtime_args! { + "other" => other + } + ) + ) + } + + pub fn pay_to_mint(&self) { + self.env.call_contract( + &self.address, + CallDef::new( + String::from("pay_to_mint"), + runtime_args! { + "amount" => self.attached_value + } + ) + .with_amount(self.attached_value) + ) + } + + pub fn get_current_block_time(&self) -> u64 { + self.env.call_contract( + &self.address, + CallDef::new(String::from("get_current_block_time"), runtime_args! {}) + ) + } + + pub fn burn_and_get_paid(&self, amount: U256) { + self.env.call_contract( + &self.address, + CallDef::new( + String::from("burn_and_get_paid"), + runtime_args! { + "amount" => amount + } + ) + ) + } + + pub fn get_event(&self, index: i32) -> Result { + self.env.get_event(&self.address, index) + } + } + + pub struct Erc20Deployer; + + impl Erc20Deployer { + pub fn init(env: &HostEnv, total_supply: Option) -> Erc20HostRef { + let epc = EntryPointsCaller::new(env.clone(), |contract_env, call_def| { + use odra2::types::ToBytes; + let mut erc20 = Erc20::new(Rc::new(contract_env)); + match call_def.method() { + "init" => { + let total_supply: Option = call_def.get("total_supply").unwrap(); + let result = erc20.init(total_supply); + Bytes::from(result.to_bytes().unwrap()) + } + "total_supply" => { + let result = erc20.total_supply(); + Bytes::from(result.to_bytes().unwrap()) + } + "balance_of" => { + let owner: Address = call_def.get("owner").unwrap(); + let result = erc20.balance_of(owner); + Bytes::from(result.to_bytes().unwrap()) + } + "transfer" => { + let to: Address = call_def.get("to").unwrap(); + let value: U256 = call_def.get("value").unwrap(); + let result = erc20.transfer(to, value); + Bytes::from(result.to_bytes().unwrap()) + } + "cross_total" => { + let other: Address = call_def.get("other").unwrap(); + let result = erc20.cross_total(other); + Bytes::from(result.to_bytes().unwrap()) + } + "pay_to_mint" => { + let result = erc20.pay_to_mint(); + Bytes::from(result.to_bytes().unwrap()) + } + "get_current_block_time" => { + let result = erc20.get_current_block_time(); + Bytes::from(result.to_bytes().unwrap()) + } + "burn_and_get_paid" => { + let amount: U256 = call_def.get("amount").unwrap(); + let result = erc20.burn_and_get_paid(amount); + Bytes::from(result.to_bytes().unwrap()) + } + _ => panic!("Unknown method") + } + }); + + let address = env.new_contract( + "erc20", + Some(runtime_args! { + "total_supply" => total_supply + }), + Some(epc) + ); + + Erc20HostRef { + address, + env: env.clone(), + attached_value: U512::zero() + } + } + } +} + +// #[cfg(not(target_arch = "wasm32"))] +pub use __erc20_test_parts::*; +use odra2::types::RuntimeArgs; + +#[cfg(test)] +mod tests { + pub use super::*; + use odra2::types::ToBytes; + use odra2::types::U512; + + #[test] + fn erc20_works() { + let env = odra2::test_env(); + let alice = env.get_account(0); + let bob = env.get_account(1); + + // Deploy the contract as Alice. + let erc20 = Erc20Deployer::init(&env, Some(100.into())); + assert_eq!(erc20.total_supply(), 100.into()); + assert_eq!(erc20.balance_of(alice), 100.into()); + assert_eq!(erc20.balance_of(bob), 0.into()); + + // Transfer 10 tokens from Alice to Bob. + erc20.transfer(bob, 10.into()); + assert_eq!(erc20.balance_of(alice), 90.into()); + assert_eq!(erc20.balance_of(bob), 10.into()); + + // Transfer 10 tokens back to Alice. + env.set_caller(bob); + erc20.transfer(alice, 10.into()); + assert_eq!(erc20.balance_of(alice), 100.into()); + assert_eq!(erc20.balance_of(bob), 0.into()); + + // Test cross calls + let pobcoin = Erc20Deployer::init(&env, Some(100.into())); + assert_eq!(erc20.cross_total(pobcoin.address.clone()), 200.into()); + + // Test attaching value and balances + let initial_balance = U512::from(100000000000000000u64); + assert_eq!(env.balance_of(&erc20.address), 0.into()); + assert_eq!(env.balance_of(&alice), initial_balance); + + env.set_caller(alice); + pobcoin.with_tokens(100.into()).pay_to_mint(); + assert_eq!(env.balance_of(&pobcoin.address), 100.into()); + assert_eq!(pobcoin.total_supply(), 200.into()); + assert_eq!(pobcoin.balance_of(alice), 100.into()); + assert_eq!(pobcoin.balance_of(bob), 100.into()); + + assert_eq!(env.balance_of(&alice), initial_balance - U512::from(100)); + assert_eq!(env.balance_of(&pobcoin.address), 100.into()); + + // Test block time + let block_time = pobcoin.get_current_block_time(); + env.advance_block_time(12345); + let new_block_time = pobcoin.get_current_block_time(); + assert_eq!(block_time + 12345, new_block_time); + + // Test transfer from contract to account + env.set_caller(alice); + let current_balance = env.balance_of(&alice); + pobcoin.burn_and_get_paid(100.into()); + assert_eq!(env.balance_of(&alice), current_balance + U512::from(100)); + + // Test events + let event: Transfer = erc20.get_event(0).unwrap(); + assert_eq!(event.from, Some(alice)); + assert_eq!(event.to, Some(bob)); + assert_eq!(event.amount, 10.into()); + + env.print_gas_report() + } +} diff --git a/examples2/src/lib.rs b/examples2/src/lib.rs new file mode 100644 index 00000000..fca3c4d3 --- /dev/null +++ b/examples2/src/lib.rs @@ -0,0 +1,11 @@ +#![cfg_attr(not(test), no_std)] +#![cfg_attr(not(test), no_main)] +#![allow(dead_code)] +#![allow(unused_imports)] +#![allow(unused_variables)] + +extern crate alloc; + +pub mod counter; +pub mod counter_pack; +pub mod erc20; diff --git a/examples2/src/ownable.rs b/examples2/src/ownable.rs new file mode 100644 index 00000000..a0b0b751 --- /dev/null +++ b/examples2/src/ownable.rs @@ -0,0 +1,34 @@ +use alloc::rc::Rc; +use odra2::{ContractEnv, prelude::Module, ModuleWrapper}; + +pub struct Ownable { + env: Rc, + owner: Variable<1, u32>, +} + +impl Module for Ownable { + fn new(env: Rc) -> Self { + let owner = Variable::new(Rc::clone(&env)); + Ownable { env, owner } + } + + fn env(&self) -> &ContractEnv { + &self.env + } +} + +pub struct OwnableWrapper { + env: Rc, + ownable: ModuleWrapper<1, Ownable>, +} + +impl Module for OwnableWrapper { + fn new(env: Rc) -> Self { + let ownable = ModuleWrapper::new(Rc::clone(&env)); + OwnableWrapper { env, ownable } + } + + fn env(&self) -> &ContractEnv { + &self.env + } +} diff --git a/justfile b/justfile index 6f38db4c..592581a1 100644 --- a/justfile +++ b/justfile @@ -28,7 +28,7 @@ prepare-test-env: install-cargo-odra sudo apt install wabt build-proxy-callers: - cargo build -p odra-casper-proxy-caller --release --target wasm32-unknown-unknown + cd odra-casper/proxy-caller && cargo build --release --target wasm32-unknown-unknown --target-dir ../../target wasm-strip target/wasm32-unknown-unknown/release/proxy_caller.wasm wasm-strip target/wasm32-unknown-unknown/release/proxy_caller_with_return.wasm cp target/wasm32-unknown-unknown/release/proxy_caller.wasm \ @@ -67,3 +67,24 @@ clean: rm -f Cargo.lock cd examples && rm -f Cargo.lock cd modules && rm -f Cargo.lock + +build-erc20: + cd examples2 && ODRA_MODULE=Erc20 cargo build --release --target wasm32-unknown-unknown --bin contract + wasm-strip examples2/target/wasm32-unknown-unknown/release/contract.wasm + rm -rf examples2/wasm/erc20.wasm + mkdir -p examples2/wasm + mv examples2/target/wasm32-unknown-unknown/release/contract.wasm examples2/wasm/erc20.wasm + +test-erc20: build-erc20 + cd examples2 && ODRA_BACKEND=casper cargo test --lib erc20_works -- --nocapture + +build-counter-pack: + cd examples2 && ODRA_MODULE=CounterPack cargo build --release --target wasm32-unknown-unknown --bin counter_pack + wasm-strip examples2/target/wasm32-unknown-unknown/release/counter_pack.wasm + cp examples2/target/wasm32-unknown-unknown/release/counter_pack.wasm examples2/wasm/counter_pack.wasm + +test-counter-pack: build-counter-pack + cd examples2 && ODRA_BACKEND=casper cargo test --lib counter_pack_works -- --nocapture + +build-erc20-schema: + cd examples2 && ODRA_MODULE=Erc20 cargo run --bin build_schema diff --git a/lang/codegen/src/generator/common.rs b/lang/codegen/src/generator/common.rs index 51343018..e5e794b8 100644 --- a/lang/codegen/src/generator/common.rs +++ b/lang/codegen/src/generator/common.rs @@ -16,11 +16,11 @@ where match ret { syn::ReturnType::Default => quote! { #args - odra::call_contract::<()>(self.address, #entrypoint_name, &args, self.attached_value); + odra2::call_contract::<()>(self.address, #entrypoint_name, &args, self.attached_value); }, syn::ReturnType::Type(_, _) => quote! { #args - odra::call_contract(self.address, #entrypoint_name, &args, self.attached_value) + odra2::call_contract(self.address, #entrypoint_name, &args, self.attached_value) } } } @@ -38,21 +38,21 @@ pub(crate) fn build_ref(ref_ident: &Ident, struct_ident: &Ident) -> TokenStream #[derive(Clone)] #[doc = #ref_comment] pub struct #ref_ident { - address: odra::types::Address, - attached_value: Option, + address: odra2::types::Address, + attached_value: Option, } impl #ref_ident { - pub fn at(address: &odra::types::Address) -> Self { + pub fn at(address: &odra2::types::Address) -> Self { Self { address: *address, attached_value: None } } - pub fn address(&self) -> &odra::types::Address { + pub fn address(&self) -> &odra2::types::Address { &self.address } pub fn with_tokens(&self, amount: T) -> Self - where T: Into { + where T: Into { Self { address: self.address, attached_value: Some(amount.into()), @@ -66,7 +66,7 @@ fn parse_args(syn_args: T) -> TokenStream where T: IntoIterator { - let mut tokens = quote!(let mut args = odra::types::casper_types::RuntimeArgs::new();); + let mut tokens = quote!(let mut args = odra2::types::casper_types::RuntimeArgs::new();); tokens.append_all(syn_args.into_iter().map(|ty| { let pat = &*ty.pat; match *ty.ty { @@ -97,7 +97,7 @@ pub fn serialize_struct(prefix: &str, struct_ident: &Ident, fields: &[Ident]) -> let deserialize_fields = fields .iter() - .map(|ident| quote!(let (#ident, bytes) = odra::types::casper_types::bytesrepr::FromBytes::from_bytes(bytes)?;)) + .map(|ident| quote!(let (#ident, bytes) = odra2::types::casper_types::bytesrepr::FromBytes::from_bytes(bytes)?;)) .collect::(); let construct_struct = fields @@ -124,9 +124,9 @@ pub fn serialize_struct(prefix: &str, struct_ident: &Ident, fields: &[Ident]) -> .collect::(); quote! { - impl odra::types::casper_types::bytesrepr::FromBytes for #struct_ident { - fn from_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), odra::types::casper_types::bytesrepr::Error> { - let (_, bytes): (odra::prelude::string::String, _) = odra::types::casper_types::bytesrepr::FromBytes::from_bytes(bytes)?; + impl odra2::types::casper_types::bytesrepr::FromBytes for #struct_ident { + fn from_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), odra2::types::casper_types::bytesrepr::Error> { + let (_, bytes): (odra2::prelude::string::String, _) = odra2::types::casper_types::bytesrepr::FromBytes::from_bytes(bytes)?; #deserialize_fields let value = #struct_ident { #construct_struct @@ -135,9 +135,9 @@ pub fn serialize_struct(prefix: &str, struct_ident: &Ident, fields: &[Ident]) -> } } - impl odra::types::casper_types::bytesrepr::ToBytes for #struct_ident { - fn to_bytes(&self) -> Result, odra::types::casper_types::bytesrepr::Error> { - let mut vec = odra::prelude::vec::Vec::with_capacity(self.serialized_length()); + impl odra2::types::casper_types::bytesrepr::ToBytes for #struct_ident { + fn to_bytes(&self) -> Result, odra2::types::casper_types::bytesrepr::Error> { + let mut vec = odra2::prelude::vec::Vec::with_capacity(self.serialized_length()); vec.append(&mut #name_literal.to_bytes()?); #append_bytes Ok(vec) @@ -150,9 +150,9 @@ pub fn serialize_struct(prefix: &str, struct_ident: &Ident, fields: &[Ident]) -> } } - impl odra::types::CLTyped for #struct_ident { - fn cl_type() -> odra::types::CLType { - odra::types::CLType::Any + impl odra2::types::CLTyped for #struct_ident { + fn cl_type() -> odra2::types::CLType { + odra2::types::CLType::Any } } } @@ -162,23 +162,23 @@ pub fn serialize_enum(enum_ident: &Ident, variants: &[Variant]) -> TokenStream { let from_bytes_code = enum_from_bytes(enum_ident, variants); quote! { - impl odra::types::casper_types::bytesrepr::FromBytes for #enum_ident { + impl odra2::types::casper_types::bytesrepr::FromBytes for #enum_ident { #from_bytes_code } - impl odra::types::casper_types::bytesrepr::ToBytes for #enum_ident { + impl odra2::types::casper_types::bytesrepr::ToBytes for #enum_ident { fn serialized_length(&self) -> usize { (self.clone() as u32).serialized_length() } - fn to_bytes(&self) -> Result, odra::types::casper_types::bytesrepr::Error> { + fn to_bytes(&self) -> Result, odra2::types::casper_types::bytesrepr::Error> { (self.clone() as u32).to_bytes() } } - impl odra::types::CLTyped for #enum_ident { - fn cl_type() -> odra::types::CLType { - odra::types::CLType::U32 + impl odra2::types::CLTyped for #enum_ident { + fn cl_type() -> odra2::types::CLType { + odra2::types::CLType::U32 } } } @@ -196,11 +196,11 @@ fn enum_from_bytes(enum_ident: &Ident, variants: &[Variant]) -> TokenStream { .collect::>(); quote! { - fn from_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), odra::types::casper_types::bytesrepr::Error> { - let (variant, bytes): (u32, _) = odra::types::casper_types::bytesrepr::FromBytes::from_bytes(bytes)?; + fn from_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), odra2::types::casper_types::bytesrepr::Error> { + let (variant, bytes): (u32, _) = odra2::types::casper_types::bytesrepr::FromBytes::from_bytes(bytes)?; match variant { #append_bytes, - _ => core::result::Result::Err(odra::types::casper_types::bytesrepr::Error::Formatting), + _ => core::result::Result::Err(odra2::types::casper_types::bytesrepr::Error::Formatting), } } } diff --git a/lang/codegen/src/generator/event_item/mod.rs b/lang/codegen/src/generator/event_item/mod.rs index 5a988968..68e2ea3e 100644 --- a/lang/codegen/src/generator/event_item/mod.rs +++ b/lang/codegen/src/generator/event_item/mod.rs @@ -22,17 +22,17 @@ impl GenerateCode for EventItem<'_> { let event_def = to_event_def(self.event); quote! { - impl odra::types::event::OdraEvent for #struct_ident { - fn emit(self) { - odra::contract_env::emit_event(self); + impl odra2::event::OdraEvent for #struct_ident { + fn emit(self, env: &odra2::ContractEnv) { + env.emit_event(self); } - fn name() -> odra::prelude::string::String { - odra::prelude::string::String::from(stringify!(#struct_ident)) + fn name() -> odra2::prelude::string::String { + odra2::prelude::string::String::from(stringify!(#struct_ident)) } #[cfg(not(target_arch = "wasm32"))] - fn schema() -> odra::types::contract_def::Event { + fn schema() -> odra2::types::contract_def::Event { #event_def } } @@ -51,9 +51,9 @@ fn to_event_def(event: &IrEventItem) -> TokenStream { let ty = &field.ty; let is_slice = matches!(ty, syn::Type::Slice(syn::TypeSlice { .. })); quote! { - odra::types::contract_def::Argument { - ident: odra::prelude::string::String::from(stringify!(#field_ident)), - ty: <#ty as odra::types::CLTyped>::cl_type(), + odra2::types::contract_def::Argument { + ident: odra2::prelude::string::String::from(stringify!(#field_ident)), + ty: <#ty as odra2::types::CLTyped>::cl_type(), is_ref: false, is_slice: #is_slice }, @@ -61,9 +61,9 @@ fn to_event_def(event: &IrEventItem) -> TokenStream { }) .collect::(); quote! { - odra::types::contract_def::Event { - ident: odra::prelude::string::String::from(stringify!(#struct_ident)), - args: odra::prelude::vec![#fields] + odra2::types::contract_def::Event { + ident: odra2::prelude::string::String::from(stringify!(#struct_ident)), + args: odra2::prelude::vec![#fields] } } } diff --git a/lang/proc-macros/Cargo.toml b/lang/proc-macros/Cargo.toml index f9926c6f..f5ed1226 100644 --- a/lang/proc-macros/Cargo.toml +++ b/lang/proc-macros/Cargo.toml @@ -17,8 +17,8 @@ syn = { version = "1.0.89", features = ["full", "extra-traits"] } odra-ir = { path = "../ir", version = "0.7.0" } odra-codegen = { path = "../codegen", version = "0.7.0" } -[dev-dependencies] -odra = { path = "../../core", version = "0.7.0" } +# [dev-dependencies] +# odra = { path = "../../core", version = "0.7.0" } [lib] proc-macro = true diff --git a/mock-vm/backend/src/contract_env.rs b/mock-vm/backend/src/contract_env.rs index 805509bb..95532924 100644 --- a/mock-vm/backend/src/contract_env.rs +++ b/mock-vm/backend/src/contract_env.rs @@ -1,14 +1,15 @@ //! Exposes the public API to communicate with the host. use core::panic; +use odra_core::event::OdraEvent; use std::backtrace::{Backtrace, BacktraceStatus}; use std::collections::hash_map::DefaultHasher; use std::hash::{Hash, Hasher}; use odra_types::casper_types::bytesrepr::{Bytes, FromBytes, ToBytes}; use odra_types::casper_types::{CLTyped, U512}; -use odra_types::{event::OdraEvent, ExecutionError, OdraError}; use odra_types::{Address, BlockTime, PublicKey}; +use odra_types::{ExecutionError, OdraError}; use crate::{borrow_env, debug, native_token::NativeTokenMetadata}; diff --git a/mock-vm/backend/src/mock_vm.rs b/mock-vm/backend/src/mock_vm.rs index 505f2fe2..b55eee28 100644 --- a/mock-vm/backend/src/mock_vm.rs +++ b/mock-vm/backend/src/mock_vm.rs @@ -1,4 +1,5 @@ use anyhow::Result; +use odra_core::event::EventError; use odra_types::{ casper_types::{ account::AccountHash, @@ -7,7 +8,7 @@ use odra_types::{ }, Address, BlockTime, EventData, ExecutionError, PublicKey }; -use odra_types::{event::EventError, OdraError, VmError}; +use odra_types::{OdraError, VmError}; use std::collections::BTreeMap; use std::sync::{Arc, RwLock}; diff --git a/mock-vm/backend/src/test_env.rs b/mock-vm/backend/src/test_env.rs index f734ca74..54a5750b 100644 --- a/mock-vm/backend/src/test_env.rs +++ b/mock-vm/backend/src/test_env.rs @@ -1,6 +1,7 @@ //! Describes test environment API. Delegates methods to the underlying env implementation. //! //! Depending on the selected feature, the actual test env is dynamically loaded in the runtime or the Odra local MockVM is used. +use odra_core::event::{EventError, OdraEvent}; use odra_types::{ casper_types::{ bytesrepr::{Bytes, FromBytes, ToBytes}, @@ -8,10 +9,7 @@ use odra_types::{ }, Address, BlockTime, PublicKey }; -use odra_types::{ - event::{EventError, OdraEvent}, - OdraAddress, OdraError -}; +use odra_types::{OdraAddress, OdraError}; use std::{collections::BTreeMap, panic::AssertUnwindSafe}; use crate::{native_token::NativeTokenMetadata, EntrypointArgs, EntrypointCall}; diff --git a/modules/src/erc20.rs b/modules/src/erc20.rs index c9b44a44..0c8158f2 100644 --- a/modules/src/erc20.rs +++ b/modules/src/erc20.rs @@ -31,7 +31,6 @@ impl Erc20 { initial_supply: &Option ) { let caller = contract_env::caller(); - self.symbol.set(symbol); self.name.set(name); self.decimals.set(decimals); diff --git a/odra-casper/backend/src/casper_env.rs b/odra-casper/backend/src/casper_env.rs index 9eff2dbf..6f031a53 100644 --- a/odra-casper/backend/src/casper_env.rs +++ b/odra-casper/backend/src/casper_env.rs @@ -1,7 +1,6 @@ use alloc::vec::Vec; use odra_types::{ casper_types::{api_error, bytesrepr, CLValue}, - event::OdraEvent, Address }; @@ -14,6 +13,7 @@ use casper_contract::{ }; use odra_casper_shared::{consts, key_maker::KeyMaker}; +use odra_core::event::OdraEvent; use odra_types::{ casper_types::{ bytesrepr::{FromBytes, ToBytes}, diff --git a/odra-casper/backend/src/contract_env.rs b/odra-casper/backend/src/contract_env.rs index cb16257c..f6c87aa3 100644 --- a/odra-casper/backend/src/contract_env.rs +++ b/odra-casper/backend/src/contract_env.rs @@ -8,9 +8,10 @@ use casper_contract::{ }; use core::ops::Deref; use odra_casper_shared::native_token::NativeTokenMetadata; +use odra_core::event::OdraEvent; use odra_types::casper_types::bytesrepr::{Bytes, FromBytes, ToBytes}; use odra_types::casper_types::{crypto, CLTyped, RuntimeArgs, U512}; -use odra_types::{event::OdraEvent, ExecutionError}; +use odra_types::ExecutionError; use odra_types::{Address, BlockTime}; use crate::{casper_env, utils::get_or_create_main_purse}; diff --git a/odra-casper/livenet/resources/proxy_caller.wasm b/odra-casper/livenet/resources/proxy_caller.wasm index 3b099b7b629e133571a03ea962009e52330c73c6..b20b9aef050ea79c5da41fdd24b847f32683707b 100755 GIT binary patch delta 5403 zcmai&e{38_6~}jG_Rf2^d&$PJ)3co)bC;yKi{kbYrM^qzB;K^CBZRSVH{z2LTci5=AP4(g>s|e}q5iKk$7s zJ9o3G5oc>}=JRIey?O7=kG*$p@vr=hpZmzD=gpTh{qg2`mU;6mW7(_er&v}IRS`zz zFo;;-vrvdA5J3=t)QCiaMC&4AK`6rYA)`VRqFF?rKnM|sA+QMPKg2)&%0OrzN32{E zx?Bm-JzyJxaF~@gdivv+)RVl+B6XJU;$OR|UgLjYO1&Vy%)X(9{hRYlWZReU@sjIo zvFYv5W$7hB8oxpq#S1LO^DQrJ3$H7PoYUlw4q0L;xPW*@(8{ITJPTJTp70P7l4#&Zz^z7uc%05!}emlRfcS zdU~NOPkLVbf05I^#dyE-eLXi|Qt?ZA_%mksYYdK&o@DV~2=E!n)Sb$suxwxT$x4;l z{}O_g?{8mV>_wF<%&TX2K9AD7I~Pj7rY`Ea=+wfWhXv30>A?DURV+4H?qddrB}+@I zOI|V>f0w#QmbB93C5^)Pa*ldwa!DgUzGM*E_0sswT)wQ1)^=pq`0*MIuxbLql4VX2 zX>tUuDJX1AvbI>}O}{HvQEu_qc%y_Kzw&hG#1T$yAUz>VH8e;N-{gB*^Sb!kRvAX^ zrN?w07#$s5%Sbn=Y!!sEP^%YeabDy3X!Nzrjb1ZY{03=F{=+_#u-RG}kY@LWHmMfh zBq5)n(6o5A%98c7+5;Cv8vi&qcL^5B6deIkXqJ#P2R*WtBy}sP8$W3;X-RA)aoQ~B zX>a-<$<1i+?U0p7mbJwNczKEJhZ~H7`1jiW++ZV#f0&zwRYfbNKL#zc1bPSR2*5;& zps|bqX49Vp1(dX2U{S_GGd==<0{N+B*Q*JPb^~!)qV|zKs+vT?EvCIpTmv} zIhBVGp3SHFJ2TZuZwbjM0P_j3Uz@o9_e zC_Yj1P5jK*F3=Npf%Xi6%=Xbfbem{ryFf?9g4>9s@aB$dop*T}U$e_YFIX4?hye7R zLJL_3fcPK)nO@=ADr5mt*ZvUXtUS2&}nH=oskj^!{yvqL8h5lp@+GW?HRM2V6pbfQgXE$yOQ)ds`)!4BVx+YV2** zT;lkP`e1Lfi>c_b{vwB0joXmrF^Olq|FDog5*>+`@&5b~#K=I@yw1jOGnyPW@zxna=wD(Q;9 zLr>f_*rd0tezCEaMSX-bq%DH3HzRea)#dZ1gjJ2(0qsA~f)m~tg)>LrjSMpeX@+SU zW{lD7wr02r1Zj(VQ#hCDK89vlVH5E%F+gJd8G6u{v%WVxliSp;pBtVzCy4b^$unt3 ztYddbudtg|>;>?i`qIX6_Kf=0#%*J(A~iRfP+yDmwP-3rsTs)mhV^x(TMP zx2bLdsq1^G-qV5A^`6=@HlZFJ-?k~?Hm&tV_@d7JGiv?BlDaqYrW&32BDo%FOy6e&@OQ&4=2+^%H`$t^7 z-!|S4s?K#sv)nTnexH@?b9?NybgxT0mUdkFu%!>X^dUGpmu@i>25xcjX4`l(sJe8(_29Ubjk`U@EFE)c-O{>CAF%WRmu|9j zlS@Ypg@L0k97- zzIe#BuWChAw?oC!ic14a1D6h2I`mn)D`D-GEiJpWWGL(_x!AXjeNc7%(CGfbBIZ`c z-5$(RW~k)o`0qo__m$}Uz(Ep!lQ#8cnGTS$Wb_Wq6E?-4!UkKO;vYsWnBsHTLb>!G zuX#9nn3$1%f`ik?<-1W*3)9WJl4bJ>58d=5P$Pm(B{I~$7?)&ZTJW^NWQbXu#w_sb0FF}WFOvSDgzFO` zppkLC!vvuxkV81P7iEPe{<)NZjaURo`cmj?S zADw9W@SPTQjHt7QsAWX0HAK>gbc@#z1x6ICA@Ym}zYn?A!|`z0 z%GM(%`O1%G?v&ZzR<6vl5%%|$<&UW{yR|ZX@>$OAsUMtrO#SBLud2%nx7D>%|DxZ& UR)SAl+%GuibF+O+oHzwRQb5TAAh$rE$D&OD zQN%;eR)Ew>ZR7S(5E2v@RkRP4NV;kTtwJO5Vk8h00V<^uK_YFDs8pmrq*4+5znL?; zv&jRFwVwHXGxJ^Mn{Q_GonMK!UJ)ywS|`$!E%B76OT|?GuqDPWCin8R82IIiEC*G| zeUArHmVN2_ez_Rb${=M)1NzFu{6Gf%fo_xo3@g)*FQp8FAmF|X=r_PG{uO~RJ}mQM zP3m?vz;vGv_`x7A3^D!V6|$3}$%nJ^VyAfLV)k?KC;mwGsQeaxG#m6@Xg@V}7v~RW zS57ZwTkd%Xt#@`TXYJxkJj?DaeS^P}y;XWXJ>P1zSoj!yvWhBBFc$t_Oc}ms>1e7e1wJ=W!#6 zpU|ih{v=1!NxZ6&7d~l_;_-A6zLCpUv)S5M`m|W6F$i8`3D!kpRZs)&M1n%V6(5(Y zB9cvcO2OI-qV8eTni(!;1Py_3QF%3t)$2U1)%Jy}q)E)t3a94zom$Ttf_zEKYvBv6 zR!ReCNJFggxc{gg^R`IBU*uL^!+1q@0iqFkNTK}yNqNci3<6q83{JcNWvs4 z_DHmsw3?qprPPYZ$hYC~HF5$mFbcvykQvrsBMD#3OE-uOc%{^c$p{;?%JDWw;Ilan;Lp5Qum3){NI7Al}Jc!*u|NW1CZx=z5pe zcoREq9M4^S)wnvruJMH$?R)M3kAEPN&Q9cR^7t{4j5B?5%t&p)Lk@hFlWz}nNs73$ z_ersKpRCeu8a*8i#I>77RVU)awG!fvo5Z!7#5>2-T3(9RL7Q}TdOwQZ!CgIaO<0)WH;G;rE9Z8(fJBNuRPVl4dK_H0}{>XQgt~whK zr_+=Wrual2%99(C^QN<7)i8pLim8BqXw!?}pcpf{X~r~;Y?D5XkbN?ue2c5=nj zrZ@)zUYWu3Oo{Mi+9sI%Uw$^8I9e;JK-r*`J0+mfsSZ_jLl9_boqt%B>gn^kEap`p z{HaL;T^C1=)=IcHXv_R2il1&Zn&L~ig!JCvMlf!Y&}<5j@lhka8$iZCjoxK&XO3e*zSLME4bxHj2jlI5hdQ>CzL;WMPc>T&M>KMI~58NEOI;>I_! zzm5$0|0>Et%Fk7^#q}S`Hf%hWBHpT?yzsJ)vYcSJfb~Tr8{(r;k*=Y-ppuOK@p>sn zZ=ThCb652V&ICjh5>-^C@KSdbK{FmiwZyF$&nEb2aa|M$wuob~8WIrAnrG=V|MO{?pMcuE;?7~Pr#Wvs~(Y};bkWZOelq@226)~qgdKn=}Vjg~0PJTz{=gvWrP?+Y;IItP zW;iErBlVrbS*O0zHH~3vJWhR6&vbt;^-ayw{e9Foby4>(Bj+fyIy<+*ld<-LoBqtR z508E`dwG4B{dV*>?ayxc(J;T%{$TG_Fa234J!F5;z9z4*09lYPA`z+n((rHVlUD~v?>C!1nr@Ayd zIo+3beY(vKxve{7g57HAtuDRA(py}**V4T%owRh)rF#tBEaE*b-fai(27P?rI>)}7 zt>|WV!cCUm4-}= z7zzV7xOlxCydE?Q54Z*nTiLKXW607WmkwGw=+Xg82V5Fj8oIRKP#DY^n zV_?jH6d<~}(0Ru~4af(mT4laUbXAZ7g>K2 zJ`;IIYk2dBm@XMFysDoyq9GRR60{)>W73<4c5#^v%#7f3*UXj_i>tDd-EZceDwr?m zP(&}{dR1Sdu*%E7RCuatMu?=&6}>CY!DM{*uf%vHEt9fREY8xN;0*(FwKve?t5TwZ zWjwXvZsuV7z1h3O?Nf|`Lf3SRXRSwnpbu|TieQ>d2_G#Agcj0^ z74r5c=HD&yH`|Ag@nQat_L0wIMgGh7>z_L#vfW$1+y2|>W1PR2?b&)bJAe9Od&@mL PZkK27ySshl$yxb7m*aJj diff --git a/odra-casper/livenet/src/contract_env.rs b/odra-casper/livenet/src/contract_env.rs index 4c6f1592..c50fa03d 100644 --- a/odra-casper/livenet/src/contract_env.rs +++ b/odra-casper/livenet/src/contract_env.rs @@ -5,12 +5,12 @@ use crate::casper_client::LivenetKeyMaker; use odra_casper_shared::key_maker::KeyMaker; use odra_casper_shared::native_token::NativeTokenMetadata; +use odra_core::event::OdraEvent; use odra_types::{ casper_types::{ bytesrepr::{Bytes, FromBytes, ToBytes}, U512 }, - event::OdraEvent, Address, BlockTime, ExecutionError, PublicKey }; diff --git a/odra-casper/proxy-caller/src/lib.rs b/odra-casper/proxy-caller/src/lib.rs index c57a55f8..480d91ee 100644 --- a/odra-casper/proxy-caller/src/lib.rs +++ b/odra-casper/proxy-caller/src/lib.rs @@ -41,7 +41,7 @@ pub struct ProxyCall { pub contract_package_hash: ContractPackageHash, pub entry_point_name: String, pub runtime_args: RuntimeArgs, - pub attached_value: Option + pub attached_value: U512 } impl ProxyCall { @@ -54,9 +54,9 @@ impl ProxyCall { if !bytes.is_empty() { revert(ApiError::Deserialize); }; - let attached_value: Option = get_named_arg(ATTACHED_VALUE_ARG); + let attached_value: U512 = get_named_arg(ATTACHED_VALUE_ARG); - if let Some(attached_value) = attached_value { + if attached_value > U512::zero() { let cargo_purse = get_cargo_purse(); top_up_cargo_purse(cargo_purse, attached_value); runtime_args diff --git a/odra-casper/shared/src/consts.rs b/odra-casper/shared/src/consts.rs index af33a127..ffca2d8d 100644 --- a/odra-casper/shared/src/consts.rs +++ b/odra-casper/shared/src/consts.rs @@ -57,3 +57,6 @@ pub const STATE_KEY: &str = "state"; /// Constructor group name. pub const CONSTRUCTOR_GROUP_NAME: &str = "constructor_group"; + +/// Constructor name +pub const CONSTRUCTOR_NAME: &str = "init"; diff --git a/odra-casper/test-env/resources/proxy_caller_with_return.wasm b/odra-casper/test-env/resources/proxy_caller_with_return.wasm index cc983653aa84b0d1e5c0224b535f154b43d7c185..c66469940b0be75019946193d764e3639a5f6c49 100755 GIT binary patch delta 5121 zcmZ`-ZEPGz8Qz)MJKtx#N$vXLug-0qao!Qx& zZ4OzoXP#%?dEa^Fop)ya>!0&~{E9#KA66)qTwX4g%Q5Hs7>f_^2iS1@L*Cr>lMDQQ zR&u1{{jA&V#{cEJj|YX&`zAU7gH0CV_eCdc3a2dyc~T+2vyewv2os-y{6!y@PE)MK z_lWcCk=S+j#w(?OJ=*(n>2)@Ho+d9#5p1z=2JNmhj9!DC_SUK6l($nn4^!Lg_U4M$ zu-<;xW9)0a#T;itUs1$*yq5pyM6BD|O!8T$%RIxsoS%oumHbEgCbt(}8NbTpRBG|1 z%3YuM!w$hCey$K*k4d{uuqDp;Oh+jh))_q5-2y!7eVb@NjWBpzL1ph-3DgOLO$E8$ zQyRj!PUyXn@SE}R>P+-3KUSp)paSMaW0NZ(Ikff#sm&y7icRjjZLx)Rga3%va~Swj zM_Gs?oW_7%lesDe1lX3~S><_~_k?kVSv%pf+CJ^0rFx$v-{i89A!Vjl%k;dY$CKIU z&nDM?^<>_w=GK^$VQ*1!dF2#x^YX{zMRjjWmy{ek=UQ9yU z(3_IZ5fA{seRl*P#C~K1U^VF?X!FSH2CGD@tmlW=XwZ%~%#qTYpkyFiW^p9~;k7KT zBp}?&;z|I*3n`8`BIHl9RZuor>TmQ?ab}Q+0${QL7CgnnbSIK!vfvv$Y&yv%5U3(p ze;26SKhy`|+qsXoBZxo>P+%SMPl?(|BUE_H;H@mK6r=H$VlNWHAD4eZXllJjM~ zJ9Pe+os+abP4OI>w^d!ee|i}*hbXaaK<$bFE@3BybK;otd6|da z^X7P{1B*ZaVgpi9=;)~pAiTc;kvicjV~DUeK%doQqO2&FInEB{Wxq_Q9T-?9)E*2h z6SND_;5wo9!LAcHu+~oKRHYPAqUicV6;q`u;Z~~ZN{VO+S2FjBmacAMSJI0RB@hNt>$DQtyRUejFr*~ zSC(nt>^S-N7qrtaWP`gRhe!W19R_E`k^?@L|2Tcr%G9|{rvH@nwk)w|eVX0Qm zfI8>sTZG1?iRS2}BrHuzCsS*Wn-|;_6`_7Zm=H;jwKl(-FVQoj<2&kU(%^>?J@dX! z(yEjb`oY%w!pM8L`)H1p;>p^s_}-~IB0S*u8d(W?1qN-LIC|kZ>e7HR8gAiQM)Q!l zN;KM_;~|2%EQ$v~5oj}u;=q)E*0U&`aC%Yh%)&S+6`N|tQ|A=%`>PB* zcDhYhJFYhsDf&3#CjAwUTd0tfc-5DoEZY5#q3V@v0NY!(`^bTc=_3kpqAdymtMdvg zQ!r&H?~;5u>Gl=J#mptLHU8&h3u{BDx_lr?2NIyK6F~Kw)YqG@`g7FpsLNFKJMkS8 z6L|ceoF0j9oi4@y9-E9;r@k2P9($esvJ3H^vETRZpLmV+9-n?;C;M^lROiobwCM>J zv2#W9Ain6F@p6=EyAgcY3hhYHL}$EEJ|jn!xmM`Qt!1WFUoQ^T3jJZ z9qLn=_&O<$?~}IAbkuDdT`=f^jov`$G_C#y8=g0V=Yht5JTf|)D1+$_8t$NNF=x;@ zvZybPezW!ZHsLN-DaazgH~;{V$cfBe(SOwPswm4+oEXDB3Z02+xiR{Zs@b7V&Gil z4Vt&noI!Io>KfDq8o#qVI^RDn+;H5sU^p zv5Iq81-|}q(Mk6V`5y|nD&7T}8P7K?5Pk}B2)F7PS)_&E8`9E?`X2(W%*6+f9ghCW zI6ucCz7#}s^K^XNj`T;|2I0i*6TS=W6xGsHYXH^IRAT@oH6;g7HBfiKS#1F4X)|vC zRnSyn0Oe>3|Ld`%hwzZBclOvx-uu_;-}3Cu-k(0eM%dfEznnfa#4hw?_-W3r#Orsj p#Ls`~`|$&3FU22{=i`~PcgDBWAL_k*&v)j<=T6_*JOA{e_#Z50u1Ej? delta 4676 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 N-uJ=Qo4>Un{tFJEOkV&1 diff --git a/odra-casper/test-env/src/dummy_contract_env.rs b/odra-casper/test-env/src/dummy_contract_env.rs index 2eed5efe..976d53e3 100644 --- a/odra-casper/test-env/src/dummy_contract_env.rs +++ b/odra-casper/test-env/src/dummy_contract_env.rs @@ -1,7 +1,8 @@ use odra_casper_shared::native_token::NativeTokenMetadata; +use odra_core::event::OdraEvent; use odra_types::casper_types::bytesrepr::{Bytes, FromBytes, ToBytes}; use odra_types::casper_types::U512; -use odra_types::{event::OdraEvent, ExecutionError}; +use odra_types::ExecutionError; use odra_types::{Address, BlockTime, PublicKey}; pub fn self_address() -> Address { diff --git a/odra-casper/test-env/src/env.rs b/odra-casper/test-env/src/env.rs index 88ee04b7..57400070 100644 --- a/odra-casper/test-env/src/env.rs +++ b/odra-casper/test-env/src/env.rs @@ -21,6 +21,7 @@ use odra_casper_shared::consts::{ self, AMOUNT_ARG, ARGS_ARG, ATTACHED_VALUE_ARG, CONTRACT_PACKAGE_HASH_ARG, ENTRY_POINT_ARG, RESULT_KEY }; +use odra_core::event::{EventError, OdraEvent}; use odra_types::{ casper_types::{ account::{Account, AccountHash}, @@ -28,7 +29,6 @@ use odra_types::{ runtime_args, ApiError, CLTyped, Contract, ContractHash, ContractPackageHash, Key, Motes, PublicKey, RuntimeArgs, SecretKey, StoredValue, URef, U512 }, - event::{EventError, OdraEvent}, Address, BlockTime, ExecutionError, OdraError, VmError }; diff --git a/odra-casper/test-env/src/test_env.rs b/odra-casper/test-env/src/test_env.rs index 9abb488a..90502722 100644 --- a/odra-casper/test-env/src/test_env.rs +++ b/odra-casper/test-env/src/test_env.rs @@ -5,13 +5,13 @@ use std::panic::AssertUnwindSafe; use crate::env::ENV; use odra_casper_shared::{consts, native_token::NativeTokenMetadata}; +use odra_core::event::{EventError, OdraEvent}; use odra_types::{ casper_types::{ account::{AccountHash, ACCOUNT_HASH_LENGTH}, bytesrepr::{Bytes, FromBytes, ToBytes}, CLTyped, RuntimeArgs, U512 }, - event::{EventError, OdraEvent}, Address, OdraError, PublicKey }; diff --git a/odra-casper/test-vm/Cargo.toml b/odra-casper/test-vm/Cargo.toml new file mode 100644 index 00000000..0cb560e0 --- /dev/null +++ b/odra-casper/test-vm/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "odra-casper-test-vm" +edition = "2021" +description = "Odra test environment for the Casper Blockchain." +keywords = ["wasm", "webassembly", "blockchain"] +categories = ["wasm", "smart contracts"] +version = { workspace = true } +authors = { workspace = true } +license = { workspace = true } +homepage = { workspace = true } +repository = { workspace = true } + +[dependencies] +casper-engine-test-support = { version = "5.0.0", features = ["test-support"] } +casper-execution-engine = { workspace = true } +odra-core = { path = "../../odra-core" } +odra-types = { path = "../../types" } +odra-casper-shared = { path = "../shared" } +casper-event-standard = "0.4.0" +odra-utils = { path = "../../utils" } + +[target.'cfg(target_arch = "wasm32")'.dependencies] +casper-contract = { workspace = true } +ink_allocator = { version = "4.2.1", default-features = false } +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +casper-contract = { version = "3.0.0", default-features = false, features = ["test-support"] } diff --git a/odra-casper/test-vm/resources/chainspec.toml b/odra-casper/test-vm/resources/chainspec.toml new file mode 100644 index 00000000..311639fd --- /dev/null +++ b/odra-casper/test-vm/resources/chainspec.toml @@ -0,0 +1,261 @@ +[protocol] +# Protocol version. +version = '1.5.1' +# Whether we need to clear latest blocks back to the switch block just before the activation point or not. +hard_reset = true +# This protocol version becomes active at this point. +# +# If it is a timestamp string, it represents the timestamp for the genesis block. This is the beginning of era 0. By +# this time, a sufficient majority (> 50% + F/2 — see finality_threshold_fraction below) of validator nodes must be up +# and running to start the blockchain. This timestamp is also used in seeding the pseudo-random number generator used +# in contract-runtime for computing genesis post-state hash. +# +# If it is an integer, it represents an era ID, meaning the protocol version becomes active at the start of this era. +activation_point = 9100 + +[network] +# Human readable name for convenience; the genesis_hash is the true identifier. The name influences the genesis hash by +# contributing to the seeding of the pseudo-random number generator used in contract-runtime for computing genesis +# post-state hash. +name = 'casper' +# The maximum size of an acceptable networking message in bytes. Any message larger than this will +# be rejected at the networking level. +maximum_net_message_size = 25_165_824 + +[core] +# Era duration. +era_duration = '120min' +# Minimum number of blocks per era. An era will take longer than `era_duration` if that is necessary to reach the +# minimum height. +minimum_era_height = 20 +# Minimum difference between a block's and its child's timestamp. +minimum_block_time = '32768ms' +# Number of slots available in validator auction. +validator_slots = 100 +# A number between 0 and 1 representing the fault tolerance threshold as a fraction, used by the internal finalizer. +# It is the fraction of validators that would need to equivocate to make two honest nodes see two conflicting blocks as +# finalized: A higher value F makes it safer to rely on finalized blocks. It also makes it more difficult to finalize +# blocks, however, and requires strictly more than (F + 1)/2 validators to be working correctly. +finality_threshold_fraction = [1, 3] +# Protocol version from which nodes are required to hold strict finality signatures. +start_protocol_version_with_strict_finality_signatures_required = '1.5.0' +# Which finality is required for legacy blocks. Options are 'Strict', 'Weak' and 'Any'. +# Used to determine finality sufficiency for new joiners syncing blocks created +# in a protocol version before +# `start_protocol_version_with_strict_finality_signatures_required`. +legacy_required_finality = 'Strict' +# Number of eras before an auction actually defines the set of validators. If you bond with a sufficient bid in era N, +# you will be a validator in era N + auction_delay + 1. +auction_delay = 1 +# The period after genesis during which a genesis validator's bid is locked. +locked_funds_period = '90days' +# The period in which genesis validator's bid is released over time after it's unlocked. +vesting_schedule_period = '13 weeks' +# Default number of eras that need to pass to be able to withdraw unbonded funds. +unbonding_delay = 7 +# Round seigniorage rate represented as a fraction of the total supply. +# +# Annual issuance: 8% +# Minimum block time: 2^15 milliseconds +# Ticks per year: 31536000000 +# +# (1+0.08)^((2^15)/31536000000)-1 is expressed as a fractional number below +# Python: +# from fractions import Fraction +# Fraction((1 + 0.08)**((2**15)/31536000000) - 1).limit_denominator(1000000000) +round_seigniorage_rate = [7, 87535408] +# Maximum number of associated keys for a single account. +max_associated_keys = 100 +# Maximum height of contract runtime call stack. +max_runtime_call_stack_height = 12 +# Minimum allowed delegation amount in motes +minimum_delegation_amount = 500_000_000_000 +# Global state prune batch size (0 = this feature is off) +prune_batch_size = 0 +# Enables strict arguments checking when calling a contract; i.e. that all non-optional args are provided and of the correct `CLType`. +strict_argument_checking = false +# Number of simultaneous peer requests. +simultaneous_peer_requests = 5 +# The consensus protocol to use. Options are "Zug" and "Highway". +consensus_protocol = 'Highway' +# The maximum amount of delegators per validator. if the value is 0, there is no maximum capacity. +max_delegators_per_validator = 1200 + +[highway] +# Highway dynamically chooses its round length, between minimum_block_time and maximum_round_length. +maximum_round_length = '132seconds' +# The factor by which rewards for a round are multiplied if the greatest summit has ≀50% quorum, i.e. no finality. +# Expressed as a fraction (1/5 by default). +reduced_reward_multiplier = [1, 5] + +[deploys] +# The maximum number of Motes allowed to be spent during payment. 0 means unlimited. +max_payment_cost = '0' +# The duration after the deploy timestamp that it can be included in a block. +max_ttl = '1day' +# The maximum number of other deploys a deploy can depend on (require to have been executed before it can execute). +max_dependencies = 10 +# Maximum block size in bytes including deploys contained by the block. 0 means unlimited. +max_block_size = 10_485_760 +# Maximum deploy size in bytes. Size is of the deploy when serialized via ToBytes. +max_deploy_size = 1_048_576 +# The maximum number of non-transfer deploys permitted in a single block. +block_max_deploy_count = 50 +# The maximum number of wasm-less transfer deploys permitted in a single block. +block_max_transfer_count = 1250 +# The maximum number of approvals permitted in a single block. +block_max_approval_count = 2600 +# The upper limit of total gas of all deploys in a block. +block_gas_limit = 10_000_000_000_000 +# The limit of length of serialized payment code arguments. +payment_args_max_length = 1024 +# The limit of length of serialized session code arguments. +session_args_max_length = 1024 +# The minimum amount in motes for a valid native transfer. +native_transfer_minimum_motes = 2_500_000_000 + +[wasm] +# Amount of free memory (in 64kB pages) each contract can use for stack. +max_memory = 64 +# Max stack height (native WebAssembly stack limiter). +max_stack_height = 500 + +[wasm.storage_costs] +# Gas charged per byte stored in the global state. +gas_per_byte = 630_000 + +[wasm.opcode_costs] +# Bit operations multiplier. +bit = 300 +# Arithmetic add operations multiplier. +add = 210 +# Mul operations multiplier. +mul = 240 +# Div operations multiplier. +div = 320 +# Memory load operation multiplier. +load = 2_500 +# Memory store operation multiplier. +store = 4_700 +# Const store operation multiplier. +const = 110 +# Local operations multiplier. +local = 390 +# Global operations multiplier. +global = 390 +# Integer operations multiplier. +integer_comparison = 250 +# Conversion operations multiplier. +conversion = 420 +# Unreachable operation multiplier. +unreachable = 270 +# Nop operation multiplier. +nop = 200 +# Get current memory operation multiplier. +current_memory = 290 +# Grow memory cost, per page (64kb). +grow_memory = 240_000 + +# Control flow operations multiplier. +[wasm.opcode_costs.control_flow] +block = 440 +loop = 440 +if = 440 +else = 440 +end = 440 +br = 440_000 +br_if = 440_000 +return = 440 +select = 440 +call = 140_000 +call_indirect = 140_000 +drop = 440 + +[wasm.opcode_costs.control_flow.br_table] +# Fixed cost per `br_table` opcode +cost = 440_000 +# Size of target labels in the `br_table` opcode will be multiplied by `size_multiplier` +size_multiplier = 100 + +# Host function declarations are located in smart_contracts/contract/src/ext_ffi.rs +[wasm.host_function_costs] +add = { cost = 5_800, arguments = [0, 0, 0, 0] } +add_associated_key = { cost = 9_000, arguments = [0, 0, 0] } +add_contract_version = { cost = 200, arguments = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0] } +blake2b = { cost = 200, arguments = [0, 0, 0, 0] } +call_contract = { cost = 4_500, arguments = [0, 0, 0, 0, 0, 420, 0] } +call_versioned_contract = { cost = 4_500, arguments = [0, 0, 0, 0, 0, 0, 0, 420, 0] } +create_contract_package_at_hash = { cost = 200, arguments = [0, 0] } +create_contract_user_group = { cost = 200, arguments = [0, 0, 0, 0, 0, 0, 0, 0] } +create_purse = { cost = 2_500_000_000, arguments = [0, 0] } +disable_contract_version = { cost = 200, arguments = [0, 0, 0, 0] } +get_balance = { cost = 3_800, arguments = [0, 0, 0] } +get_blocktime = { cost = 330, arguments = [0] } +get_caller = { cost = 380, arguments = [0] } +get_key = { cost = 2_000, arguments = [0, 440, 0, 0, 0] } +get_main_purse = { cost = 1_300, arguments = [0] } +get_named_arg = { cost = 200, arguments = [0, 0, 0, 0] } +get_named_arg_size = { cost = 200, arguments = [0, 0, 0] } +get_phase = { cost = 710, arguments = [0] } +get_system_contract = { cost = 1_100, arguments = [0, 0, 0] } +has_key = { cost = 1_500, arguments = [0, 840] } +is_valid_uref = { cost = 760, arguments = [0, 0] } +load_named_keys = { cost = 42_000, arguments = [0, 0] } +new_uref = { cost = 17_000, arguments = [0, 0, 590] } +random_bytes = { cost = 200, arguments = [0, 0] } +print = { cost = 20_000, arguments = [0, 4_600] } +provision_contract_user_group_uref = { cost = 200, arguments = [0, 0, 0, 0, 0] } +put_key = { cost = 38_000, arguments = [0, 1_100, 0, 0] } +read_host_buffer = { cost = 3_500, arguments = [0, 310, 0] } +read_value = { cost = 6_000, arguments = [0, 0, 0] } +read_value_local = { cost = 5_500, arguments = [0, 590, 0] } +remove_associated_key = { cost = 4_200, arguments = [0, 0] } +remove_contract_user_group = { cost = 200, arguments = [0, 0, 0, 0] } +remove_contract_user_group_urefs = { cost = 200, arguments = [0, 0, 0, 0, 0, 0] } +remove_key = { cost = 61_000, arguments = [0, 3_200] } +ret = { cost = 23_000, arguments = [0, 420_000] } +revert = { cost = 500, arguments = [0] } +set_action_threshold = { cost = 74_000, arguments = [0, 0] } +transfer_from_purse_to_account = { cost = 2_500_000_000, arguments = [0, 0, 0, 0, 0, 0, 0, 0, 0] } +transfer_from_purse_to_purse = { cost = 82_000, arguments = [0, 0, 0, 0, 0, 0, 0, 0] } +transfer_to_account = { cost = 2_500_000_000, arguments = [0, 0, 0, 0, 0, 0, 0] } +update_associated_key = { cost = 4_200, arguments = [0, 0, 0] } +write = { cost = 14_000, arguments = [0, 0, 0, 980] } +write_local = { cost = 9_500, arguments = [0, 1_800, 0, 520] } + +[system_costs] +wasmless_transfer_cost = 100_000_000 + +[system_costs.auction_costs] +get_era_validators = 10_000 +read_seigniorage_recipients = 10_000 +add_bid = 2_500_000_000 +withdraw_bid = 2_500_000_000 +delegate = 2_500_000_000 +undelegate = 2_500_000_000 +run_auction = 10_000 +slash = 10_000 +distribute = 10_000 +withdraw_delegator_reward = 10_000 +withdraw_validator_reward = 10_000 +read_era_id = 10_000 +activate_bid = 10_000 +redelegate = 2_500_000_000 + +[system_costs.mint_costs] +mint = 2_500_000_000 +reduce_total_supply = 10_000 +create = 2_500_000_000 +balance = 10_000 +transfer = 10_000 +read_base_round_reward = 10_000 +mint_into_existing_purse = 2_500_000_000 + +[system_costs.handle_payment_costs] +get_payment_purse = 10_000 +set_refund_purse = 10_000 +get_refund_purse = 10_000 +finalize_payment = 10_000 + +[system_costs.standard_payment_costs] +pay = 10_000 diff --git a/odra-casper/test-vm/resources/proxy_caller_with_return.wasm b/odra-casper/test-vm/resources/proxy_caller_with_return.wasm new file mode 100755 index 0000000000000000000000000000000000000000..c66469940b0be75019946193d764e3639a5f6c49 GIT binary patch literal 43377 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 Sf0K*;tGOZVGSk0WcmEqkp;$El literal 0 HcmV?d00001 diff --git a/odra-casper/test-vm/src/casper_host.rs b/odra-casper/test-vm/src/casper_host.rs new file mode 100644 index 00000000..a215a86a --- /dev/null +++ b/odra-casper/test-vm/src/casper_host.rs @@ -0,0 +1,83 @@ +use odra_core::prelude::{collections::*, *}; +use std::cell::RefCell; +use std::env; +use std::path::PathBuf; + +use casper_engine_test_support::{ + DeployItemBuilder, ExecuteRequestBuilder, InMemoryWasmTestBuilder, ARG_AMOUNT, + DEFAULT_ACCOUNT_INITIAL_BALANCE, DEFAULT_CHAINSPEC_REGISTRY, DEFAULT_GENESIS_CONFIG, + DEFAULT_GENESIS_CONFIG_HASH, DEFAULT_PAYMENT +}; +use std::rc::Rc; + +use crate::CasperVm; +use casper_execution_engine::core::engine_state::{GenesisAccount, RunGenesisRequest}; +use odra_casper_shared::consts; +use odra_casper_shared::consts::*; +use odra_core::entry_point_callback::EntryPointsCaller; +use odra_core::event::EventError; +use odra_core::{CallDef, ContractEnv, HostContext, HostEnv}; +use odra_types::casper_types::account::AccountHash; +use odra_types::casper_types::bytesrepr::{Bytes, ToBytes}; +use odra_types::casper_types::{ + runtime_args, BlockTime, ContractPackageHash, Key, Motes, SecretKey +}; +use odra_types::{Address, PublicKey, U512}; +use odra_types::{EventData, RuntimeArgs}; + +pub struct CasperHost { + pub vm: Rc> +} + +impl HostContext for CasperHost { + fn set_caller(&self, caller: Address) { + self.vm.borrow_mut().set_caller(caller) + } + + fn get_account(&self, index: usize) -> Address { + self.vm.borrow().get_account(index) + } + + fn balance_of(&self, address: &Address) -> U512 { + self.vm.borrow().balance_of(address) + } + + fn advance_block_time(&self, time_diff: u64) { + self.vm.borrow_mut().advance_block_time(time_diff) + } + + fn get_event(&self, contract_address: &Address, index: i32) -> Result { + self.vm.borrow().get_event(contract_address, index) + } + + fn call_contract(&self, address: &Address, call_def: CallDef, use_proxy: bool) -> Bytes { + self.vm + .borrow_mut() + .call_contract(address, call_def, use_proxy) + } + + fn new_contract( + &self, + name: &str, + init_args: Option, + entry_points_caller: Option + ) -> Address { + self.vm + .borrow_mut() + .new_contract(name, init_args, entry_points_caller) + } + + fn contract_env(&self) -> ContractEnv { + unreachable!() + } + + fn print_gas_report(&self) { + self.vm.borrow().print_gas_report() + } +} + +impl CasperHost { + pub fn new(vm: Rc>) -> Rc> { + Rc::new(RefCell::new(Self { vm })) + } +} diff --git a/odra-casper/test-vm/src/lib.rs b/odra-casper/test-vm/src/lib.rs new file mode 100644 index 00000000..d2297b79 --- /dev/null +++ b/odra-casper/test-vm/src/lib.rs @@ -0,0 +1,9 @@ +#![allow(dead_code)] +#![allow(unused_imports)] +#![allow(unused_variables)] + +mod casper_host; +mod vm; + +pub use casper_host::CasperHost; +pub use vm::casper_vm::CasperVm; diff --git a/odra-casper/test-vm/src/vm/casper_vm.rs b/odra-casper/test-vm/src/vm/casper_vm.rs new file mode 100644 index 00000000..27558f26 --- /dev/null +++ b/odra-casper/test-vm/src/vm/casper_vm.rs @@ -0,0 +1,426 @@ +use odra_casper_shared::consts::*; +use odra_core::prelude::{collections::*, *}; + +use odra_core::prelude::{collections::*, *}; +use std::cell::RefCell; +use std::env; +use std::path::PathBuf; + +use casper_engine_test_support::{ + DeployItemBuilder, ExecuteRequestBuilder, InMemoryWasmTestBuilder, ARG_AMOUNT, + DEFAULT_ACCOUNT_INITIAL_BALANCE, DEFAULT_CHAINSPEC_REGISTRY, DEFAULT_GENESIS_CONFIG, + DEFAULT_GENESIS_CONFIG_HASH, DEFAULT_PAYMENT +}; +use casper_event_standard::try_full_name_from_bytes; +use std::rc::Rc; + +use casper_execution_engine::core::engine_state::{GenesisAccount, RunGenesisRequest}; +use odra_casper_shared::consts; +use odra_casper_shared::consts::*; +use odra_core::entry_point_callback::EntryPointsCaller; +use odra_core::event::EventError; +use odra_core::{CallDef, ContractEnv, HostContext, HostEnv}; +use odra_types::casper_types::account::{Account, AccountHash}; +use odra_types::casper_types::bytesrepr::{Bytes, ToBytes}; +use odra_types::casper_types::{ + runtime_args, BlockTime, Contract, ContractHash, ContractPackageHash, Key, Motes, SecretKey, + StoredValue, URef +}; +use odra_types::{Address, PublicKey, U512}; +use odra_types::{CLTyped, FromBytes, OdraError, RuntimeArgs}; + +pub struct CasperVm { + pub accounts: Vec
, + pub key_pairs: BTreeMap, + pub active_account: Address, + pub context: InMemoryWasmTestBuilder, + pub block_time: u64, + pub calls_counter: u32, + pub error: Option, + pub attached_value: U512, + pub gas_used: BTreeMap, + pub gas_cost: Vec<(String, U512)> +} + +impl CasperVm { + pub fn new() -> Rc> { + Rc::new(RefCell::new(Self::new_instance())) + } + + fn new_instance() -> Self { + let mut genesis_config = DEFAULT_GENESIS_CONFIG.clone(); + let mut accounts: Vec
= Vec::new(); + let mut key_pairs = BTreeMap::new(); + for i in 0..20 { + // Create keypair. + let secret_key = SecretKey::ed25519_from_bytes([i; 32]).unwrap(); + let public_key = PublicKey::from(&secret_key); + + // Create an AccountHash from a public key. + let account_addr = AccountHash::from(&public_key); + + // Create a GenesisAccount. + let account = GenesisAccount::account( + public_key.clone(), + Motes::new(U512::from(DEFAULT_ACCOUNT_INITIAL_BALANCE)), + None + ); + genesis_config.ee_config_mut().push_account(account); + + accounts.push(account_addr.try_into().unwrap()); + key_pairs.insert(account_addr.try_into().unwrap(), (secret_key, public_key)); + } + let run_genesis_request = RunGenesisRequest::new( + *DEFAULT_GENESIS_CONFIG_HASH, + genesis_config.protocol_version(), + genesis_config.take_ee_config(), + DEFAULT_CHAINSPEC_REGISTRY.clone() + ); + + let chainspec_path = + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/chainspec.toml"); + let mut builder = InMemoryWasmTestBuilder::new_with_chainspec(chainspec_path, None); + + builder.run_genesis(&run_genesis_request).commit(); + + Self { + active_account: accounts[0], + context: builder, + accounts, + block_time: 0u64, + calls_counter: 0, + error: None, + attached_value: U512::zero(), + gas_used: BTreeMap::new(), + gas_cost: Vec::new(), + key_pairs + } + } + + fn deploy_contract(&mut self, wasm_path: &str, args: &RuntimeArgs) { + self.error = None; + let session_code = PathBuf::from(wasm_path); + // if let Ok(path) = env::var(ODRA_WASM_PATH_ENV_KEY) { + // let mut path = PathBuf::from(path); + // path.push(wasm_path); + // if path.exists() { + // session_code = path; + // } else { + // panic!("WASM file not found: {:?}", path); + // } + // } + + let deploy_item = DeployItemBuilder::new() + .with_empty_payment_bytes(runtime_args! {ARG_AMOUNT => *DEFAULT_PAYMENT}) + .with_authorization_keys(&[self.active_account_hash()]) + .with_address(self.active_account_hash()) + .with_session_code(session_code, args.clone()) + .with_deploy_hash(self.next_hash()) + .build(); + + let execute_request = ExecuteRequestBuilder::from_deploy_item(deploy_item) + .with_block_time(u64::from(self.block_time)) + .build(); + self.context.exec(execute_request).commit().expect_success(); + self.collect_gas(); + self.gas_cost.push(( + format!("deploy_contract {}", wasm_path), + self.last_call_contract_gas_cost() + )); + } + + /// Read a ContractPackageHash of a given name, from the active account. + pub fn contract_package_hash_from_name(&self, name: &str) -> ContractPackageHash { + let account = self + .context + .get_account(self.active_account_hash()) + .unwrap(); + let key: &Key = account.named_keys().get(name).unwrap(); + ContractPackageHash::from(key.into_hash().unwrap()) + } + pub fn set_caller(&mut self, caller: Address) { + self.active_account = caller; + } + + pub fn get_account(&self, index: usize) -> Address { + self.accounts[index] + } + + pub fn advance_block_time(&mut self, time_diff: u64) { + self.block_time += time_diff + } + + pub fn get_event(&self, contract_address: &Address, index: i32) -> Result { + let contract_package_hash = contract_address.as_contract_package_hash().unwrap(); + let contract_hash: ContractHash = self.get_contract_package_hash(contract_package_hash); + + let dictionary_seed_uref: URef = *self + .context + .get_contract(contract_hash) + .unwrap() + .named_keys() + .get(consts::EVENTS) + .unwrap() + .as_uref() + .unwrap(); + + let events_length: u32 = self + .context + .query( + None, + Key::Hash(contract_hash.value()), + &[String::from(consts::EVENTS_LENGTH)] + ) + .unwrap() + .as_cl_value() + .unwrap() + .clone() + .into_t() + .unwrap(); + + let event_position = odra_utils::event_absolute_position(events_length as usize, index) + .ok_or(EventError::IndexOutOfBounds)?; + + match self.context.query_dictionary_item( + None, + dictionary_seed_uref, + &event_position.to_string() + ) { + Ok(val) => { + let bytes = val + .as_cl_value() + .unwrap() + .clone() + .into_t::() + .unwrap(); + Ok(bytes) + } + Err(_) => Err(EventError::IndexOutOfBounds) + } + } + + pub fn attach_value(&mut self, amount: U512) { + self.attached_value = amount; + } + + pub fn call_contract( + &mut self, + address: &Address, + call_def: CallDef, + use_proxy: bool + ) -> Bytes { + self.error = None; + // TODO: handle unwrap + let hash = *address.as_contract_package_hash().unwrap(); + + let deploy_item = if use_proxy { + let session_code = + include_bytes!("../../resources/proxy_caller_with_return.wasm").to_vec(); + let args_bytes: Vec = call_def.args.to_bytes().unwrap(); + let entry_point = call_def.entry_point.clone(); + let args = runtime_args! { + CONTRACT_PACKAGE_HASH_ARG => hash, + ENTRY_POINT_ARG => entry_point, + ARGS_ARG => Bytes::from(args_bytes), + ATTACHED_VALUE_ARG => call_def.amount, + AMOUNT_ARG => call_def.amount, + }; + + let deploy_item = DeployItemBuilder::new() + .with_empty_payment_bytes(runtime_args! { ARG_AMOUNT => *DEFAULT_PAYMENT}) + .with_authorization_keys(&[self.active_account_hash()]) + .with_address(self.active_account_hash()) + .with_session_bytes(session_code, args) + .with_deploy_hash(self.next_hash()) + .build(); + deploy_item + } else { + let deploy_item = DeployItemBuilder::new() + .with_empty_payment_bytes(runtime_args! { ARG_AMOUNT => *DEFAULT_PAYMENT}) + .with_authorization_keys(&[self.active_account_hash()]) + .with_address(self.active_account_hash()) + .with_stored_versioned_contract_by_hash( + hash.value(), + None, + &call_def.entry_point, + call_def.args.clone() + ) + .with_deploy_hash(self.next_hash()) + .build(); + deploy_item + }; + + let execute_request = ExecuteRequestBuilder::from_deploy_item(deploy_item) + .with_block_time(u64::from(self.block_time)) + .build(); + self.context.exec(execute_request).commit(); + self.collect_gas(); + self.gas_cost.push(( + format!("call_entrypoint {}", call_def.entry_point), + self.last_call_contract_gas_cost() + )); + + self.attached_value = U512::zero(); + if let Some(error) = self.context.get_error() { + // TODO: handle error + // let odra_error = parse_error(error); + panic!("Error: {}", error); + // self.error = Some(odra_error.clone()); + // self.panic_with_error(odra_error, call_def.entry_point, hash); + } else { + self.get_active_account_result() + } + } + + pub fn new_contract( + &mut self, + name: &str, + init_args: Option, + entry_points_caller: Option + ) -> Address { + let wasm_path = format!("{}.wasm", name); + let package_hash_key_name = format!("{}_package_hash", name); + let mut args = init_args.clone().unwrap_or(runtime_args! {}); + args.insert(PACKAGE_HASH_KEY_NAME_ARG, package_hash_key_name.clone()) + .unwrap(); + args.insert(ALLOW_KEY_OVERRIDE_ARG, true).unwrap(); + args.insert(IS_UPGRADABLE_ARG, false).unwrap(); + + if init_args.is_some() { + args.insert(CONSTRUCTOR_NAME_ARG, CONSTRUCTOR_NAME.to_string()) + .unwrap(); + }; + + self.deploy_contract(&wasm_path, &args); + let contract_package_hash = self.contract_package_hash_from_name(&package_hash_key_name); + contract_package_hash.try_into().unwrap() + } + /// Create a new instance with predefined accounts. + pub fn active_account_hash(&self) -> AccountHash { + *self.active_account.as_account_hash().unwrap() + } + + pub fn next_hash(&mut self) -> [u8; 32] { + let seed = self.calls_counter; + self.calls_counter += 1; + let mut hash = [0u8; 32]; + hash[0] = seed as u8; + hash[1] = (seed >> 8) as u8; + hash + } + + /// Returns the balance of the given address. + /// + /// The accepted value can be either an [Address::Account] or [Address::Contract]. + pub fn balance_of(&self, address: &Address) -> U512 { + match address { + Address::Account(account_hash) => self.get_account_cspr_balance(account_hash), + Address::Contract(contract_hash) => self.get_contract_cspr_balance(contract_hash) + } + } + + fn get_account_cspr_balance(&self, account_hash: &AccountHash) -> U512 { + let account: Account = self.context.get_account(account_hash.clone()).unwrap(); + let purse = account.main_purse(); + let gas_used = self + .gas_used + .get(account_hash) + .map(|x| *x) + .unwrap_or(U512::zero()); + self.context.get_purse_balance(purse) + gas_used + } + + fn get_contract_cspr_balance(&self, contract_hash: &ContractPackageHash) -> U512 { + let contract_hash: ContractHash = self.get_contract_package_hash(contract_hash); + let contract: Contract = self.context.get_contract(contract_hash).unwrap(); + contract + .named_keys() + .get(consts::CONTRACT_MAIN_PURSE) + .and_then(|key| key.as_uref()) + .map(|purse| self.context.get_purse_balance(*purse)) + .unwrap_or_else(U512::zero) + } + + fn get_contract_package_hash(&self, contract_hash: &ContractPackageHash) -> ContractHash { + self.context + .get_contract_package(contract_hash.clone()) + .unwrap() + .current_contract_hash() + .unwrap() + } + + /// Read a value from Account's named keys. + pub fn get_account_value( + &self, + hash: AccountHash, + name: &str + ) -> Result { + let result: Result = + self.context + .query(None, Key::Account(hash), &[name.to_string()]); + + result.map(|value| value.as_cl_value().unwrap().clone().into_t().unwrap()) + } + + pub fn get_active_account_result(&self) -> Bytes { + let active_account = self.active_account_hash(); + let bytes: Bytes = self + .get_account_value(active_account, RESULT_KEY) + .unwrap_or_default(); + bytes + } + + pub fn collect_gas(&mut self) { + *self + .gas_used + .entry(*self.active_account.as_account_hash().unwrap()) + .or_insert_with(U512::zero) += *DEFAULT_PAYMENT; + } + + /// Returns the cost of the last deploy. + /// Keep in mind that this may be different from the cost of the deploy on the live network. + /// This is NOT the amount of gas charged - see [last_call_contract_gas_used]. + pub fn last_call_contract_gas_cost(&self) -> U512 { + self.context.last_exec_gas_cost().value() + } + + /// Returns the amount of gas used for last call. + pub fn last_call_contract_gas_used(&self) -> U512 { + *DEFAULT_PAYMENT + } + + /// Returns total gas used by the account. + pub fn total_gas_used(&self, address: Address) -> U512 { + match &address { + Address::Account(address) => self.gas_used.get(address).cloned().unwrap_or_default(), + Address::Contract(address) => panic!("Contract {} can't burn gas.", address) + } + } + + /// Returns the report of the gas used during the whole lifetime of the CasperVM. + pub fn gas_report(&self) -> Vec<(String, U512)> { + self.gas_cost.clone() + } + + /// Returns the public key that corresponds to the given Account Address. + pub fn public_key(&self, address: &Address) -> PublicKey { + let (_, public_key) = self.key_pairs.get(address).unwrap(); + public_key.clone() + } + + /// Cryptographically signs a message as a given account. + pub fn sign_message(&self, message: &Bytes, address: &Address) -> Bytes { + let (secret_key, public_key) = self.key_pairs.get(address).unwrap(); + let signature = odra_types::casper_types::crypto::sign(message, secret_key, public_key) + .to_bytes() + .unwrap(); + Bytes::from(signature) + } + + pub fn print_gas_report(&self) { + println!("Gas report:"); + for (name, cost) in self.gas_report() { + println!("{}: {}", name, cost); + } + } +} diff --git a/odra-casper/test-vm/src/vm/mod.rs b/odra-casper/test-vm/src/vm/mod.rs new file mode 100644 index 00000000..57b68e16 --- /dev/null +++ b/odra-casper/test-vm/src/vm/mod.rs @@ -0,0 +1 @@ +pub mod casper_vm; diff --git a/odra-casper/wasm-env/Cargo.toml b/odra-casper/wasm-env/Cargo.toml new file mode 100644 index 00000000..3d630a83 --- /dev/null +++ b/odra-casper/wasm-env/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "odra-casper-wasm-env" +version = "0.1.0" +edition = "2021" + +[dependencies] +casper-types = "3.0.0" +casper-event-standard = { workspace = true } +lazy_static = { version = "1.4.0", features = [ "spin_no_std" ] } +odra-types = { path = "../../types" } +odra-core = { path = "../../odra-core" } +bytes = "1.5.0" + +[target.'cfg(target_arch = "wasm32")'.dependencies] +casper-contract = { version = "3.0.0", default-features = false, features = ["test-support"] } +ink_allocator = { version = "4.2.1", default-features = false } + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +casper-contract = { version = "3.0.0", features = ["test-support"] } \ No newline at end of file diff --git a/odra-casper/wasm-env/src/consts.rs b/odra-casper/wasm-env/src/consts.rs new file mode 100644 index 00000000..e442ddf5 --- /dev/null +++ b/odra-casper/wasm-env/src/consts.rs @@ -0,0 +1,58 @@ +/// The arg name for the package hash key name. +pub const PACKAGE_HASH_KEY_NAME_ARG: &str = "odra_cfg_package_hash_key_name"; + +/// The arg name for the allow key override. +pub const ALLOW_KEY_OVERRIDE_ARG: &str = "odra_cfg_allow_key_override"; + +/// The arg name for the contract upgradeability setting. +pub const IS_UPGRADABLE_ARG: &str = "odra_cfg_is_upgradable"; + +/// Constructor name argument. +pub const CONSTRUCTOR_NAME_ARG: &str = "odra_cfg_constructor"; + +/// The state key name. +pub const STATE_KEY: &str = "state"; + +/// Constuctor group name. +pub const CONSTRUCTOR_GROUP_NAME: &str = "constructor_group"; +/// The key under which the events are stored. +pub const EVENTS: &str = casper_event_standard::EVENTS_DICT; + +/// The key under which the events length is stored. +pub const EVENTS_LENGTH: &str = casper_event_standard::EVENTS_LENGTH; + +/// The key under which the contract main purse URef is stored. +pub const CONTRACT_MAIN_PURSE: &str = "__contract_main_purse"; + +/// The key under which the contract cargo purse URef is stored. +pub const CONTRACT_CARGO_PURSE: &str = "__contract_cargo_purse"; + +/// The key under which the reentrancy guard status is stored. +pub const REENTRANCY_GUARD: [u8; 18] = *b"__reentrancy_guard"; + +/// The key for account's cargo purse. +pub const CARGO_PURSE_KEY: &str = "__cargo_purse"; + +/// The key for the result bytes. It's used in test_env. +pub const RESULT_KEY: &str = "__result"; + +/// The arg name of a temporally purse that is used transfer tokens to a contract. +pub const CARGO_PURSE_ARG: &str = "cargo_purse"; + +/// The arg name of the contract package hash. +pub const CONTRACT_PACKAGE_HASH_ARG: &str = "contract_package_hash"; + +/// The arg name of the entry point. +pub const ENTRY_POINT_ARG: &str = "entry_point"; + +/// The arg name of the args. +pub const ARGS_ARG: &str = "args"; + +/// The arg name of the CSPR attached amount. +pub const ATTACHED_VALUE_ARG: &str = "attached_value"; + +/// The arg name for `amount` argument. +pub const AMOUNT_ARG: &str = "amount"; + +/// Constructor name +pub const CONSTRUCTOR_NAME: &str = "init"; diff --git a/odra-casper/wasm-env/src/host_functions.rs b/odra-casper/wasm-env/src/host_functions.rs new file mode 100644 index 00000000..eafa143f --- /dev/null +++ b/odra-casper/wasm-env/src/host_functions.rs @@ -0,0 +1,474 @@ +extern crate alloc; + +use alloc::borrow::ToOwned; +use alloc::boxed::Box; +use alloc::{format, string::String, vec, vec::Vec}; +use casper_contract::contract_api::system::{ + create_purse, get_purse_balance, transfer_from_purse_to_account, transfer_from_purse_to_purse +}; +use casper_contract::contract_api::{runtime, storage}; +use casper_contract::unwrap_or_revert::UnwrapOrRevert; +use casper_contract::{contract_api, ext_ffi}; +use casper_event_standard::{Schema, Schemas}; +use casper_types::bytesrepr::Bytes; +use casper_types::system::CallStackElement; +use casper_types::{api_error, ContractVersion, RuntimeArgs, U512}; +use casper_types::{ + bytesrepr::{FromBytes, ToBytes}, + contracts::NamedKeys, + ApiError, CLTyped, ContractPackageHash, EntryPoints, Key, URef +}; +use core::mem::MaybeUninit; +use odra_types::call_def::CallDef; +use odra_types::{Address, ExecutionError}; + +use crate::consts; + +lazy_static::lazy_static! { + static ref STATE: URef = { + let key = runtime::get_key(consts::STATE_KEY).unwrap_or_revert(); + let state_uref: URef = *key.as_uref().unwrap_or_revert(); + state_uref + }; + + static ref STATE_BYTES: Vec = { + (*STATE).into_bytes().unwrap_or_revert() + }; +} + +pub(crate) static mut ATTACHED_VALUE: U512 = U512::zero(); + +pub fn install_contract( + entry_points: EntryPoints, + events: Schemas, + init_args: Option +) -> ContractPackageHash { + // Read arguments + let package_hash_key: String = runtime::get_named_arg(consts::PACKAGE_HASH_KEY_NAME_ARG); + let allow_key_override: bool = runtime::get_named_arg(consts::ALLOW_KEY_OVERRIDE_ARG); + let is_upgradable: bool = runtime::get_named_arg(consts::IS_UPGRADABLE_ARG); + + // Check if the package hash is already in the storage. + // Revert if key override is not allowed. + if !allow_key_override && runtime::has_key(&package_hash_key) { + revert(ExecutionError::contract_already_installed().code()); // TODO: fix + }; + + // Prepare named keys. + let named_keys = initial_named_keys(events); + + // Create new contract. + if is_upgradable { + let access_uref_key = format!("{}_access_token", package_hash_key); + storage::new_contract( + entry_points, + Some(named_keys), + Some(package_hash_key.clone()), + Some(access_uref_key) + ); + } else { + storage::new_locked_contract( + entry_points, + Some(named_keys), + Some(package_hash_key.clone()), + None + ); + } + + // Read contract package hash from the storage. + let contract_package_hash: ContractPackageHash = runtime::get_key(&package_hash_key) + .unwrap_or_revert() + .into_hash() + .unwrap_or_revert() + .into(); + + if let Some(args) = init_args { + let init_access = create_constructor_group(contract_package_hash); + let _: () = runtime::call_versioned_contract(contract_package_hash, None, "init", args); + revoke_access_to_constructor_group(contract_package_hash, init_access); + } + + contract_package_hash +} + +fn initial_named_keys(schemas: Schemas) -> NamedKeys { + let mut named_keys = casper_types::contracts::NamedKeys::new(); + named_keys.insert( + String::from(consts::STATE_KEY), + Key::URef(storage::new_dictionary(consts::STATE_KEY).unwrap_or_revert()) + ); + named_keys.insert( + String::from(casper_event_standard::EVENTS_DICT), + Key::URef(storage::new_dictionary(casper_event_standard::EVENTS_DICT).unwrap_or_revert()) + ); + named_keys.insert( + String::from(casper_event_standard::EVENTS_LENGTH), + Key::URef(storage::new_uref(0u32)) + ); + named_keys.insert( + String::from(casper_event_standard::CES_VERSION_KEY), + Key::URef(storage::new_uref(casper_event_standard::CES_VERSION)) + ); + named_keys.insert( + String::from(casper_event_standard::EVENTS_SCHEMA), + Key::URef(storage::new_uref(schemas)) + ); + + runtime::remove_key(consts::STATE_KEY); + runtime::remove_key(casper_event_standard::EVENTS_DICT); + runtime::remove_key(casper_event_standard::EVENTS_LENGTH); + runtime::remove_key(casper_event_standard::EVENTS_SCHEMA); + runtime::remove_key(casper_event_standard::CES_VERSION_KEY); + + named_keys +} + +/// Revert the execution. +#[inline(always)] +pub fn revert(error: u16) -> ! { + runtime::revert(ApiError::User(error)) +} + +pub fn get_block_time() -> u64 { + runtime::get_blocktime().into() +} + +pub fn get_value(key: &[u8]) -> Option> { + let uref_ptr = (*STATE_BYTES).as_ptr(); + let uref_size = (*STATE_BYTES).len(); + + let dictionary_item_key_ptr = key.as_ptr(); + let dictionary_item_key_size = key.len(); + + let value_size = { + let mut value_size = core::mem::MaybeUninit::uninit(); + let ret = unsafe { + casper_contract::ext_ffi::casper_dictionary_get( + uref_ptr, + uref_size, + dictionary_item_key_ptr, + dictionary_item_key_size, + value_size.as_mut_ptr() + ) + }; + match casper_types::api_error::result_from(ret) { + Ok(_) => unsafe { value_size.assume_init() }, + Err(ApiError::ValueNotFound) => return None, + Err(e) => runtime::revert(e) + } + }; + + let value_bytes = read_host_buffer(value_size).unwrap_or_revert(); + let value_bytes = Vec::from_bytes(value_bytes.as_slice()).unwrap_or_revert(); + Some(value_bytes.0) +} + +/// Transfers native token from the contract caller to the given address. +pub fn transfer_tokens(to: &Address, amount: &U512) { + let main_purse = get_or_create_main_purse(); + + match to { + Address::Account(account) => { + transfer_from_purse_to_account(main_purse, *account, *amount, None).unwrap_or_revert(); + } + // todo: Why? + Address::Contract(_) => revert(ExecutionError::can_not_transfer_to_contract().code()) + }; +} + +fn read_host_buffer(size: usize) -> Result, ApiError> { + let mut dest: Vec = if size == 0 { + Vec::new() + } else { + let bytes_non_null_ptr = casper_contract::contract_api::alloc_bytes(size); + unsafe { Vec::from_raw_parts(bytes_non_null_ptr.as_ptr(), size, size) } + }; + read_host_buffer_into(&mut dest)?; + Ok(dest) +} + +fn read_host_buffer_into(dest: &mut [u8]) -> Result { + let mut bytes_written = core::mem::MaybeUninit::uninit(); + let ret = unsafe { + casper_contract::ext_ffi::casper_read_host_buffer( + dest.as_mut_ptr(), + dest.len(), + bytes_written.as_mut_ptr() + ) + }; + casper_types::api_error::result_from(ret)?; + Ok(unsafe { bytes_written.assume_init() }) +} + +pub fn emit_event(event: &Bytes) { + casper_event_standard::emit(event) +} + +pub fn set_value(key: &[u8], value: &[u8]) { + let uref_ptr = (*STATE_BYTES).as_ptr(); + let uref_size = (*STATE_BYTES).len(); + + let dictionary_item_key_ptr = key.as_ptr(); + let dictionary_item_key_size = key.len(); + + let cl_value = casper_types::CLValue::from_t(value.to_vec()).unwrap_or_revert(); + let (value_ptr, value_size, _bytes) = to_ptr(cl_value); + // let value_ptr = value.as_ptr(); + // let value_size = value.len(); + + let result = unsafe { + let ret = casper_contract::ext_ffi::casper_dictionary_put( + uref_ptr, + uref_size, + dictionary_item_key_ptr, + dictionary_item_key_size, + value_ptr, + value_size + ); + casper_types::api_error::result_from(ret) + }; + + result.unwrap_or_revert(); +} + +fn to_ptr(t: T) -> (*const u8, usize, Vec) { + let bytes = t.into_bytes().unwrap_or_revert(); + let ptr = bytes.as_ptr(); + let size = bytes.len(); + (ptr, size, bytes) +} + +/// Gets the immediate session caller of the current execution. +/// +/// This function ensures that only session code can execute this function, and disallows stored +/// session/stored contracts. +#[inline(always)] +pub fn caller() -> Address { + let second_elem = take_call_stack_elem(1); + call_stack_element_to_address(second_elem) +} + +/// Calls a contract method by Address +#[inline(always)] +pub fn call_contract(address: Address, call_def: CallDef) -> Bytes { + let contract_package_hash = *address.as_contract_package_hash().unwrap_or_revert(); + let method = call_def.method(); + let mut args = call_def.args().to_owned(); + if call_def.amount == U512::zero() { + call_versioned_contract(contract_package_hash, None, method, args) + } else { + let cargo_purse = get_or_create_cargo_purse(); + let main_purse = get_main_purse().unwrap_or_revert(); + + transfer_from_purse_to_purse(main_purse, cargo_purse, call_def.amount, None) + .unwrap_or_revert_with(ApiError::Transfer); + args.insert(consts::CARGO_PURSE_ARG, cargo_purse) + .unwrap_or_revert(); + + let result = call_versioned_contract(contract_package_hash, None, method, args); + if !is_purse_empty(cargo_purse) { + runtime::revert(ApiError::InvalidPurse) + } + result + } +} + +fn is_purse_empty(purse: URef) -> bool { + get_purse_balance(purse) + .map(|balance| balance.is_zero()) + .unwrap_or_else(|| true) +} +fn get_or_create_cargo_purse() -> URef { + match runtime::get_key(consts::CONTRACT_CARGO_PURSE) { + Some(key) => *key.as_uref().unwrap_or_revert(), + None => { + let purse = create_purse(); + runtime::put_key(consts::CONTRACT_CARGO_PURSE, purse.into()); + purse + } + } +} + +/// Gets the address of the currently run contract +#[inline(always)] +pub fn self_address() -> Address { + let first_elem = take_call_stack_elem(0); + call_stack_element_to_address(first_elem) +} + +/// Returns address based on a [`CallStackElement`]. +/// +/// For `Session` and `StoredSession` variants it will return account hash, and for `StoredContract` +/// case it will use contract hash as the address. +fn call_stack_element_to_address(call_stack_element: CallStackElement) -> Address { + match call_stack_element { + CallStackElement::Session { account_hash } => Address::try_from(account_hash) + .map_err(|e| ApiError::User(ExecutionError::from(e).code())) + .unwrap_or_revert(), + CallStackElement::StoredSession { account_hash, .. } => { + // Stored session code acts in account's context, so if stored session + // wants to interact, caller's address will be used. + Address::try_from(account_hash) + .map_err(|e| ApiError::User(ExecutionError::from(e).code())) + .unwrap_or_revert() + } + CallStackElement::StoredContract { + contract_package_hash, + .. + } => Address::try_from(contract_package_hash) + .map_err(|e| ApiError::User(ExecutionError::from(e).code())) + .unwrap_or_revert() + } +} + +fn take_call_stack_elem(n: usize) -> CallStackElement { + runtime::get_call_stack() + .into_iter() + .nth_back(n) + .unwrap_or_revert() +} + +fn create_constructor_group(contract_package_hash: ContractPackageHash) -> URef { + storage::create_contract_user_group( + contract_package_hash, + consts::CONSTRUCTOR_GROUP_NAME, + 1, + Default::default() + ) + .unwrap_or_revert() + .pop() + .unwrap_or_revert() +} + +fn revoke_access_to_constructor_group( + contract_package_hash: ContractPackageHash, + constructor_access: URef +) { + let mut urefs = alloc::collections::BTreeSet::new(); + urefs.insert(constructor_access); + storage::remove_contract_user_group_urefs( + contract_package_hash, + consts::CONSTRUCTOR_GROUP_NAME, + urefs + ) + .unwrap_or_revert(); +} +/// Invokes the specified `entry_point_name` of stored logic at a specific `contract_package_hash` +/// address, for the most current version of a contract package by default or a specific +/// `contract_version` if one is provided, and passing the provided `runtime_args` to it +/// +/// If the stored contract calls [`ret`], then that value is returned from +/// `call_versioned_contract`. If the stored contract calls [`revert`], then execution stops and +/// `call_versioned_contract` doesn't return. Otherwise `call_versioned_contract` returns `()`. +pub fn call_versioned_contract( + contract_package_hash: ContractPackageHash, + contract_version: Option, + entry_point_name: &str, + runtime_args: RuntimeArgs +) -> odra_types::Bytes { + let (contract_package_hash_ptr, contract_package_hash_size, _bytes) = + to_ptr(contract_package_hash); + let (contract_version_ptr, contract_version_size, _bytes) = to_ptr(contract_version); + let (entry_point_name_ptr, entry_point_name_size, _bytes) = to_ptr(entry_point_name); + let (runtime_args_ptr, runtime_args_size, _bytes) = to_ptr(runtime_args); + + let bytes_written = { + let mut bytes_written = MaybeUninit::uninit(); + let ret = unsafe { + ext_ffi::casper_call_versioned_contract( + contract_package_hash_ptr, + contract_package_hash_size, + contract_version_ptr, + contract_version_size, + entry_point_name_ptr, + entry_point_name_size, + runtime_args_ptr, + runtime_args_size, + bytes_written.as_mut_ptr() + ) + }; + api_error::result_from(ret).unwrap_or_revert(); + unsafe { bytes_written.assume_init() } + }; + odra_types::Bytes::from(deserialize_contract_result(bytes_written)) +} +fn deserialize_contract_result(bytes_written: usize) -> Vec { + let serialized_result = if bytes_written == 0 { + // If no bytes were written, the host buffer hasn't been set and hence shouldn't be read. + vec![] + } else { + // NOTE: this is a copy of the contents of `read_host_buffer()`. Calling that directly from + // here causes several contracts to fail with a Wasmi `Unreachable` error. + let bytes_non_null_ptr = contract_api::alloc_bytes(bytes_written); + let mut dest: Vec = unsafe { + Vec::from_raw_parts(bytes_non_null_ptr.as_ptr(), bytes_written, bytes_written) + }; + read_host_buffer_into(&mut dest).unwrap_or_revert(); + dest + }; + serialized_result +} + +pub fn attached_value() -> U512 { + unsafe { ATTACHED_VALUE } +} + +/// Stores in memory the amount attached to the current call. +pub fn set_attached_value(amount: U512) { + unsafe { + ATTACHED_VALUE = amount; + } +} + +/// Zeroes the amount attached to the current call. +pub fn clear_attached_value() { + unsafe { ATTACHED_VALUE = U512::zero() } +} +/// Checks if given named argument exists. +pub fn named_arg_exists(name: &str) -> bool { + let mut arg_size: usize = 0; + let ret = unsafe { + casper_contract::ext_ffi::casper_get_named_arg_size( + name.as_bytes().as_ptr(), + name.len(), + &mut arg_size as *mut usize + ) + }; + ret == 0 +} + +/// Transfers attached value to the currently executing contract. +pub fn handle_attached_value() { + // If the cargo purse argument is not present, do nothing. + // Attached value is set to zero by default. + if !named_arg_exists(consts::CARGO_PURSE_ARG) { + return; + } + + // Handle attached value. + let cargo_purse = runtime::get_named_arg(consts::CARGO_PURSE_ARG); + let amount = get_purse_balance(cargo_purse); + if let Some(amount) = amount { + let contract_purse = get_or_create_main_purse(); + transfer_from_purse_to_purse(cargo_purse, contract_purse, amount, None).unwrap_or_revert(); + set_attached_value(amount); + } else { + revert(ExecutionError::native_token_transfer_error().code()) + } +} + +pub fn get_or_create_main_purse() -> URef { + match get_main_purse() { + Some(purse) => purse, + None => { + let purse = create_purse(); + runtime::put_key(consts::CONTRACT_MAIN_PURSE, purse.into()); + purse + } + } +} + +#[inline(always)] +pub fn get_main_purse() -> Option { + runtime::get_key(consts::CONTRACT_MAIN_PURSE).map(|key| *key.as_uref().unwrap_or_revert()) +} diff --git a/odra-casper/wasm-env/src/lib.rs b/odra-casper/wasm-env/src/lib.rs new file mode 100644 index 00000000..eba34e2f --- /dev/null +++ b/odra-casper/wasm-env/src/lib.rs @@ -0,0 +1,25 @@ +#![no_std] +#![cfg_attr(not(test), feature(core_intrinsics))] +#![allow(dead_code)] +#![allow(unused_imports)] +#![allow(unused_variables)] + +extern crate alloc; + +pub mod consts; +pub mod host_functions; +pub mod wasm_contract_env; + +pub use casper_contract; +pub use wasm_contract_env::WasmContractEnv; + +#[cfg(target_arch = "wasm32")] +#[allow(unused_imports)] +use ink_allocator; + +#[cfg(target_arch = "wasm32")] +#[panic_handler] +#[no_mangle] +pub fn panic(_info: &core::panic::PanicInfo) -> ! { + core::intrinsics::abort(); +} diff --git a/odra-casper/wasm-env/src/wasm_contract_env.rs b/odra-casper/wasm-env/src/wasm_contract_env.rs new file mode 100644 index 00000000..59104a47 --- /dev/null +++ b/odra-casper/wasm-env/src/wasm_contract_env.rs @@ -0,0 +1,59 @@ +extern crate alloc; + +use crate::host_functions; +use alloc::boxed::Box; +use casper_types::bytesrepr::ToBytes; +use casper_types::U512; +use odra_core::{prelude::*, ContractContext, ContractEnv}; +use odra_types::{Address, Bytes, EventData}; + +#[derive(Clone)] +pub struct WasmContractEnv; + +impl ContractContext for WasmContractEnv { + fn get_value(&self, key: &[u8]) -> Option { + host_functions::get_value(key).map(|v| Bytes::from(v)) + } + + fn set_value(&self, key: &[u8], value: Bytes) { + host_functions::set_value(key, value.as_slice()); + } + + fn caller(&self) -> Address { + host_functions::caller() + } + + fn self_address(&self) -> Address { + host_functions::self_address() + } + + fn call_contract(&self, address: Address, call_def: odra_core::CallDef) -> Bytes { + host_functions::call_contract(address, call_def) + } + + fn get_block_time(&self) -> u64 { + host_functions::get_block_time() + } + + fn attached_value(&self) -> U512 { + host_functions::attached_value() + } + + fn emit_event(&self, event: &Bytes) { + host_functions::emit_event(event); + } + + fn transfer_tokens(&self, to: &Address, amount: &U512) { + host_functions::transfer_tokens(to, amount); + } + + fn revert(&self, code: u16) -> ! { + host_functions::revert(code) + } +} + +impl WasmContractEnv { + pub fn new() -> ContractEnv { + ContractEnv::new(0, Rc::new(RefCell::new(WasmContractEnv))) + } +} diff --git a/odra-core/Cargo.toml b/odra-core/Cargo.toml new file mode 100644 index 00000000..7fe4eff0 --- /dev/null +++ b/odra-core/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "odra-core" # Odra Core is new odra. +version = "0.1.0" +edition = "2021" + +[dependencies] +odra-types = { path = "../types" } +casper-event-standard = "0.4.0" \ No newline at end of file diff --git a/odra-core/src/contract_context.rs b/odra-core/src/contract_context.rs new file mode 100644 index 00000000..b4bd0e30 --- /dev/null +++ b/odra-core/src/contract_context.rs @@ -0,0 +1,15 @@ +use odra_types::call_def::CallDef; +use odra_types::{Address, Bytes, ToBytes, U512}; + +pub trait ContractContext { + fn get_value(&self, key: &[u8]) -> Option; + fn set_value(&self, key: &[u8], value: Bytes); + fn caller(&self) -> Address; + fn self_address(&self) -> Address; + fn call_contract(&self, address: Address, call_def: CallDef) -> Bytes; + fn get_block_time(&self) -> u64; + fn attached_value(&self) -> U512; + fn emit_event(&self, event: &Bytes); + fn transfer_tokens(&self, to: &Address, amount: &U512); + fn revert(&self, code: u16) -> !; +} diff --git a/odra-core/src/contract_env.rs b/odra-core/src/contract_env.rs new file mode 100644 index 00000000..cf3f369a --- /dev/null +++ b/odra-core/src/contract_env.rs @@ -0,0 +1,104 @@ +use crate::prelude::*; +use odra_types::call_def::CallDef; +use odra_types::{Address, Bytes, CLTyped, FromBytes, ToBytes, U512}; + +use crate::key_maker; +pub use crate::ContractContext; + +pub struct ContractEnv { + index: u32, + mapping_data: Vec, + backend: Rc> +} + +impl ContractEnv { + pub const fn new(index: u32, backend: Rc>) -> Self { + Self { + index, + mapping_data: Vec::new(), + backend + } + } + + pub fn duplicate(&self) -> Self { + Self { + index: self.index, + mapping_data: self.mapping_data.clone(), + backend: self.backend.clone() + } + } + + pub fn current_key(&self) -> Vec { + let index_bytes = key_maker::u32_to_hex(self.index); + let mapping_data_bytes = key_maker::bytes_to_hex(&self.mapping_data); + let mut key = Vec::new(); + key.extend_from_slice(&index_bytes); + key.extend_from_slice(&mapping_data_bytes); + key + } + + pub fn add_to_mapping_data(&mut self, data: &[u8]) { + self.mapping_data.extend_from_slice(data); + } + + pub fn child(&self, index: u8) -> Self { + Self { + index: (self.index << 4) + index as u32, + mapping_data: self.mapping_data.clone(), + backend: self.backend.clone() + } + } + + pub fn get_value(&self, key: &[u8]) -> Option { + self.backend + .borrow() + .get_value(key) + .map(|bytes| T::from_bytes(&bytes).unwrap().0) + } + + pub fn set_value(&self, key: &[u8], value: T) { + let bytes = value.to_bytes().unwrap(); + self.backend.borrow().set_value(key, Bytes::from(bytes)); + } + + pub fn caller(&self) -> Address { + let backend = self.backend.borrow(); + backend.caller() + } + + pub fn call_contract(&self, address: Address, call: CallDef) -> T { + let backend = self.backend.borrow(); + let bytes = backend.call_contract(address, call); + T::from_bytes(&bytes).unwrap().0 + } + + pub fn self_address(&self) -> Address { + let backend = self.backend.borrow(); + backend.self_address() + } + + pub fn transfer_tokens(&self, to: &Address, amount: &U512) { + let backend = self.backend.borrow(); + backend.transfer_tokens(to, amount) + } + + pub fn get_block_time(&self) -> u64 { + let backend = self.backend.borrow(); + backend.get_block_time() + } + + pub fn attached_value(&self) -> U512 { + let backend = self.backend.borrow(); + backend.attached_value() + } + + pub fn revert(&self, code: u16) -> ! { + let backend = self.backend.borrow(); + backend.revert(code) + } + + pub fn emit_event(&self, event: T) { + let backend = self.backend.borrow(); + backend.emit_event(&event.to_bytes().unwrap().into()) + } +} diff --git a/odra-core/src/entry_point_callback.rs b/odra-core/src/entry_point_callback.rs new file mode 100644 index 00000000..38f525d3 --- /dev/null +++ b/odra-core/src/entry_point_callback.rs @@ -0,0 +1,22 @@ +use crate::{ContractEnv, HostEnv}; +use odra_types::call_def::CallDef; +use odra_types::Bytes; + +#[derive(Clone)] +pub struct EntryPointsCaller { + pub f: fn(contract_env: ContractEnv, call_def: CallDef) -> Bytes, + host_env: HostEnv +} + +impl EntryPointsCaller { + pub fn new( + host_env: HostEnv, + f: fn(contract_env: ContractEnv, call_def: CallDef) -> Bytes + ) -> Self { + EntryPointsCaller { f, host_env } + } + + pub fn call(&self, call_def: CallDef) -> Bytes { + (self.f)(self.host_env.contract_env(), call_def) + } +} diff --git a/types/src/event.rs b/odra-core/src/event.rs similarity index 53% rename from types/src/event.rs rename to odra-core/src/event.rs index bf1103d6..2b2cf81e 100644 --- a/types/src/event.rs +++ b/odra-core/src/event.rs @@ -2,20 +2,6 @@ use alloc::string::String; -#[cfg(not(target_arch = "wasm32"))] -use crate::contract_def::Event as Schema; - -/// Event interface -pub trait OdraEvent { - /// Emits &self in the current environment. - fn emit(self); - /// Returns the event name. - fn name() -> String; - #[cfg(not(target_arch = "wasm32"))] - /// Returns the event schema. - fn schema() -> Schema; -} - /// Event-related errors. #[derive(Debug, PartialEq, Eq, PartialOrd)] pub enum EventError { @@ -26,5 +12,7 @@ pub enum EventError { /// Formatting error while deserializing. Formatting, /// Unexpected error while deserializing. - Parsing + Parsing, + /// Could not extract event name. + CouldntExtractName } diff --git a/odra-core/src/host_context.rs b/odra-core/src/host_context.rs new file mode 100644 index 00000000..34908704 --- /dev/null +++ b/odra-core/src/host_context.rs @@ -0,0 +1,24 @@ +use crate::entry_point_callback::EntryPointsCaller; +use crate::event::EventError; +use crate::{CallDef, ContractEnv}; +use odra_types::Bytes; +use odra_types::RuntimeArgs; +use odra_types::{Address, U512}; + +pub trait HostContext { + fn set_caller(&self, caller: Address); + fn get_account(&self, index: usize) -> Address; + fn balance_of(&self, address: &Address) -> U512; + fn advance_block_time(&self, time_diff: u64); + /// Returns event bytes by contract address and index. + fn get_event(&self, contract_address: &Address, index: i32) -> Result; + fn call_contract(&self, address: &Address, call_def: CallDef, use_proxy: bool) -> Bytes; + fn new_contract( + &self, + name: &str, + init_args: Option, + entry_points_caller: Option + ) -> Address; + fn contract_env(&self) -> ContractEnv; + fn print_gas_report(&self); +} diff --git a/odra-core/src/host_env.rs b/odra-core/src/host_env.rs new file mode 100644 index 00000000..f511a543 --- /dev/null +++ b/odra-core/src/host_env.rs @@ -0,0 +1,92 @@ +use crate::entry_point_callback::EntryPointsCaller; +use crate::event::EventError; +use crate::host_context::HostContext; +use crate::prelude::*; +use crate::{CallDef, ContractEnv}; +use casper_event_standard::EventInstance; +use odra_types::{Address, U512}; +use odra_types::{Bytes, RuntimeArgs}; +use odra_types::{CLTyped, FromBytes}; + +#[derive(Clone)] +pub struct HostEnv { + backend: Rc> +} + +impl HostEnv { + pub fn new(backend: Rc>) -> HostEnv { + HostEnv { backend } + } + + pub fn get_account(&self, index: usize) -> Address { + let backend = self.backend.borrow(); + backend.get_account(index) + } + + pub fn set_caller(&self, address: Address) { + let backend = self.backend.borrow(); + backend.set_caller(address) + } + + pub fn advance_block_time(&self, time_diff: u64) { + let backend = self.backend.borrow(); + backend.advance_block_time(time_diff) + } + + pub fn new_contract( + &self, + name: &str, + init_args: Option, + entry_points_caller: Option + ) -> Address { + let backend = self.backend.borrow(); + backend.new_contract(name, init_args, entry_points_caller) + } + + pub fn call_contract(&self, address: &Address, call_def: CallDef) -> T { + let backend = self.backend.borrow(); + let use_proxy = T::cl_type() != <()>::cl_type() || !call_def.attached_value().is_zero(); + let result = backend.call_contract(address, call_def, use_proxy); + T::from_bytes(&result).unwrap().0 + } + + pub fn contract_env(&self) -> ContractEnv { + self.backend.borrow().contract_env() + } + + pub fn print_gas_report(&self) { + let backend = self.backend.borrow(); + backend.print_gas_report() + } + + pub fn balance_of(&self, address: &Address) -> U512 { + let backend = self.backend.borrow(); + backend.balance_of(address) + } + + pub fn get_event( + &self, + contract_address: &Address, + index: i32 + ) -> Result { + let backend = self.backend.borrow(); + + let bytes = backend.get_event(contract_address, index)?; + // TODO: Make following line go away by passing ToBytes insted of Bytes to event. + let bytes = Bytes::from_bytes(bytes.as_slice()).unwrap().0; + let event_name = Self::extract_event_name(&bytes)?; + if event_name == format!("event_{}", T::name()) { + T::from_bytes(&bytes) + .map_err(|_| EventError::Parsing) + .map(|r| r.0) + } else { + Err(EventError::UnexpectedType(event_name)) + } + } + + /// Returns the name of the passed event + fn extract_event_name(bytes: &[u8]) -> Result { + let name = FromBytes::from_bytes(bytes).map_err(|_| EventError::CouldntExtractName)?; + Ok(name.0) + } +} diff --git a/odra-core/src/key_maker.rs b/odra-core/src/key_maker.rs new file mode 100644 index 00000000..c9499fca --- /dev/null +++ b/odra-core/src/key_maker.rs @@ -0,0 +1,38 @@ +use crate::prelude::*; + +static TABLE: &[u8] = b"0123456789abcdef"; + +#[inline] +fn hex(byte: u8) -> u8 { + TABLE[byte as usize] +} + +pub fn u32_to_hex(value: u32) -> [u8; 8] { + let mut result = [0u8; 8]; + let bytes = value.to_be_bytes(); + for i in 0..4 { + result[2 * i] = hex(bytes[i] >> 4); + result[2 * i + 1] = hex(bytes[i] & 0xf); + } + result +} + +pub fn bytes_to_hex(bytes: &[u8]) -> Vec { + let mut result = Vec::new(); + for byte in bytes { + result.push(hex(byte >> 4)); + result.push(hex(byte & 0xf)); + } + result +} + +#[cfg(test)] +mod tests { + use crate::key_maker::u32_to_hex; + + #[test] + fn test_u32_to_hex() { + assert_eq!(&u32_to_hex(0), b"00000000"); + assert_eq!(&u32_to_hex(255), b"000000ff"); + } +} diff --git a/odra-core/src/lib.rs b/odra-core/src/lib.rs new file mode 100644 index 00000000..2524c807 --- /dev/null +++ b/odra-core/src/lib.rs @@ -0,0 +1,28 @@ +#![no_std] +#![feature(type_alias_impl_trait)] +#![feature(once_cell)] + +extern crate alloc; + +mod contract_context; +mod contract_env; +pub mod entry_point_callback; +pub mod event; +mod host_context; +mod host_env; +mod key_maker; +pub mod mapping; +pub mod module; +mod odra_result; +pub mod prelude; +pub mod variable; + +pub use casper_event_standard; +pub use contract_context::ContractContext; +pub use contract_env::ContractEnv; +pub use entry_point_callback::EntryPointsCaller; +pub use host_context::HostContext; +pub use host_env::HostEnv; +pub use module::ModuleCaller; +pub use odra_result::OdraResult; +pub use odra_types::call_def::CallDef; diff --git a/odra-core/src/mapping.rs b/odra-core/src/mapping.rs new file mode 100644 index 00000000..b87315dd --- /dev/null +++ b/odra-core/src/mapping.rs @@ -0,0 +1,53 @@ +use crate::{ + module::{Module, ModuleWrapper}, + prelude::*, + variable::Variable, + ContractEnv +}; +use odra_types::{CLTyped, FromBytes, ToBytes}; + +pub struct Mapping { + parent_env: Rc, + phantom: core::marker::PhantomData<(K, V)>, + index: u8 +} + +impl Mapping { + pub const fn new(env: Rc, index: u8) -> Self { + Self { + parent_env: env, + phantom: core::marker::PhantomData, + index + } + } +} + +impl Mapping { + fn env_for_key(&self, key: K) -> ContractEnv { + let mut env = self.parent_env.duplicate(); + let key = key.to_bytes().unwrap_or_default(); + env.add_to_mapping_data(&key); + env + } +} + +impl Mapping { + pub fn get_or_default(&self, key: K) -> V { + let env = self.env_for_key(key); + Variable::::new(Rc::new(env), self.index).get_or_default() + } +} + +impl Mapping { + pub fn set(&mut self, key: K, value: V) { + let env = self.env_for_key(key); + Variable::::new(Rc::new(env), self.index).set(value) + } +} + +impl Mapping { + pub fn module(&self, key: K) -> ModuleWrapper { + let env = self.env_for_key(key); + ModuleWrapper::new(Rc::new(env), self.index) + } +} diff --git a/odra-core/src/module.rs b/odra-core/src/module.rs new file mode 100644 index 00000000..b5ab0e27 --- /dev/null +++ b/odra-core/src/module.rs @@ -0,0 +1,70 @@ +use core::cell::OnceCell; +use core::ops::{Deref, DerefMut}; + +use crate::contract_env::ContractEnv; +use crate::odra_result::OdraResult; +use crate::prelude::*; +use odra_types::call_def::CallDef; + +pub trait Callable { + fn call(&self, env: ContractEnv, call_def: CallDef) -> OdraResult>; +} + +#[derive(Clone)] +pub struct ModuleCaller(pub fn(env: ContractEnv, call_def: CallDef) -> OdraResult>); + +impl ModuleCaller { + pub fn new(f: fn(env: ContractEnv, call_def: CallDef) -> OdraResult>) -> Self { + Self(f) + } + pub fn call_module(&self, env: ContractEnv, call_def: CallDef) -> OdraResult> { + (self.0)(env, call_def) + } +} + +pub trait Module { + fn new(env: Rc) -> Self; + fn env(&self) -> Rc; +} + +pub struct ModuleWrapper { + env: Rc, + module: OnceCell, + index: u8 +} + +impl ModuleWrapper { + pub fn new(env: Rc, index: u8) -> Self { + Self { + env, + module: OnceCell::new(), + index + } + } + + pub fn module(&self) -> &T { + self.module + .get_or_init(|| T::new(Rc::new(self.env.child(self.index)))) + } + + pub fn module_mut(&mut self) -> &mut T { + if self.module.get().is_none() { + let _ = self.module(); + } + self.module.get_mut().unwrap() + } +} + +impl Deref for ModuleWrapper { + type Target = T; + + fn deref(&self) -> &Self::Target { + self.module() + } +} + +impl DerefMut for ModuleWrapper { + fn deref_mut(&mut self) -> &mut Self::Target { + self.module_mut() + } +} diff --git a/odra-core/src/odra_result.rs b/odra-core/src/odra_result.rs new file mode 100644 index 00000000..8ac575b1 --- /dev/null +++ b/odra-core/src/odra_result.rs @@ -0,0 +1,3 @@ +use odra_types::OdraError; + +pub type OdraResult = Result; diff --git a/odra-core/src/prelude.rs b/odra-core/src/prelude.rs new file mode 100644 index 00000000..708abcba --- /dev/null +++ b/odra-core/src/prelude.rs @@ -0,0 +1,38 @@ +#[cfg(feature = "std")] +pub use std::{borrow, boxed, format, string, vec}; + +#[cfg(feature = "std")] +pub use std::string::ToString; + +#[cfg(feature = "std")] +pub mod collections { + pub use self::{ + binary_heap::BinaryHeap, btree_map::BTreeMap, btree_set::BTreeSet, linked_list::LinkedList, + vec_deque::VecDeque, Bound + }; + pub use std::collections::*; +} + +#[cfg(feature = "std")] +pub use std::cell::RefCell; +#[cfg(feature = "std")] +pub use std::rc::Rc; + +#[cfg(not(feature = "std"))] +mod prelude { + pub use alloc::rc::Rc; + pub use core::cell::RefCell; + pub mod collections { + pub use self::{BTreeMap, BTreeSet, BinaryHeap, LinkedList, VecDeque}; + pub use alloc::collections::*; + pub use core::ops::Bound; + } + pub use crate::module::Module; + pub use alloc::string::String; + pub use alloc::vec; + pub use alloc::{borrow, boxed, format, string, string::ToString}; + pub use vec::Vec; +} + +#[cfg(not(feature = "std"))] +pub use prelude::*; diff --git a/odra-core/src/utils.rs b/odra-core/src/utils.rs new file mode 100644 index 00000000..e69de29b diff --git a/odra-core/src/variable.rs b/odra-core/src/variable.rs new file mode 100644 index 00000000..00b3dc31 --- /dev/null +++ b/odra-core/src/variable.rs @@ -0,0 +1,40 @@ +use crate::prelude::*; +use odra_types::{CLTyped, FromBytes, ToBytes}; + +use crate::contract_env::ContractEnv; + +pub struct Variable { + env: Rc, + phantom: core::marker::PhantomData, + index: u8 +} + +impl Variable { + pub fn env(&self) -> ContractEnv { + self.env.child(self.index) + } +} + +impl Variable { + pub const fn new(env: Rc, index: u8) -> Self { + Self { + env, + phantom: core::marker::PhantomData, + index + } + } +} + +impl Variable { + pub fn get_or_default(&self) -> T { + let env = self.env(); + env.get_value(&env.current_key()).unwrap_or_default() + } +} + +impl Variable { + pub fn set(&mut self, value: T) { + let env = self.env(); + env.set_value(&env.current_key(), value); + } +} diff --git a/odra-vm/Cargo.toml b/odra-vm/Cargo.toml new file mode 100644 index 00000000..5c9303a2 --- /dev/null +++ b/odra-vm/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "odra-vm" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +odra-core = { path = "../odra-core" } +odra-types = { path = "../types", features = ["test-support"] } +odra-utils = { path = "../utils"} +anyhow = "1.0.75" \ No newline at end of file diff --git a/odra-vm/src/lib.rs b/odra-vm/src/lib.rs new file mode 100644 index 00000000..e0d90e8f --- /dev/null +++ b/odra-vm/src/lib.rs @@ -0,0 +1,10 @@ +#![allow(dead_code)] +#![allow(unused_imports)] +#![allow(unused_variables)] + +mod odra_vm_contract_env; +mod odra_vm_host; +mod vm; + +pub use odra_vm_host::OdraVmHost; +pub use vm::OdraVm; diff --git a/odra-vm/src/odra_vm_contract_env.rs b/odra-vm/src/odra_vm_contract_env.rs new file mode 100644 index 00000000..24de6f06 --- /dev/null +++ b/odra-vm/src/odra_vm_contract_env.rs @@ -0,0 +1,57 @@ +use crate::vm::OdraVm; +use odra_core::prelude::*; +use odra_core::{CallDef, ContractContext}; +use odra_types::casper_types::BlockTime; +use odra_types::{casper_types, Address, Bytes, EventData, ToBytes, U512}; + +pub struct OdraVmContractEnv { + vm: Rc> +} + +impl ContractContext for OdraVmContractEnv { + fn get_value(&self, key: &[u8]) -> Option { + self.vm.borrow().get_var(key) + } + + fn set_value(&self, key: &[u8], value: Bytes) { + self.vm.borrow().set_var(key, value) + } + + fn caller(&self) -> Address { + self.vm.borrow().caller() + } + + fn self_address(&self) -> Address { + self.vm.borrow().self_address() + } + + fn call_contract(&self, address: Address, call_def: CallDef) -> Bytes { + self.vm.borrow().call_contract(address, call_def) + } + + fn get_block_time(&self) -> u64 { + self.vm.borrow().get_block_time() + } + + fn attached_value(&self) -> U512 { + self.vm.borrow().attached_value() + } + + fn emit_event(&self, event: &Bytes) { + self.vm.borrow().emit_event(event); + } + + fn transfer_tokens(&self, to: &Address, amount: &U512) { + self.vm.borrow().transfer_tokens(to, &amount) + } + + fn revert(&self, code: u16) -> ! { + todo!() + } +} + +impl OdraVmContractEnv { + pub fn new(vm: Rc>) -> Rc> { + Rc::new(RefCell::new(Self { vm })) + } +} diff --git a/odra-vm/src/odra_vm_host.rs b/odra-vm/src/odra_vm_host.rs new file mode 100644 index 00000000..7f052045 --- /dev/null +++ b/odra-vm/src/odra_vm_host.rs @@ -0,0 +1,77 @@ +use crate::odra_vm_contract_env::OdraVmContractEnv; +use crate::OdraVm; +use odra_core::entry_point_callback::EntryPointsCaller; +use odra_core::event::EventError; +use odra_core::prelude::{collections::*, *}; +use odra_core::{CallDef, ContractContext, ContractEnv, HostContext, HostEnv}; +use odra_types::{Address, Bytes, EventData, RuntimeArgs, U512}; + +pub struct OdraVmHost { + vm: Rc>, + contract_env: Rc +} + +impl HostContext for OdraVmHost { + fn set_caller(&self, caller: Address) { + self.vm.borrow().set_caller(caller) + } + + fn get_account(&self, index: usize) -> Address { + self.vm.borrow().get_account(index) + } + + fn balance_of(&self, address: &Address) -> U512 { + self.vm.borrow().balance_of(address) + } + + fn advance_block_time(&self, time_diff: u64) { + self.vm.borrow().advance_block_time_by(time_diff.into()) + } + + fn get_event(&self, contract_address: &Address, index: i32) -> Result { + self.vm.borrow().get_event(contract_address, index) + } + + fn call_contract(&self, address: &Address, call_def: CallDef, _use_proxy: bool) -> Bytes { + self.vm.borrow().call_contract(*address, call_def) + } + + fn new_contract( + &self, + name: &str, + init_args: Option, + entry_points_caller: Option + ) -> Address { + // TODO: panic in nice way + let address = self + .vm + .borrow() + .register_contract(name, entry_points_caller.unwrap()); + + if let Some(init_args) = init_args { + let _: Bytes = self.call_contract( + &address, + CallDef::new(String::from("init"), init_args), + false + ); + } + + address + } + + fn contract_env(&self) -> ContractEnv { + self.contract_env.duplicate() + } + + fn print_gas_report(&self) { + // For OdraVM there is no gas, so nothing to report. + println!("No gas report for OdraVM"); + } +} + +impl OdraVmHost { + pub fn new(vm: Rc>) -> Rc> { + let contract_env = Rc::new(ContractEnv::new(0, OdraVmContractEnv::new(vm.clone()))); + Rc::new(RefCell::new(Self { vm, contract_env })) + } +} diff --git a/odra-vm/src/vm/balance.rs b/odra-vm/src/vm/balance.rs new file mode 100644 index 00000000..ca763c16 --- /dev/null +++ b/odra-vm/src/vm/balance.rs @@ -0,0 +1,54 @@ +use anyhow::{Context, Result}; +use odra_types::casper_types::U512; + +#[derive(Eq, Hash, PartialEq, Clone, Default, Debug)] +pub struct AccountBalance { + value: U512, + prev_value: U512 +} + +impl AccountBalance { + pub fn new(amount: U512) -> Self { + Self { + value: amount, + prev_value: U512::zero() + } + } + + pub fn increase(&mut self, amount: U512) -> Result<()> { + let result = self + .value + .checked_add(amount) + .context("Addition overflow")?; + + self.prev_value = self.value; + self.value = result; + Ok(()) + } + + pub fn reduce(&mut self, amount: U512) -> Result<()> { + let result = self + .value + .checked_sub(amount) + .context("Subtraction overflow")?; + self.prev_value = self.value; + self.value = result; + Ok(()) + } + + pub fn value(&self) -> U512 { + self.value + } +} + +impl From for AccountBalance { + fn from(value: u32) -> Self { + Self::new(value.into()) + } +} + +impl From for AccountBalance { + fn from(value: u64) -> Self { + Self::new(value.into()) + } +} diff --git a/odra-vm/src/vm/callstack.rs b/odra-vm/src/vm/callstack.rs new file mode 100644 index 00000000..55da920b --- /dev/null +++ b/odra-vm/src/vm/callstack.rs @@ -0,0 +1,74 @@ +use odra_types::{casper_types::U512, Address}; + +#[derive(Clone)] +pub enum CallstackElement { + Account(Address), + Entrypoint(Entrypoint) +} + +impl CallstackElement { + pub fn address(&self) -> &Address { + match self { + CallstackElement::Account(address) => address, + CallstackElement::Entrypoint(entrypoint) => &entrypoint.address + } + } +} + +#[derive(Clone)] +pub struct Entrypoint { + pub address: Address, + pub entrypoint: String, + pub attached_value: U512 +} + +impl Entrypoint { + pub fn new(address: Address, entrypoint: &str, value: U512) -> Self { + Self { + address: address, + entrypoint: entrypoint.to_string(), + attached_value: value + } + } +} + +#[derive(Clone, Default)] +pub struct Callstack(Vec); + +impl Callstack { + pub fn pop(&mut self) -> Option { + self.0.pop() + } + + pub fn push(&mut self, element: CallstackElement) { + self.0.push(element); + } + + pub fn attached_value(&self) -> U512 { + let ce = self.0.last().unwrap(); + match ce { + CallstackElement::Account(_) => U512::zero(), + CallstackElement::Entrypoint(e) => e.attached_value + } + } + + pub fn attach_value(&mut self, amount: U512) { + if let Some(CallstackElement::Entrypoint(entrypoint)) = self.0.last_mut() { + entrypoint.attached_value = amount; + } + } + + pub fn current(&self) -> &CallstackElement { + self.0.last().expect("Not enough elements on callstack") + } + + pub fn previous(&self) -> &CallstackElement { + self.0 + .get(self.0.len() - 2) + .expect("Not enough elements on callstack") + } + + pub fn len(&self) -> usize { + self.0.len() + } +} diff --git a/odra-vm/src/vm/contract_container.rs b/odra-vm/src/vm/contract_container.rs new file mode 100644 index 00000000..dd801d68 --- /dev/null +++ b/odra-vm/src/vm/contract_container.rs @@ -0,0 +1,247 @@ +use odra_core::entry_point_callback::EntryPointsCaller; +use odra_core::prelude::{collections::*, *}; +use odra_core::HostEnv; +use odra_types::call_def::CallDef; +use odra_types::{ + casper_types::{NamedArg, RuntimeArgs}, + Bytes, OdraError, VmError +}; + +#[doc(hidden)] +pub type EntrypointCall = fn(String, &RuntimeArgs) -> Vec; +#[doc(hidden)] +pub type EntrypointArgs = Vec; + +#[derive(Clone)] +pub struct ContractContainer { + name: String, + entry_points_caller: EntryPointsCaller +} + +impl ContractContainer { + pub fn new(name: &str, entry_points_caller: EntryPointsCaller) -> Self { + Self { + name: String::from(name), + entry_points_caller + } + } + + pub fn call(&self, call_def: CallDef) -> Result { + Ok(self.entry_points_caller.call(call_def)) + // if self.constructors.get(&entrypoint).is_some() { + // return Err(OdraError::VmError(VmError::InvalidContext)); + // } + // + // match self.entrypoints.get(&entrypoint) { + // Some((ep_args, call)) => { + // self.validate_args(ep_args, args)?; + // Ok(call(self.name.clone(), args)) + // } + // None => Err(OdraError::VmError(VmError::NoSuchMethod(entrypoint))) + // } + // } + // + // pub fn call_constructor( + // &self, + // entrypoint: String, + // args: &RuntimeArgs + // ) -> Result, OdraError> { + // match self.constructors.get(&entrypoint) { + // Some((ep_args, call)) => { + // self.validate_args(ep_args, args)?; + // Ok(call(self.name.clone(), args)) + // } + // None => Err(OdraError::VmError(VmError::NoSuchMethod(entrypoint))) + // } + } + + fn _validate_args(&self, args: &[String], input_args: &RuntimeArgs) -> Result<(), OdraError> { + // TODO: What's the purpose of this code? Is it needed? + let named_args = input_args + .named_args() + .map(NamedArg::name) + .collect::>(); + + if args + .iter() + .filter(|arg| !named_args.contains(&arg.as_str())) + .map(|arg| arg.to_owned()) + .next() + .is_none() + { + Ok(()) + } else { + Err(OdraError::VmError(VmError::MissingArg)) + } + } +} + +#[cfg(test)] +mod tests { + use odra_core::prelude::{collections::*, *}; + use odra_types::{ + casper_types::{runtime_args, RuntimeArgs}, + OdraError, VmError + }; + + use crate::contract_container::{EntrypointArgs, EntrypointCall}; + + use super::ContractContainer; + + #[test] + fn test_call_wrong_entrypoint() { + // Given an instance with no entrypoints. + let instance = ContractContainer::empty(); + + // When call some entrypoint. + let result = instance.call(String::from("ep"), &RuntimeArgs::new()); + + // Then an error occurs. + assert!(result.is_err()); + } + + #[test] + fn test_call_valid_entrypoint() { + // Given an instance with a single no-args entrypoint. + let ep_name = String::from("ep"); + let instance = ContractContainer::setup_entrypoint(ep_name.clone(), vec![]); + + // When call the registered entrypoint. + let result = instance.call(ep_name, &RuntimeArgs::new()); + + // Then teh call succeeds. + assert!(result.is_ok()); + } + + #[test] + fn test_call_valid_entrypoint_with_wrong_arg_name() { + // Given an instance with a single entrypoint with one arg named "first". + let ep_name = String::from("ep"); + let instance = ContractContainer::setup_entrypoint(ep_name.clone(), vec!["first"]); + + // When call the registered entrypoint with an arg named "second". + let args = runtime_args! { "second" => 0 }; + let result = instance.call(ep_name, &args); + + // Then MissingArg error is returned. + assert_eq!(result.unwrap_err(), OdraError::VmError(VmError::MissingArg)); + } + + #[test] + fn test_call_valid_entrypoint_with_missing_arg() { + // Given an instance with a single entrypoint with one arg named "first". + let ep_name = String::from("ep"); + let instance = ContractContainer::setup_entrypoint(ep_name.clone(), vec!["first"]); + + // When call a valid entrypoint without args. + let result = instance.call(ep_name, &RuntimeArgs::new()); + + // Then MissingArg error is returned. + assert_eq!(result.unwrap_err(), OdraError::VmError(VmError::MissingArg)); + } + + #[test] + #[ignore = "At the moment is impossible to find the name of all missing args."] + fn test_all_missing_args_are_caught() { + // Given an instance with a single entrypoint with "first", "second" and "third" args. + let ep_name = String::from("ep"); + let instance = + ContractContainer::setup_entrypoint(ep_name.clone(), vec!["first", "second", "third"]); + + // When call a valid entrypoint with a single valid args, + let args = runtime_args! { "third" => 0 }; + let result = instance.call(ep_name, &args); + + // Then MissingArg error is returned with the two remaining args. + assert_eq!(result.unwrap_err(), OdraError::VmError(VmError::MissingArg)); + } + + #[test] + fn test_call_valid_constructor() { + // Given an instance with a single no-arg constructor. + let name = String::from("init"); + let instance = ContractContainer::setup_constructor(name.clone(), vec![]); + + // When call a valid constructor with a single valid args, + let result = instance.call_constructor(name, &RuntimeArgs::new()); + + // Then the call succeeds. + assert!(result.is_ok()); + } + + #[test] + fn test_call_invalid_constructor() { + // Given an instance with no constructors. + let instance = ContractContainer::empty(); + + // When try to call some constructor. + let result = instance.call_constructor(String::from("c"), &RuntimeArgs::new()); + + // Then the call fails. + assert!(result.is_err()); + } + + #[test] + fn test_call_valid_constructor_with_missing_arg() { + // Given an instance with a single constructor with one arg named "first". + let name = String::from("init"); + let instance = ContractContainer::setup_constructor(name.clone(), vec!["first"]); + + // When call a valid constructor, but with no args. + let result = instance.call_constructor(name, &RuntimeArgs::new()); + + // Then MissingArgs error is returned. + assert_eq!(result.unwrap_err(), OdraError::VmError(VmError::MissingArg)); + } + + #[test] + fn test_call_constructor_in_invalid_context() { + // Given an instance with a single constructor. + let name = String::from("init"); + let instance = ContractContainer::setup_constructor(name.clone(), vec![]); + + // When call the constructor in the entrypoint context. + let result = instance.call(name, &RuntimeArgs::new()); + + // Then the call fails. + assert!(result.is_err()); + } + + impl ContractContainer { + fn empty() -> Self { + Self { + name: String::from("contract"), + entrypoints: BTreeMap::new(), + constructors: BTreeMap::new() + } + } + + fn setup_entrypoint(ep_name: String, args: Vec<&str>) -> Self { + let call: EntrypointCall = |_, _| vec![1, 2, 3]; + let args: EntrypointArgs = args.iter().map(|arg| arg.to_string()).collect(); + + let mut entrypoints = BTreeMap::new(); + entrypoints.insert(ep_name, (args, call)); + + Self { + name: String::from("contract"), + entrypoints, + constructors: BTreeMap::new() + } + } + + fn setup_constructor(ep_name: String, args: Vec<&str>) -> Self { + let call: EntrypointCall = |_, _| vec![1, 2, 3]; + let args: EntrypointArgs = args.iter().map(|arg| arg.to_string()).collect(); + + let mut constructors = BTreeMap::new(); + constructors.insert(ep_name, (args, call)); + + Self { + name: String::from("contract"), + entrypoints: BTreeMap::new(), + constructors + } + } + } +} diff --git a/odra-vm/src/vm/contract_register.rs b/odra-vm/src/vm/contract_register.rs new file mode 100644 index 00000000..cd573b4d --- /dev/null +++ b/odra-vm/src/vm/contract_register.rs @@ -0,0 +1,45 @@ +use odra_core::prelude::{collections::*, *}; +use odra_core::HostEnv; +use odra_types::call_def::CallDef; +use odra_types::{casper_types::RuntimeArgs, Address, Bytes, OdraError, VmError}; + +use super::contract_container::ContractContainer; + +#[derive(Default)] +pub struct ContractRegister { + contracts: BTreeMap +} + +impl ContractRegister { + pub fn add(&mut self, addr: Address, container: ContractContainer) { + self.contracts.insert(addr, container); + } + + pub fn call(&self, addr: &Address, call_def: CallDef) -> Result { + // todo: make it better + self.contracts.get(addr).unwrap().call(call_def) + } + + // pub fn call_constructor( + // &self, + // addr: &Address, + // entrypoint: String, + // args: &RuntimeArgs + // ) -> Result, OdraError> { + // self.internal_call(addr, |container| { + // container.call_constructor(entrypoint, args) + // }) + // } + // + // fn internal_call Result, OdraError>>( + // &self, + // addr: &Address, + // call_fn: F + // ) -> Result, OdraError> { + // let contract = self.contracts.get(addr); + // match contract { + // Some(container) => call_fn(container), + // None => Err(OdraError::VmError(VmError::InvalidContractAddress)) + // } + // } +} diff --git a/odra-vm/src/vm/mod.rs b/odra-vm/src/vm/mod.rs new file mode 100644 index 00000000..dc2fc7ad --- /dev/null +++ b/odra-vm/src/vm/mod.rs @@ -0,0 +1,12 @@ +#![allow(dead_code)] +#![allow(unused_imports)] +#![allow(unused_variables)] +mod balance; +mod callstack; +mod contract_container; +mod contract_register; +mod odra_vm; +mod odra_vm_state; +mod storage; + +pub use odra_vm::OdraVm; diff --git a/odra-vm/src/vm/odra_vm.rs b/odra-vm/src/vm/odra_vm.rs new file mode 100644 index 00000000..5ee36238 --- /dev/null +++ b/odra-vm/src/vm/odra_vm.rs @@ -0,0 +1,563 @@ +use std::cell::RefCell; +use std::rc::Rc; +use std::sync::{Arc, RwLock}; + +use anyhow::Result; +use odra_core::entry_point_callback::EntryPointsCaller; +use odra_core::event::EventError; +use odra_types::call_def::CallDef; +use odra_types::{ + casper_types::{ + bytesrepr::{FromBytes, ToBytes}, + U512 + }, + Address, Bytes, ExecutionError, PublicKey +}; +use odra_types::{OdraError, VmError}; + +use super::callstack::{CallstackElement, Entrypoint}; +use super::contract_container::ContractContainer; +use super::contract_register::ContractRegister; +use super::odra_vm_state::OdraVmState; + +#[derive(Default)] +pub struct OdraVm { + state: Arc>, + contract_register: Arc> +} + +impl OdraVm { + pub fn new() -> Rc> { + Rc::new(RefCell::new(Self::default())) + } + + pub fn register_contract(&self, name: &str, entry_points_caller: EntryPointsCaller) -> Address { + // Create a new address. + let address = { self.state.write().unwrap().next_contract_address() }; + // Register new contract under the new address. + { + let contract_namespace = self.state.read().unwrap().get_contract_namespace(); + let contract = ContractContainer::new(&contract_namespace, entry_points_caller); + self.contract_register + .write() + .unwrap() + .add(address, contract); + self.state + .write() + .unwrap() + .set_balance(address, U512::zero()); + } + + // Call constructor if needed. + // if let Some(constructor) = constructor { + // let (constructor_name, args, _) = constructor; + // self.call_constructor(address, &constructor_name, args); + // } + address + } + + pub fn call_contract(&self, address: Address, call_def: CallDef) -> Bytes { + self.prepare_call(address, &call_def.entry_point, call_def.amount); + // Call contract from register. + if call_def.amount > U512::zero() { + let status = self.checked_transfer_tokens(&self.caller(), &address, &call_def.amount); + if let Err(err) = status { + self.revert(err.clone()); + panic!("{:?}", err); + } + } + + let result = self + .contract_register + .read() + .unwrap() + .call(&address, call_def); + + self.handle_call_result(result) + } + + // fn call_constructor(&self, address: Address, entrypoint: &str, args: &RuntimeArgs) -> Vec { + // self.prepare_call(address, entrypoint, None); + // // Call contract from register. + // let register = self.contract_register.read().unwrap(); + // let result = register.call_constructor(&address, String::from(entrypoint), args); + // self.handle_call_result(result) + // } + + fn prepare_call(&self, address: Address, entrypoint: &str, amount: U512) { + let mut state = self.state.write().unwrap(); + // If only one address on the call_stack, record snapshot. + if state.is_in_caller_context() { + state.take_snapshot(); + state.clear_error(); + } + // Put the address on stack. + + let element = CallstackElement::Entrypoint(Entrypoint::new(address, entrypoint, amount)); + state.push_callstack_element(element); + } + + fn handle_call_result(&self, result: Result) -> Bytes { + let mut state = self.state.write().unwrap(); + let result = match result { + Ok(data) => data, + Err(err) => { + state.set_error(err); + Bytes::new() + } + }; + + // Drop the address from stack. + state.pop_callstack_element(); + + if state.error.is_none() { + // If only one address on the call_stack, drop the snapshot + if state.is_in_caller_context() { + state.drop_snapshot(); + } + result + } else { + // If only one address on the call_stack an an error occurred, restore the snapshot + if state.is_in_caller_context() { + state.restore_snapshot(); + }; + Bytes::new() + } + } + + pub fn revert(&self, error: OdraError) { + let mut state = self.state.write().unwrap(); + state.set_error(error); + state.clear_callstack(); + if state.is_in_caller_context() { + state.restore_snapshot(); + } + } + + pub fn error(&self) -> Option { + self.state.read().unwrap().error() + } + + pub fn get_backend_name(&self) -> String { + self.state.read().unwrap().get_backend_name() + } + + /// Returns the callee, i.e. the currently executing contract. + pub fn self_address(&self) -> Address { + self.state.read().unwrap().callee() + } + + pub fn caller(&self) -> Address { + self.state.read().unwrap().caller() + } + + pub fn callstack_tip(&self) -> CallstackElement { + self.state.read().unwrap().callstack_tip().clone() + } + + pub fn set_caller(&self, caller: Address) { + self.state.write().unwrap().set_caller(caller); + } + + pub fn set_var(&self, key: &[u8], value: Bytes) { + self.state.write().unwrap().set_var(key, value); + } + + pub fn get_var(&self, key: &[u8]) -> Option { + let result = { self.state.read().unwrap().get_var(key) }; + match result { + Ok(result) => result, + Err(error) => { + self.state + .write() + .unwrap() + .set_error(Into::::into(error)); + None + } + } + } + + pub fn set_dict_value(&self, dict: &[u8], key: &[u8], value: Bytes) { + self.state.write().unwrap().set_dict_value(dict, key, value); + } + + pub fn get_dict_value(&self, dict: &[u8], key: &[u8]) -> Option { + let result = { self.state.read().unwrap().get_dict_value(dict, key) }; + match result { + Ok(result) => result, + Err(error) => { + self.state + .write() + .unwrap() + .set_error(Into::::into(error)); + None + } + } + } + + pub fn emit_event(&self, event_data: &Bytes) { + self.state.write().unwrap().emit_event(event_data); + } + + pub fn get_event(&self, address: &Address, index: i32) -> Result { + self.state.read().unwrap().get_event(address, index) + } + + pub fn attach_value(&self, amount: U512) { + self.state.write().unwrap().attach_value(amount); + } + + pub fn get_block_time(&self) -> u64 { + self.state.read().unwrap().block_time() + } + + pub fn advance_block_time_by(&self, milliseconds: u64) { + self.state + .write() + .unwrap() + .advance_block_time_by(milliseconds) + } + + pub fn attached_value(&self) -> U512 { + self.state.read().unwrap().attached_value() + } + + pub fn get_account(&self, n: usize) -> Address { + self.state.read().unwrap().accounts.get(n).cloned().unwrap() + } + + pub fn balance_of(&self, address: &Address) -> U512 { + self.state.read().unwrap().balance_of(address) + } + + pub fn transfer_tokens(&self, to: &Address, amount: &U512) { + if amount.is_zero() { + return; + } + + let from = &self.self_address(); + + let mut state = self.state.write().unwrap(); + if state.reduce_balance(from, amount).is_err() { + self.revert(OdraError::VmError(VmError::BalanceExceeded)) + } + if state.increase_balance(to, amount).is_err() { + self.revert(OdraError::VmError(VmError::BalanceExceeded)) + } + } + + pub fn checked_transfer_tokens( + &self, + from: &Address, + to: &Address, + amount: &U512 + ) -> Result<(), OdraError> { + if amount.is_zero() { + return Ok(()); + } + + let mut state = self.state.write().unwrap(); + if state.reduce_balance(from, amount).is_err() { + return Err(OdraError::VmError(VmError::BalanceExceeded)); + } + if state.increase_balance(to, amount).is_err() { + return Err(OdraError::VmError(VmError::BalanceExceeded)); + } + + Ok(()) + } + + pub fn self_balance(&self) -> U512 { + let address = self.self_address(); + self.state.read().unwrap().balance_of(&address) + } + + pub fn public_key(&self, address: &Address) -> PublicKey { + self.state.read().unwrap().public_key(address) + } +} + +#[cfg(test)] +mod tests { + use std::collections::BTreeMap; + + use crate::vm::contract_container::{EntrypointArgs, EntrypointCall}; + use odra_types::casper_types::bytesrepr::FromBytes; + use odra_types::casper_types::{RuntimeArgs, U512}; + use odra_types::OdraAddress; + use odra_types::{Address, EventData}; + use odra_types::{ExecutionError, OdraError, VmError}; + + use super::OdraVm; + + #[test] + fn contracts_have_different_addresses() { + // given a new instance + let instance = OdraVm::default(); + // when register two contracts with the same entrypoints + let entrypoint: Vec<(String, (EntrypointArgs, EntrypointCall))> = + vec![(String::from("abc"), (vec![], |_, _| vec![]))]; + let entrypoints = entrypoint.into_iter().collect::>(); + let constructors = BTreeMap::new(); + + let address1 = instance.register_contract(None, constructors.clone(), entrypoints.clone()); + let address2 = instance.register_contract(None, constructors, entrypoints); + + // then addresses are different + assert_ne!(address1, address2); + } + + #[test] + fn addresses_have_different_type() { + // given an address of a contract and an address of an account + let instance = OdraVm::default(); + let (contract_address, _, _) = setup_contract(&instance); + let account_address = instance.get_account(0); + + // Then the contract address is a contract + assert!(contract_address.is_contract()); + // And the account address is not a contract + assert!(!account_address.is_contract()); + } + + #[test] + fn test_contract_call() { + // given an instance with a registered contract having one entrypoint + let instance = OdraVm::default(); + + let (contract_address, entrypoint, call_result) = setup_contract(&instance); + + // when call an existing entrypoint + let result = + instance.call_contract::(contract_address, &entrypoint, &RuntimeArgs::new(), None); + + // then returns the expected value + assert_eq!(result, call_result); + } + + #[test] + fn test_call_non_existing_contract() { + // given an empty vm + let instance = OdraVm::default(); + + let address = Address::contract_from_u32(42); + + // when call a contract + instance.call_contract::<()>(address, "abc", &RuntimeArgs::new(), None); + + // then the vm is in error state + assert_eq!( + instance.error(), + Some(OdraError::VmError(VmError::InvalidContractAddress)) + ); + } + + #[test] + fn test_call_non_existing_entrypoint() { + // given an instance with a registered contract having one entrypoint + let instance = OdraVm::default(); + let (contract_address, entrypoint, _) = setup_contract(&instance); + + // when call non-existing entrypoint + let invalid_entrypoint = entrypoint.chars().take(1).collect::(); + instance.call_contract::<()>( + contract_address, + &invalid_entrypoint, + &RuntimeArgs::new(), + None + ); + + // then the vm is in error state + assert_eq!( + instance.error(), + Some(OdraError::VmError(VmError::NoSuchMethod( + invalid_entrypoint + ))) + ); + } + + #[test] + fn test_caller_switching() { + // given an empty instance + let instance = OdraVm::default(); + + // when set a new caller + let new_caller = Address::account_from_str("ff"); + instance.set_caller(new_caller); + // put a fake contract on stack + push_address(&instance, &new_caller); + + // then the caller is set + assert_eq!(instance.caller(), new_caller); + } + + #[test] + fn test_revert() { + // given an empty instance + let instance = OdraVm::default(); + + // when revert + instance.revert(ExecutionError::new(1, "err").into()); + + // then an error is set + assert_eq!(instance.error(), Some(ExecutionError::new(1, "err").into())); + } + + #[test] + fn test_read_write_value() { + // given an empty instance + let instance = OdraVm::default(); + + // when set a value + let key = b"key"; + let value = 32u8; + instance.set_var(key, value); + + // then the value can be read + assert_eq!(instance.get_var(key), Some(value)); + // then the value under unknown key does not exist + assert_eq!(instance.get_var::<()>(b"other_key"), None); + } + + #[test] + fn test_read_write_dict() { + // given an empty instance + let instance = OdraVm::default(); + + // when set a value + let dict = b"dict"; + let key: [u8; 2] = [1, 2]; + let value = 32u8; + instance.set_dict_value(dict, &key, value); + + // then the value can be read + assert_eq!(instance.get_dict_value(dict, &key), Some(value)); + // then the value under the key in unknown dict does not exist + assert_eq!(instance.get_dict_value::<()>(b"other_dict", &key), None); + // then the value under unknown key does not exist + assert_eq!(instance.get_dict_value::<()>(dict, &[]), None); + } + + #[test] + fn events() { + // given an empty instance + let instance = OdraVm::default(); + + let first_contract_address = Address::account_from_str("abc"); + // put a contract on stack + push_address(&instance, &first_contract_address); + + let first_event: EventData = vec![1, 2, 3]; + let second_event: EventData = vec![4, 5, 6]; + instance.emit_event(&first_event); + instance.emit_event(&second_event); + + let second_contract_address = Address::account_from_str("bca"); + // put a next contract on stack + push_address(&instance, &second_contract_address); + + let third_event: EventData = vec![7, 8, 9]; + let fourth_event: EventData = vec![11, 22, 33]; + instance.emit_event(&third_event); + instance.emit_event(&fourth_event); + + assert_eq!( + instance.get_event(first_contract_address, 0), + Ok(first_event) + ); + assert_eq!( + instance.get_event(first_contract_address, 1), + Ok(second_event) + ); + + assert_eq!( + instance.get_event(second_contract_address, 0), + Ok(third_event) + ); + assert_eq!( + instance.get_event(second_contract_address, 1), + Ok(fourth_event) + ); + } + + #[test] + fn test_current_contract_address() { + // given an empty instance + let instance = OdraVm::default(); + + // when push a contract into the stack + let contract_address = Address::contract_from_u32(100); + push_address(&instance, &contract_address); + + // then the contract address in the callee + assert_eq!(instance.self_address(), contract_address); + } + + #[test] + fn test_call_contract_with_amount() { + // given an instance with a registered contract having one entrypoint + let instance = OdraVm::default(); + let (contract_address, entrypoint_name, _) = setup_contract(&instance); + + // when call a contract with the whole balance of the caller + let caller = instance.get_account(0); + let caller_balance = instance.token_balance(caller); + + instance.call_contract::( + contract_address, + &entrypoint_name, + &RuntimeArgs::new(), + Some(caller_balance) + ); + + // then the contract has the caller tokens and the caller balance is zero + assert_eq!(instance.token_balance(contract_address), caller_balance); + assert_eq!(instance.token_balance(caller), U512::zero()); + } + + #[test] + #[should_panic(expected = "VmError(BalanceExceeded)")] + fn test_call_contract_with_amount_exceeding_balance() { + // given an instance with a registered contract having one entrypoint + let instance = OdraVm::default(); + let (contract_address, entrypoint_name, _) = setup_contract(&instance); + + let caller = instance.get_account(0); + let caller_balance = instance.token_balance(caller); + + // when call a contract with the amount exceeding caller's balance + instance.call_contract::<()>( + contract_address, + &entrypoint_name, + &RuntimeArgs::new(), + Some(caller_balance + 1) + ); + } + + fn push_address(vm: &OdraVm, address: &Address) { + let element = CallstackElement::Account(*address); + vm.state.write().unwrap().push_callstack_element(element); + } + + fn setup_contract(instance: &OdraVm) -> (Address, String, u32) { + let entrypoint_name = "abc"; + let result = vec![1, 1, 0, 0]; + + let entrypoint: Vec<(String, (EntrypointArgs, EntrypointCall))> = vec![( + String::from(entrypoint_name), + (vec![], |_, _| vec![1, 1, 0, 0]) + )]; + let constructors = BTreeMap::new(); + let contract_address = instance.register_contract( + None, + constructors, + entrypoint.into_iter().collect::>() + ); + + ( + contract_address, + String::from(entrypoint_name), + ::from_vec(result).unwrap().0 + ) + } +} diff --git a/odra-vm/src/vm/odra_vm_state.rs b/odra-vm/src/vm/odra_vm_state.rs new file mode 100644 index 00000000..dbb30123 --- /dev/null +++ b/odra-vm/src/vm/odra_vm_state.rs @@ -0,0 +1,244 @@ +use super::balance::AccountBalance; +use super::callstack::{Callstack, CallstackElement}; +use super::storage::Storage; +use anyhow::Result; +use odra_core::event::EventError; +use odra_types::casper_types::account::AccountHash; +use odra_types::casper_types::bytesrepr::Error; +use odra_types::{ + Address, Bytes, EventData, ExecutionError, FromBytes, OdraError, PublicKey, SecretKey, ToBytes, + U512 +}; +use std::collections::BTreeMap; + +pub struct OdraVmState { + storage: Storage, + callstack: Callstack, + events: BTreeMap>, + contract_counter: u32, + pub error: Option, + block_time: u64, + pub accounts: Vec
, + key_pairs: BTreeMap +} + +impl OdraVmState { + pub fn get_backend_name(&self) -> String { + "MockVM".to_string() + } + + pub fn callee(&self) -> Address { + *self.callstack.current().address() + } + + pub fn caller(&self) -> Address { + *self.callstack.previous().address() + } + + pub fn callstack_tip(&self) -> &CallstackElement { + self.callstack.current() + } + + pub fn set_caller(&mut self, address: Address) { + self.pop_callstack_element(); + self.push_callstack_element(CallstackElement::Account(address)); + } + + pub fn set_var(&mut self, key: &[u8], value: Bytes) { + let ctx = self.callstack.current().address(); + if let Err(error) = self.storage.set_value(ctx, key, value) { + self.set_error(Into::::into(error)); + } + } + + pub fn get_var(&self, key: &[u8]) -> Result, Error> { + let ctx = self.callstack.current().address(); + self.storage.get_value(ctx, key) + } + + pub fn set_dict_value(&mut self, dict: &[u8], key: &[u8], value: Bytes) { + let ctx = self.callstack.current().address(); + if let Err(error) = self.storage.insert_dict_value(ctx, dict, key, value) { + self.set_error(Into::::into(error)); + } + } + + pub fn get_dict_value(&self, dict: &[u8], key: &[u8]) -> Result, Error> { + let ctx = &self.callstack.current().address(); + self.storage.get_dict_value(ctx, dict, key) + } + + pub fn emit_event(&mut self, event_data: &Bytes) { + let contract_address = self.callstack.current().address(); + let events = self.events.get_mut(contract_address).map(|events| { + events.push(event_data.clone()); + events + }); + if events.is_none() { + self.events + .insert(*contract_address, vec![event_data.clone()]); + } + } + + pub fn get_event(&self, address: &Address, index: i32) -> Result { + let events = self.events.get(address); + if events.is_none() { + return Err(EventError::IndexOutOfBounds); + } + let events: &Vec = events.unwrap(); + let event_position = odra_utils::event_absolute_position(events.len(), index) + .ok_or(EventError::IndexOutOfBounds)?; + // TODO: Make following line go away by passing ToBytes insted of Bytes to event. + let event = events.get(event_position).ok_or(EventError::IndexOutOfBounds)?; + Ok(Bytes::from(event.to_bytes().unwrap())) + } + + pub fn attach_value(&mut self, amount: U512) { + self.callstack.attach_value(amount); + } + + pub fn push_callstack_element(&mut self, element: CallstackElement) { + self.callstack.push(element); + } + + pub fn pop_callstack_element(&mut self) { + self.callstack.pop(); + } + + pub fn clear_callstack(&mut self) { + let mut element = self.callstack.pop(); + while element.is_some() { + let new_element = self.callstack.pop(); + if new_element.is_none() { + self.callstack.push(element.unwrap()); + return; + } + element = new_element; + } + } + + pub fn next_contract_address(&mut self) -> Address { + self.contract_counter += 1; + Address::contract_from_u32(self.contract_counter) + } + + pub fn get_contract_namespace(&self) -> String { + self.contract_counter.to_string() + } + + pub fn set_error(&mut self, error: E) + where + E: Into + { + if self.error.is_none() { + self.error = Some(error.into()); + } + } + + pub fn attached_value(&self) -> U512 { + self.callstack.attached_value() + } + + pub fn clear_error(&mut self) { + self.error = None; + } + + pub fn error(&self) -> Option { + self.error.clone() + } + + pub fn is_in_caller_context(&self) -> bool { + self.callstack.len() == 1 + } + + pub fn take_snapshot(&mut self) { + self.storage.take_snapshot(); + } + + pub fn drop_snapshot(&mut self) { + self.storage.drop_snapshot(); + } + + pub fn restore_snapshot(&mut self) { + self.storage.restore_snapshot(); + } + + pub fn block_time(&self) -> u64 { + self.block_time + } + + pub fn advance_block_time_by(&mut self, milliseconds: u64) { + self.block_time += milliseconds; + } + + pub fn balance_of(&self, address: &Address) -> U512 { + self.storage + .balance_of(address) + .map(|b| b.value()) + .unwrap_or_default() + } + + pub fn all_balances(&self) -> Vec { + self.storage + .balances + .iter() + .fold(Vec::new(), |mut acc, (_, balance)| { + acc.push(balance.clone()); + acc + }) + } + + pub fn set_balance(&mut self, address: Address, amount: U512) { + self.storage + .set_balance(address, AccountBalance::new(amount)); + } + + pub fn increase_balance(&mut self, address: &Address, amount: &U512) -> Result<()> { + self.storage.increase_balance(address, amount) + } + + pub fn reduce_balance(&mut self, address: &Address, amount: &U512) -> Result<()> { + self.storage.reduce_balance(address, amount) + } + + pub fn public_key(&self, address: &Address) -> PublicKey { + let (_, public_key) = self.key_pairs.get(address).unwrap(); + public_key.clone() + } +} + +impl Default for OdraVmState { + fn default() -> Self { + let mut addresses: Vec
= Vec::new(); + let mut key_pairs = BTreeMap::::new(); + for i in 0..20 { + // Create keypair. + let secret_key = SecretKey::ed25519_from_bytes([i; 32]).unwrap(); + let public_key = PublicKey::from(&secret_key); + + // Create an AccountHash from a public key. + let account_addr = AccountHash::from(&public_key); + + addresses.push(account_addr.try_into().unwrap()); + key_pairs.insert(account_addr.try_into().unwrap(), (secret_key, public_key)); + } + + let mut balances = BTreeMap::::new(); + for address in addresses.clone() { + balances.insert(address, 100_000_000_000_000_000u64.into()); + } + + let mut backend = OdraVmState { + storage: Storage::new(balances), + callstack: Default::default(), + events: Default::default(), + contract_counter: 0, + error: None, + block_time: 0, + accounts: addresses.clone(), + key_pairs + }; + backend.push_callstack_element(CallstackElement::Account(*addresses.first().unwrap())); + backend + } +} diff --git a/odra-vm/src/vm/storage.rs b/odra-vm/src/vm/storage.rs new file mode 100644 index 00000000..231b3da1 --- /dev/null +++ b/odra-vm/src/vm/storage.rs @@ -0,0 +1,303 @@ +use anyhow::{Context, Result}; +use odra_types::{ + casper_types::{ + bytesrepr::{Error, FromBytes, ToBytes}, + U512 + }, + Address, Bytes +}; +use std::{ + collections::{hash_map::DefaultHasher, BTreeMap}, + hash::{Hash, Hasher} +}; + +use super::balance::AccountBalance; + +#[derive(Default, Clone)] +pub struct Storage { + state: BTreeMap, + pub balances: BTreeMap, + state_snapshot: Option>, + balances_snapshot: Option> +} + +impl Storage { + pub fn new(balances: BTreeMap) -> Self { + Self { + state: Default::default(), + balances, + state_snapshot: Default::default(), + balances_snapshot: Default::default() + } + } + + pub fn balance_of(&self, address: &Address) -> Option<&AccountBalance> { + self.balances.get(address) + } + + pub fn set_balance(&mut self, address: Address, balance: AccountBalance) { + self.balances.insert(address, balance); + } + + pub fn increase_balance(&mut self, address: &Address, amount: &U512) -> Result<()> { + let balance = self.balances.get_mut(address).context("Unknown address")?; + balance.increase(*amount) + } + + pub fn reduce_balance(&mut self, address: &Address, amount: &U512) -> Result<()> { + let balance = self.balances.get_mut(address).context("Unknown address")?; + balance.reduce(*amount) + } + + pub fn get_value(&self, address: &Address, key: &[u8]) -> Result, Error> { + let hash = Storage::hashed_key(address, key); + let result = self.state.get(&hash).cloned(); + + match result { + Some(res) => Ok(Some(res)), + None => Ok(None) + } + } + + pub fn set_value(&mut self, address: &Address, key: &[u8], value: Bytes) -> Result<(), Error> { + let hash = Storage::hashed_key(address, key); + self.state.insert(hash, value); + Ok(()) + } + + pub fn insert_dict_value( + &mut self, + address: &Address, + collection: &[u8], + key: &[u8], + value: Bytes + ) -> Result<(), Error> { + let dict_key = [collection, key].concat(); + let hash = Storage::hashed_key(address, dict_key); + self.state.insert(hash, value); + Ok(()) + } + + pub fn get_dict_value( + &self, + address: &Address, + collection: &[u8], + key: &[u8] + ) -> Result, Error> { + let dict_key = [collection, key].concat(); + let hash = Storage::hashed_key(address, dict_key); + let result = self.state.get(&hash).cloned(); + + Ok(result) + } + + pub fn take_snapshot(&mut self) { + self.state_snapshot = Some(self.state.clone()); + self.balances_snapshot = Some(self.balances.clone()); + } + + pub fn drop_snapshot(&mut self) { + self.state_snapshot = None; + self.balances_snapshot = None; + } + + pub fn restore_snapshot(&mut self) { + if let Some(snapshot) = self.state_snapshot.clone() { + self.state = snapshot; + self.state_snapshot = None; + }; + if let Some(snapshot) = self.balances_snapshot.clone() { + self.balances = snapshot; + self.balances_snapshot = None; + }; + } + + fn hashed_key(address: &Address, key: H) -> u64 { + let mut hasher = DefaultHasher::new(); + address.hash(&mut hasher); + key.hash(&mut hasher); + hasher.finish() + } +} + +#[cfg(test)] +mod test { + + use odra_types::Address; + + use super::Storage; + + fn setup() -> (Address, [u8; 3], u8) { + let address = Address::account_from_str("add"); + let key = b"key"; + let value = 88u8; + + (address, *key, value) + } + + #[test] + fn read_write_single_value() { + // given an empty storage + let mut storage = Storage::default(); + let (address, key, value) = setup(); + + // when put a value + storage.set_value(&address, &key, value).unwrap(); + + // then the value can be read + assert_eq!(storage.get_value(&address, &key).unwrap(), Some(value)); + } + + #[test] + fn override_single_value() { + // given a storage with some stored value + let mut storage = Storage::default(); + let (address, key, value) = setup(); + storage.set_value(&address, &key, value).unwrap(); + + // when the next value is set under the same key + let next_value = String::from("new_value"); + storage + .set_value(&address, &key, next_value.clone()) + .unwrap(); + + // then the previous value is replaced + assert_eq!(storage.get_value(&address, &key).unwrap(), Some(next_value)); + } + + #[test] + fn read_non_existing_key_returns_none() { + // given an empty storage + let storage = Storage::default(); + let (address, key, _) = setup(); + + // when lookup a key + let result: Option<()> = storage.get_value(&address, &key).unwrap(); + + // then the None value is returned + assert_eq!(result, None); + } + + #[test] + fn read_write_dict_value() { + // given an empty storage + let mut storage = Storage::default(); + let (address, key, value) = setup(); + let collection = b"dict"; + + // when put a value into a collection + storage + .insert_dict_value(&address, collection, &key, value) + .unwrap(); + + // then the value can be read + assert_eq!( + storage.get_dict_value(&address, collection, &key).unwrap(), + Some(value) + ); + } + + #[test] + fn read_from_non_existing_collection_returns_none() { + // given storage with some stored value + let mut storage = Storage::default(); + let (address, key, value) = setup(); + let collection = b"dict"; + storage + .insert_dict_value(&address, collection, &key, value) + .unwrap(); + + // when read a value from a non exisiting collection + let non_existing_collection = b"collection"; + let result: Option<()> = storage + .get_dict_value(&address, non_existing_collection, &key) + .unwrap(); + + // then None is returned + assert_eq!(result, None); + } + + #[test] + fn read_from_non_existing_key_from_existing_collection_returns_none() { + // given storage with some stored value + let mut storage = Storage::default(); + let (address, key, value) = setup(); + let collection = b"dict"; + storage + .insert_dict_value(&address, collection, &key, value) + .unwrap(); + + // when read a value from a non existing collection + let non_existing_key = [2u8]; + let result: Option<()> = storage + .get_dict_value(&address, collection, &non_existing_key) + .unwrap(); + + // then None is returned + assert_eq!(result, None); + } + + #[test] + fn restore_snapshot() { + // given storage with some state and a snapshot of the previous state + let mut storage = Storage::default(); + let (address, key, initial_value) = setup(); + storage.set_value(&address, &key, initial_value).unwrap(); + storage.take_snapshot(); + let next_value = String::from("next_value"); + storage.set_value(&address, &key, next_value).unwrap(); + + // when restore the snapshot + storage.restore_snapshot(); + + // then the changes are reverted + assert_eq!( + storage.get_value(&address, &key).unwrap(), + Some(initial_value) + ); + // the snapshot is removed + assert_eq!(storage.state_snapshot, None); + } + + #[test] + fn test_snapshot_override() { + // given storage with some state and a snapshot of the previous state + let mut storage = Storage::default(); + let (address, key, initial_value) = setup(); + let second_value = 2_000u32; + let third_value = 3_000u32; + storage.set_value(&address, &key, initial_value).unwrap(); + storage.take_snapshot(); + storage.set_value(&address, &key, second_value).unwrap(); + + // when take another snapshot and restore it + storage.take_snapshot(); + storage.set_value(&address, &key, third_value).unwrap(); + storage.restore_snapshot(); + + // then the most recent snapshot is restored + assert_eq!( + storage.get_value(&address, &key).unwrap(), + Some(second_value), + ); + } + + #[test] + fn drop_snapshot() { + // given storage with some state and a snapshot of the previous state + let mut storage = Storage::default(); + let (address, key, initial_value) = setup(); + let next_value = 1_000u32; + storage.set_value(&address, &key, initial_value).unwrap(); + storage.take_snapshot(); + storage.set_value(&address, &key, next_value).unwrap(); + + // when the snapshot is dropped + storage.drop_snapshot(); + + // then storage state does not change + assert_eq!(storage.get_value(&address, &key).unwrap(), Some(next_value),); + // the snapshot is wiped out + assert_eq!(storage.state_snapshot, None); + } +} diff --git a/odra2/Cargo.toml b/odra2/Cargo.toml new file mode 100644 index 00000000..fc7738eb --- /dev/null +++ b/odra2/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "odra2" +edition = "2021" +version.workspace = true +authors.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true + +[dependencies] +odra-core = { path = "../odra-core" } +odra-types = { path = "../types" } +odra-proc-macros = { path = "../lang/proc-macros" } + +[target.'cfg(target_arch = "wasm32")'.dependencies] +odra-casper-wasm-env = { path = "../odra-casper/wasm-env" } + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +odra-casper-test-vm = { path = "../odra-casper/test-vm" } +odra-vm = { path = "../odra-vm" } diff --git a/odra2/src/lib.rs b/odra2/src/lib.rs new file mode 100644 index 00000000..612d751c --- /dev/null +++ b/odra2/src/lib.rs @@ -0,0 +1,48 @@ +#![no_std] + +extern crate alloc; + +pub use odra_core::casper_event_standard; +pub use odra_core::casper_event_standard::Event; +pub use odra_core::event; +pub use odra_core::EntryPointsCaller; +pub use odra_core::{ + mapping::Mapping, module, module::ModuleWrapper, prelude, variable::Variable, CallDef, + ContractEnv, HostEnv +}; +pub use odra_types as types; + +#[cfg(target_arch = "wasm32")] +pub use odra_casper_wasm_env; + +#[cfg(not(target_arch = "wasm32"))] +mod odra_test { + use odra_casper_test_vm::{CasperHost, CasperVm}; + use odra_core::prelude::String; + use odra_core::HostEnv; + use odra_vm::{OdraVm, OdraVmHost}; + + pub fn test_env() -> odra_core::HostEnv { + extern crate std; + let backend: String = std::env::var("ODRA_BACKEND").unwrap_or(String::from("odra-vm")); + match backend.as_str() { + "casper" => casper_env(), + _ => odra_env() + } + } + + fn casper_env() -> HostEnv { + let vm = CasperVm::new(); + let host_env = CasperHost::new(vm); + HostEnv::new(host_env) + } + + fn odra_env() -> HostEnv { + let vm = OdraVm::new(); + let host_env = OdraVmHost::new(vm); + HostEnv::new(host_env) + } +} + +#[cfg(not(target_arch = "wasm32"))] +pub use odra_test::*; diff --git a/types/Cargo.toml b/types/Cargo.toml index 4fdf2c7e..4b21d880 100644 --- a/types/Cargo.toml +++ b/types/Cargo.toml @@ -18,4 +18,5 @@ odra-utils = { path = "../utils", version = "0.7.0" } [features] default = [] +std = [] test-support = [] \ No newline at end of file diff --git a/types/src/call_def.rs b/types/src/call_def.rs new file mode 100644 index 00000000..fb33a1eb --- /dev/null +++ b/types/src/call_def.rs @@ -0,0 +1,44 @@ +use alloc::string::String; +use casper_types::bytesrepr::FromBytes; +use casper_types::{CLTyped, RuntimeArgs, U512}; + +#[derive(Clone, Debug)] +pub struct CallDef { + pub entry_point: String, + pub args: RuntimeArgs, + pub amount: U512 +} + +impl CallDef { + pub fn new(method: String, args: RuntimeArgs) -> Self { + CallDef { + entry_point: method, + args, + amount: U512::zero() + } + } + + pub fn with_amount(mut self, amount: U512) -> Self { + self.amount = amount; + self + } + + pub fn args(&self) -> &RuntimeArgs { + &self.args + } + + pub fn get(&self, name: &str) -> Option { + self.args + .get(name) + .map(|v| v.clone().into_t().ok()) + .flatten() + } + + pub fn method(&self) -> &str { + &self.entry_point + } + + pub fn attached_value(&self) -> U512 { + self.amount + } +} diff --git a/types/src/contract_def.rs b/types/src/contract_def.rs index b8a4de57..6546027e 100644 --- a/types/src/contract_def.rs +++ b/types/src/contract_def.rs @@ -90,6 +90,11 @@ pub struct ContractBlueprint { pub fqn: &'static str } +#[derive(Debug, Clone)] +pub struct ContractBlueprint2 { + pub name: String +} + #[cfg(test)] #[allow(dead_code)] mod test { diff --git a/types/src/error.rs b/types/src/error.rs index ae848fa6..af4c62ea 100644 --- a/types/src/error.rs +++ b/types/src/error.rs @@ -20,6 +20,7 @@ const CODE_INDEX_OUT_OF_BOUNDS: u16 = 108; const CODE_ZERO_ADDRESS: u16 = 109; const CODE_ADDRESS_CREATION_FAILED: u16 = 110; const CODE_SERIALIZATION_FAILED: u16 = 111; +const CODE_KEY_NOT_FOUND: u16 = 112; /// General error type in Odra framework #[derive(Clone, Debug, PartialEq)] @@ -161,6 +162,10 @@ impl ExecutionError { pub fn address_creation_failed() -> Self { Self::sys(CODE_ADDRESS_CREATION_FAILED, "Address creation failed") } + + pub fn key_not_found() -> Self { + Self::sys(CODE_KEY_NOT_FOUND, "Key not found") + } } impl PartialEq for ExecutionError { diff --git a/types/src/lib.rs b/types/src/lib.rs index a3346ef8..f187aa8e 100644 --- a/types/src/lib.rs +++ b/types/src/lib.rs @@ -4,24 +4,25 @@ extern crate alloc; -mod address; +pub mod address; pub mod arithmetic; -#[cfg(not(target_arch = "wasm32"))] +pub mod call_def; pub mod contract_def; mod error; -pub mod event; -mod item; pub mod uints; +mod item; use alloc::vec::Vec; -use casper_types::{bytesrepr::FromBytes, CLValue, RuntimeArgs}; -pub type BlockTime = u64; +// TODO: remove as we'll use Bytes pub type EventData = Vec; pub use address::{Address, OdraAddress}; pub use casper_types; -pub use casper_types::{CLType, CLTyped, PublicKey, U128, U256, U512}; +pub use casper_types::bytesrepr::{Bytes, FromBytes, ToBytes}; +use casper_types::CLValue; +pub use casper_types::{runtime_args, RuntimeArgs}; +pub use casper_types::{CLType, CLTyped, PublicKey, SecretKey, U128, U256, U512}; pub use error::{AddressError, CollectionError, ExecutionError, OdraError, VmError}; pub use item::OdraItem;