├── .gitignore ├── Cargo.toml ├── LICENSE ├── Makefile ├── README.md ├── bazaar.yaml ├── circle.yml ├── scripts ├── bazaar │ ├── .cargo │ │ └── config │ ├── Cargo.toml │ ├── chisel.toml │ └── src │ │ └── lib.rs └── helloworld │ ├── .cargo │ └── config │ ├── Cargo.toml │ ├── chisel.toml │ └── src │ └── lib.rs ├── src ├── main.rs └── types.rs └── test.yaml /.gitignore: -------------------------------------------------------------------------------- 1 | **/target 2 | **/*.rs.bk 3 | **/Cargo.lock 4 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "phase2-scout" 3 | version = "0.0.0" 4 | authors = ["Alex Beregszaszi "] 5 | license = "Apache-2.0" 6 | repository = "https://github.com/ewasm/scout" 7 | description = "Eth 2.0 Phase 2 execution prototyping engine" 8 | publish = false 9 | edition = "2018" 10 | 11 | [dependencies] 12 | wasmi = "0.5" 13 | rustc-hex = "1.0" 14 | serde = { version = "1.0", features = ["derive"] } 15 | serde_yaml = "0.8" 16 | log = "0.4" 17 | env_logger = "0.7" 18 | primitive-types = "0.6" 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: build test 2 | 3 | build: 4 | cd scripts/helloworld && cargo build --release && chisel run --config chisel.toml 5 | cd scripts/bazaar && cargo build --release && chisel run --config chisel.toml 6 | cargo build --release 7 | 8 | test: 9 | target/release/phase2-scout 10 | target/release/phase2-scout bazaar.yaml 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Scout 2 | 3 | Scout is a Ethereum 2.0 Phase 2 execution prototyping engine. 4 | 5 | **Warning: this is super experimental** 6 | 7 | ## Goals 8 | 9 | 1) Create a (black boxed) execution prototyping engine 10 | 2) Create some example contracts ("execution scripts") 11 | 3) Enable "easy" onboarding for creating scripts 12 | 4) By having actual real world use cases in scripts, we can benchmark the design and identify bottlenecks 13 | 14 | ## What is this? 15 | 16 | This engine intentionally avoids a lot of details and therefore it is not usable as a Eth 2.0 client. 17 | Instead of being a client, it should support reading and outputting shard/beacon states in a YAML format. 18 | 19 | ## How to use this? 20 | 21 | Install Rust first. Add the wasm32 target to the toolchain and install `chisel` using cargo: 22 | ```sh 23 | rustup target add wasm32-unknown-unknown 24 | rustup component add rustfmt 25 | rustup update 26 | cargo install chisel 27 | ``` 28 | 29 | There is a `Makefile` to make building easy: 30 | - `build` will build all components (the runner and the example scripts) 31 | - `test` will run tests using the YAML test files 32 | - `all` will do both 33 | 34 | The runner is called scout and is available at `target/release/phase2-scout` after being built. 35 | 36 | The runner expects a YAML test file: 37 | ```yaml 38 | beacon_state: 39 | execution_scripts: 40 | - scripts/helloworld/target/wasm32-unknown-unknown/release/phase2_helloworld.wasm 41 | shard_pre_state: 42 | exec_env_states: 43 | - "0000000000000000000000000000000000000000000000000000000000000000" 44 | shard_blocks: 45 | - env: 0 46 | data: "" 47 | - env: 0 48 | data: "" 49 | shard_post_state: 50 | exec_env_states: 51 | - "0000000000000000000000000000000000000000000000000000000000000000" 52 | ``` 53 | 54 | The runner expects a filename pointing to the test file or will default to `test.yaml` in the local directory if nothing was specified. 55 | 56 | ## How to code scripts? 57 | 58 | An example script is located in `scripts/helloworld`. It uses [ewasm-rust-api](https://github.com/ewasm/ewasm-rust-api) with the experimental `eth2` feature. 59 | 60 | ```rust 61 | extern crate ewasm_api; 62 | 63 | use ewasm_api::*; 64 | 65 | #[cfg(not(test))] 66 | #[no_mangle] 67 | pub extern "C" fn main() { 68 | let pre_state_root = eth2::load_pre_state_root(); 69 | 70 | assert!(eth2::block_data_size() == 0); 71 | 72 | // No updates were made to the state 73 | let post_state_root = pre_state_root; 74 | 75 | eth2::save_post_state_root(post_state) 76 | } 77 | ``` 78 | 79 | A better example is located in `scripts/bazaar` which is in essence a stateless contract. It uses SSZ serialisation. A test case is included in `bazaar.yaml`. 80 | 81 | It should be possible to import any Rust crate as long as it can be compiled to the wasm32 target. 82 | 83 | ## Maintainer 84 | 85 | * Alex Beregszaszi 86 | 87 | ## License 88 | 89 | Apache 2.0 90 | -------------------------------------------------------------------------------- /bazaar.yaml: -------------------------------------------------------------------------------- 1 | beacon_state: 2 | execution_scripts: 3 | - scripts/helloworld/target/wasm32-unknown-unknown/release/phase2_helloworld.wasm 4 | - scripts/bazaar/target/wasm32-unknown-unknown/release/phase2_bazaar.wasm 5 | shard_pre_state: 6 | exec_env_states: 7 | - "0000000000000000000000000000000000000000000000000000000000000000" 8 | - "22ea9b045f8792170b45ec629c98e1b92bc6a19cd8d0e9f37baaadf2564142f4" 9 | shard_blocks: 10 | - env: 0 11 | data: "" 12 | - env: 0 13 | data: "" 14 | - env: 1 15 | data: "5c0000005000000001000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000001010101010101010101010101010101010101010101010101010101010101010400000000000000" 16 | shard_post_state: 17 | exec_env_states: 18 | - "0000000000000000000000000000000000000000000000000000000000000000" 19 | - "29505fd952857b5766c759bcb4af58eb8df5a91043540c1398dd987a503127fc" 20 | deposit_receipts: [] 21 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | jobs: 4 | build: 5 | docker: 6 | - image: rust:1 7 | steps: 8 | - checkout 9 | - run: 10 | name: Update environment 11 | command: | 12 | rustup target add wasm32-unknown-unknown 13 | rustup component add rustfmt 14 | rustup update 15 | cargo install chisel 16 | - run: 17 | name: Check formatting 18 | command: | 19 | rustfmt --version 20 | cargo fmt --all -- --check 21 | - run: 22 | name: Build 23 | command: make 24 | -------------------------------------------------------------------------------- /scripts/bazaar/.cargo/config: -------------------------------------------------------------------------------- 1 | [build] 2 | target = "wasm32-unknown-unknown" -------------------------------------------------------------------------------- /scripts/bazaar/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "phase2-bazaar" 3 | version = "0.0.0" 4 | authors = ["Alex Beregszaszi "] 5 | license = "Apache-2.0" 6 | repository = "https://github.com/ewasm/scout" 7 | description = "Eth 2.0 Phase 2 execution script: Bazaar message board" 8 | publish = false 9 | edition = "2018" 10 | 11 | [dependencies] 12 | ewasm_api = { version = "0.11", features = ["std", "eth2", "qimalloc"] } 13 | ssz = "0.1.2" 14 | ssz-derive = "0.1.2" 15 | sha3 = "^0.6" 16 | 17 | [lib] 18 | crate-type = ["cdylib"] 19 | 20 | [profile.release] 21 | lto = true 22 | debug = false 23 | -------------------------------------------------------------------------------- /scripts/bazaar/chisel.toml: -------------------------------------------------------------------------------- 1 | bazaar: 2 | file: "target/wasm32-unknown-unknown/release/phase2_bazaar.wasm" 3 | trimexports: 4 | preset: "ewasm" 5 | verifyexports: 6 | preset: "ewasm" 7 | snip: 8 | preset: "ewasm" 9 | -------------------------------------------------------------------------------- /scripts/bazaar/src/lib.rs: -------------------------------------------------------------------------------- 1 | /// 2 | /// This is an example Phase 2 script called "Bazaar". 3 | /// 4 | /// The "state" consists of an append-only list of messages. Each transaction 5 | /// has to supply the new list of messages to be appended, as well as the entire 6 | /// current state. The hash of the SSZ encoded state is stored as the "state root". 7 | /// Obviously this has an unsustainable growth, but the main point is to demonstrate 8 | /// how to work with SSZ serialised data. 9 | /// 10 | /// It doesn't yet use SSZ merkleization and SSZ partial, that should be an obvious next step. 11 | /// 12 | /// Message 13 | /// { 14 | /// "timestamp": uint64, 15 | /// "message": bytes 16 | /// } 17 | /// 18 | /// State { 19 | /// "messages": [Message] 20 | /// } 21 | /// 22 | /// InputBlock { 23 | /// "new_messages": [Message], 24 | /// "state": State 25 | /// } 26 | /// 27 | extern crate ewasm_api; 28 | extern crate sha3; 29 | extern crate ssz; 30 | 31 | #[macro_use] 32 | extern crate ssz_derive; 33 | 34 | use ewasm_api::*; 35 | use sha3::{Digest, Keccak256}; 36 | use ssz::{Decode, Encode}; 37 | 38 | #[derive(Debug, PartialEq, Ssz, Default)] 39 | struct Message { 40 | pub timestamp: u64, 41 | pub message: [u8; 32], 42 | } 43 | 44 | #[derive(Debug, PartialEq, Ssz, Default)] 45 | struct State { 46 | pub messages: Vec, 47 | } 48 | 49 | #[derive(Debug, PartialEq, Ssz, Default)] 50 | struct InputBlock { 51 | pub new_messages: Vec, 52 | pub state: State, 53 | } 54 | 55 | trait StateRoot { 56 | fn state_root(&self) -> types::Bytes32; 57 | } 58 | 59 | impl StateRoot for State { 60 | fn state_root(&self) -> types::Bytes32 { 61 | let serialised = self.encode(); 62 | let hash = Keccak256::digest(&serialised[..]); 63 | let mut ret = types::Bytes32::default(); 64 | ret.bytes.copy_from_slice(&hash[..]); 65 | ret 66 | } 67 | } 68 | 69 | fn process_block(pre_state_root: types::Bytes32, mut block_data: &[u8]) -> types::Bytes32 { 70 | let mut block = InputBlock::decode(&mut block_data).expect("valid input"); 71 | 72 | // Validate pre state 73 | assert!(block.state.state_root() == pre_state_root); 74 | 75 | for message in block.new_messages { 76 | block.state.messages.push(message) 77 | } 78 | 79 | #[cfg(test)] 80 | println!("{:#?}", block.state); 81 | 82 | block.state.state_root() 83 | } 84 | 85 | #[cfg(not(test))] 86 | #[no_mangle] 87 | pub extern "C" fn main() { 88 | let pre_state_root = eth2::load_pre_state_root(); 89 | let block_data = eth2::acquire_block_data(); 90 | let post_state_root = process_block(pre_state_root, &block_data); 91 | eth2::save_post_state_root(&post_state_root) 92 | } 93 | 94 | #[cfg(test)] 95 | mod tests { 96 | use super::*; 97 | 98 | #[test] 99 | fn empty_block() { 100 | let block = InputBlock::default(); 101 | // Lets say the previous state was empty 102 | let pre_state = block.state.state_root(); 103 | // Process the input block, we're adding nothing to it 104 | let post_state = process_block(pre_state, &block.encode()); 105 | assert!( 106 | post_state.bytes 107 | == [ 108 | 34, 234, 155, 4, 95, 135, 146, 23, 11, 69, 236, 98, 156, 152, 225, 185, 43, 109 | 198, 161, 156, 216, 208, 233, 243, 123, 170, 173, 242, 86, 65, 66, 244 110 | ] 111 | ); 112 | assert!(pre_state == post_state) 113 | } 114 | 115 | #[test] 116 | fn two_messages_in_block() { 117 | let mut block = InputBlock::default(); 118 | // Lets say the previous state was empty 119 | let pre_state = block.state.state_root(); 120 | // Add new messages now 121 | block.new_messages.push(Message { 122 | timestamp: 1, 123 | message: [0u8; 32], 124 | }); 125 | block.new_messages.push(Message { 126 | timestamp: 2, 127 | message: [1u8; 32], 128 | }); 129 | // Process the input block, we're adding nothing to it 130 | let post_state = process_block(pre_state, &block.encode()); 131 | assert!( 132 | post_state.bytes 133 | == [ 134 | 41, 80, 95, 217, 82, 133, 123, 87, 102, 199, 89, 188, 180, 175, 88, 235, 141, 135 | 245, 169, 16, 67, 84, 12, 19, 152, 221, 152, 122, 80, 49, 39, 252 136 | ] 137 | ); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /scripts/helloworld/.cargo/config: -------------------------------------------------------------------------------- 1 | [build] 2 | target = "wasm32-unknown-unknown" -------------------------------------------------------------------------------- /scripts/helloworld/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "phase2-helloworld" 3 | version = "0.0.0" 4 | authors = ["Alex Beregszaszi "] 5 | license = "Apache-2.0" 6 | repository = "https://github.com/ewasm/scout" 7 | description = "Eth 2.0 Phase 2 execution script: Hello World" 8 | publish = false 9 | edition = "2018" 10 | 11 | [dependencies] 12 | ewasm_api = { version = "0.11", features = ["std", "eth2", "qimalloc", "debug"] } 13 | 14 | [lib] 15 | crate-type = ["cdylib"] 16 | 17 | [profile.release] 18 | lto = true 19 | debug = false 20 | -------------------------------------------------------------------------------- /scripts/helloworld/chisel.toml: -------------------------------------------------------------------------------- 1 | helloworld: 2 | file: "target/wasm32-unknown-unknown/release/phase2_helloworld.wasm" 3 | trimexports: 4 | preset: "ewasm" 5 | verifyexports: 6 | preset: "ewasm" 7 | snip: 8 | preset: "ewasm" 9 | -------------------------------------------------------------------------------- /scripts/helloworld/src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate ewasm_api; 2 | 3 | use ewasm_api::*; 4 | 5 | #[cfg(not(test))] 6 | #[no_mangle] 7 | pub extern "C" fn main() { 8 | let pre_state_root = eth2::load_pre_state_root(); 9 | 10 | // Show debug functionality 11 | debug::log("hello world!"); 12 | debug::print32(42); 13 | debug::print64(99); 14 | debug::print_mem(&pre_state_root.bytes); 15 | debug::print_mem_hex(&pre_state_root.bytes); 16 | 17 | assert!(eth2::block_data_size() == 0); 18 | 19 | // No updates were made to the state 20 | let post_state_root = pre_state_root; 21 | 22 | // Deposit only for demo purposes 23 | // This is valid length-wise, but invalid content-wise 24 | let deposit = [0u8; 184]; 25 | eth2::push_new_deposit(&deposit); 26 | 27 | eth2::save_post_state_root(&post_state_root) 28 | } 29 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | extern crate rustc_hex; 2 | extern crate wasmi; 3 | #[macro_use] 4 | extern crate log; 5 | extern crate env_logger; 6 | 7 | use primitive_types::U256; 8 | use rustc_hex::{FromHex, ToHex}; 9 | use serde::{Deserialize, Serialize}; 10 | use std::convert::{TryFrom, TryInto}; 11 | use std::env; 12 | use std::error::Error; 13 | use std::fmt; 14 | use wasmi::{ 15 | Error as InterpreterError, Externals, FuncInstance, FuncRef, ImportsBuilder, MemoryRef, Module, 16 | ModuleImportResolver, ModuleInstance, NopExternals, RuntimeArgs, RuntimeValue, Signature, Trap, 17 | TrapKind, ValueType, 18 | }; 19 | 20 | mod types; 21 | use crate::types::*; 22 | 23 | #[derive(Debug)] 24 | pub struct ScoutError(String); 25 | 26 | impl From for ScoutError { 27 | fn from(error: String) -> Self { 28 | ScoutError { 0: error } 29 | } 30 | } 31 | 32 | impl From for ScoutError { 33 | fn from(error: std::io::Error) -> Self { 34 | ScoutError { 35 | 0: error.description().to_string(), 36 | } 37 | } 38 | } 39 | 40 | impl From for ScoutError { 41 | fn from(error: rustc_hex::FromHexError) -> Self { 42 | ScoutError { 43 | 0: error.description().to_string(), 44 | } 45 | } 46 | } 47 | 48 | impl From for ScoutError { 49 | fn from(error: serde_yaml::Error) -> Self { 50 | ScoutError { 51 | 0: error.description().to_string(), 52 | } 53 | } 54 | } 55 | 56 | impl From for ScoutError { 57 | fn from(error: wasmi::Error) -> Self { 58 | ScoutError { 59 | 0: error.description().to_string(), 60 | } 61 | } 62 | } 63 | 64 | impl From for ScoutError { 65 | fn from(error: wasmi::Trap) -> Self { 66 | ScoutError { 67 | 0: error.description().to_string(), 68 | } 69 | } 70 | } 71 | 72 | const LOADPRESTATEROOT_FUNC_INDEX: usize = 0; 73 | const BLOCKDATASIZE_FUNC_INDEX: usize = 1; 74 | const BLOCKDATACOPY_FUNC_INDEX: usize = 2; 75 | const SAVEPOSTSTATEROOT_FUNC_INDEX: usize = 3; 76 | const PUSHNEWDEPOSIT_FUNC_INDEX: usize = 4; 77 | const USETICKS_FUNC_INDEX: usize = 5; 78 | const DEBUG_PRINT32_FUNC: usize = 6; 79 | const DEBUG_PRINT64_FUNC: usize = 7; 80 | const DEBUG_PRINTMEM_FUNC: usize = 8; 81 | const DEBUG_PRINTMEMHEX_FUNC: usize = 9; 82 | const BIGNUM_ADD256_FUNC: usize = 10; 83 | const BIGNUM_SUB256_FUNC: usize = 11; 84 | 85 | fn load_import(code: &[u8]) -> Result { 86 | let module = Module::from_buffer(&code)?; 87 | let imports = ImportsBuilder::new(); 88 | let instance = ModuleInstance::new(&module, &imports)?.run_start(&mut NopExternals)?; 89 | Ok(instance) 90 | } 91 | 92 | #[derive(Default, PartialEq, Clone, Debug)] 93 | pub struct Library { 94 | name: String, 95 | code: Vec, 96 | } 97 | 98 | // TODO: move elsehwere? 99 | type DepositBlob = Vec; 100 | 101 | struct Runtime<'a> { 102 | code: &'a [u8], 103 | libraries: &'a [Library], 104 | ticks_left: u32, 105 | memory: Option, 106 | pre_state: &'a Bytes32, 107 | block_data: &'a ShardBlockBody, 108 | post_state: Bytes32, 109 | deposits: Vec, 110 | } 111 | 112 | impl<'a> Runtime<'a> { 113 | fn new( 114 | code: &'a [u8], 115 | libraries: &'a [Library], 116 | pre_state: &'a Bytes32, 117 | block_data: &'a ShardBlockBody, 118 | ) -> Runtime<'a> { 119 | Runtime { 120 | code: code, 121 | libraries: libraries, 122 | ticks_left: 10_000_000, // FIXME: make this configurable 123 | memory: None, 124 | pre_state: pre_state, 125 | block_data: block_data, 126 | post_state: Bytes32::default(), 127 | deposits: Vec::new(), 128 | } 129 | } 130 | 131 | fn execute(&mut self) -> Result<(Bytes32, Vec), ScoutError> { 132 | let module = Module::from_buffer(&self.code)?; 133 | let mut imports = ImportsBuilder::new(); 134 | // TODO: remove this and rely on Eth2ImportResolver and DebugImportResolver 135 | imports.push_resolver("env", &RuntimeModuleImportResolver); 136 | imports.push_resolver("eth2", &Eth2ImportResolver); 137 | imports.push_resolver("bignum", &BignumImportResolver); 138 | imports.push_resolver("debug", &DebugImportResolver); 139 | 140 | // Load all libraries 141 | // NOTE: creating this variable here to track lifetime 142 | let libraries: Result, ScoutError> = self 143 | .libraries 144 | .iter() 145 | .map(|ref library| Ok((library.name.to_string(), load_import(&library.code)?))) 146 | .collect(); 147 | let libraries = libraries?; 148 | 149 | // Link them to the current instance 150 | for library in &libraries { 151 | debug!("Attaching library: {}", &library.0); 152 | imports.push_resolver(&library.0, &library.1); 153 | } 154 | 155 | let instance = ModuleInstance::new(&module, &imports)?.run_start(&mut NopExternals)?; 156 | 157 | // FIXME: pass through errors here and not use .expect() 158 | let internal_mem = instance 159 | .export_by_name("memory") 160 | .expect("Module expected to have 'memory' export") 161 | .as_memory() 162 | .cloned() 163 | .expect("'memory' export should be a memory"); 164 | 165 | self.memory = Some(internal_mem); 166 | 167 | let result = instance.invoke_export("main", &[], self)?; 168 | 169 | info!("Result: {:?}", result); 170 | 171 | // TODO: avoid cloning here 172 | Ok((self.post_state, self.deposits.clone())) 173 | } 174 | } 175 | 176 | impl<'a> Externals for Runtime<'a> { 177 | fn invoke_index( 178 | &mut self, 179 | index: usize, 180 | args: RuntimeArgs, 181 | ) -> Result, Trap> { 182 | match index { 183 | USETICKS_FUNC_INDEX => { 184 | let ticks: u32 = args.nth(0); 185 | if self.ticks_left < ticks { 186 | // FIXME: use TrapKind::Host 187 | return Err(Trap::new(TrapKind::Unreachable)); 188 | } 189 | self.ticks_left -= ticks; 190 | Ok(None) 191 | } 192 | LOADPRESTATEROOT_FUNC_INDEX => { 193 | let ptr: u32 = args.nth(0); 194 | info!("loadprestateroot to {}", ptr); 195 | 196 | // TODO: add checks for out of bounds access 197 | let memory = self.memory.as_ref().expect("expects memory object"); 198 | memory 199 | .set(ptr, &self.pre_state.bytes) 200 | .expect("expects writing to memory to succeed"); 201 | 202 | Ok(None) 203 | } 204 | SAVEPOSTSTATEROOT_FUNC_INDEX => { 205 | let ptr: u32 = args.nth(0); 206 | info!("savepoststateroot from {}", ptr); 207 | 208 | // TODO: add checks for out of bounds access 209 | let memory = self.memory.as_ref().expect("expects memory object"); 210 | memory 211 | .get_into(ptr, &mut self.post_state.bytes) 212 | .expect("expects reading from memory to succeed"); 213 | 214 | Ok(None) 215 | } 216 | BLOCKDATASIZE_FUNC_INDEX => { 217 | let ret: i32 = self.block_data.data.len() as i32; 218 | info!("blockdatasize {}", ret); 219 | Ok(Some(ret.into())) 220 | } 221 | BLOCKDATACOPY_FUNC_INDEX => { 222 | let ptr: u32 = args.nth(0); 223 | let offset: u32 = args.nth(1); 224 | let length: u32 = args.nth(2); 225 | info!( 226 | "blockdatacopy to {} from {} for {} bytes", 227 | ptr, offset, length 228 | ); 229 | 230 | // TODO: add overflow check 231 | let offset = offset as usize; 232 | let length = length as usize; 233 | 234 | // TODO: add checks for out of bounds access 235 | let memory = self.memory.as_ref().expect("expects memory object"); 236 | memory 237 | .set(ptr, &self.block_data.data[offset..length]) 238 | .expect("expects writing to memory to succeed"); 239 | 240 | Ok(None) 241 | } 242 | PUSHNEWDEPOSIT_FUNC_INDEX => { 243 | let ptr: u32 = args.nth(0); 244 | let length: u32 = args.nth(1); 245 | info!("pushnewdeposit from {} for {} bytes", ptr, length); 246 | 247 | let memory = self.memory.as_ref().expect("expects memory"); 248 | let tmp = memory 249 | .get(ptr, length as usize) 250 | .expect("expects reading from memory to succeed"); 251 | debug!("deposit: {}", tmp.to_hex()); 252 | self.deposits.push(tmp); 253 | 254 | Ok(None) 255 | } 256 | DEBUG_PRINT32_FUNC => { 257 | let value: u32 = args.nth(0); 258 | debug!("print.i32: {}", value); 259 | Ok(None) 260 | } 261 | DEBUG_PRINT64_FUNC => { 262 | let value: u64 = args.nth(0); 263 | debug!("print.i64: {}", value); 264 | Ok(None) 265 | } 266 | DEBUG_PRINTMEM_FUNC => { 267 | let ptr: u32 = args.nth(0); 268 | let length: u32 = args.nth(1); 269 | let mut buf = Vec::with_capacity(length as usize); 270 | unsafe { buf.set_len(length as usize) }; 271 | // TODO: add checks for out of bounds access 272 | let memory = self.memory.as_ref().expect("expects memory object"); 273 | memory 274 | .get_into(ptr, &mut buf) 275 | .expect("expects reading from memory to succeed"); 276 | debug!("print: {}", String::from_utf8_lossy(&buf)); 277 | Ok(None) 278 | } 279 | DEBUG_PRINTMEMHEX_FUNC => { 280 | let ptr: u32 = args.nth(0); 281 | let length: u32 = args.nth(1); 282 | let mut buf = Vec::with_capacity(length as usize); 283 | unsafe { buf.set_len(length as usize) }; 284 | // TODO: add checks for out of bounds access 285 | let memory = self.memory.as_ref().expect("expects memory object"); 286 | memory 287 | .get_into(ptr, &mut buf) 288 | .expect("expects reading from memory to succeed"); 289 | debug!("print.hex: {}", buf.to_hex()); 290 | Ok(None) 291 | } 292 | BIGNUM_ADD256_FUNC => { 293 | let a_ptr: u32 = args.nth(0); 294 | let b_ptr: u32 = args.nth(1); 295 | let c_ptr: u32 = args.nth(2); 296 | 297 | let mut a_raw = [0u8; 32]; 298 | let mut b_raw = [0u8; 32]; 299 | let mut c_raw = [0u8; 32]; 300 | 301 | let memory = self.memory.as_ref().expect("expects memory object"); 302 | memory 303 | .get_into(a_ptr, &mut a_raw) 304 | .expect("expects reading from memory to succeed"); 305 | memory 306 | .get_into(b_ptr, &mut b_raw) 307 | .expect("expects reading from memory to succeed"); 308 | 309 | let a = U256::from_big_endian(&a_raw); 310 | let b = U256::from_big_endian(&b_raw); 311 | let c = a.checked_add(b).expect("expects non-overflowing addition"); 312 | c.to_big_endian(&mut c_raw); 313 | 314 | memory 315 | .set(c_ptr, &c_raw) 316 | .expect("expects writing to memory to succeed"); 317 | 318 | Ok(None) 319 | } 320 | BIGNUM_SUB256_FUNC => { 321 | let a_ptr: u32 = args.nth(0); 322 | let b_ptr: u32 = args.nth(1); 323 | let c_ptr: u32 = args.nth(2); 324 | 325 | let mut a_raw = [0u8; 32]; 326 | let mut b_raw = [0u8; 32]; 327 | let mut c_raw = [0u8; 32]; 328 | 329 | let memory = self.memory.as_ref().expect("expects memory object"); 330 | memory 331 | .get_into(a_ptr, &mut a_raw) 332 | .expect("expects reading from memory to succeed"); 333 | memory 334 | .get_into(b_ptr, &mut b_raw) 335 | .expect("expects reading from memory to succeed"); 336 | 337 | let a = U256::from_big_endian(&a_raw); 338 | let b = U256::from_big_endian(&b_raw); 339 | let c = a 340 | .checked_sub(b) 341 | .expect("expects non-overflowing subtraction"); 342 | c.to_big_endian(&mut c_raw); 343 | 344 | memory 345 | .set(c_ptr, &c_raw) 346 | .expect("expects writing to memory to succeed"); 347 | 348 | Ok(None) 349 | } 350 | _ => panic!("unknown function index"), 351 | } 352 | } 353 | } 354 | 355 | // TODO: remove this and rely on Eth2ImportResolver and DebugImportResolver 356 | struct RuntimeModuleImportResolver; 357 | 358 | impl<'a> ModuleImportResolver for RuntimeModuleImportResolver { 359 | fn resolve_func( 360 | &self, 361 | field_name: &str, 362 | _signature: &Signature, 363 | ) -> Result { 364 | let func_ref = match field_name { 365 | "eth2_useTicks" => FuncInstance::alloc_host( 366 | Signature::new(&[ValueType::I32][..], None), 367 | USETICKS_FUNC_INDEX, 368 | ), 369 | "eth2_loadPreStateRoot" => FuncInstance::alloc_host( 370 | Signature::new(&[ValueType::I32][..], None), 371 | LOADPRESTATEROOT_FUNC_INDEX, 372 | ), 373 | "eth2_blockDataSize" => FuncInstance::alloc_host( 374 | Signature::new(&[][..], Some(ValueType::I32)), 375 | BLOCKDATASIZE_FUNC_INDEX, 376 | ), 377 | "eth2_blockDataCopy" => FuncInstance::alloc_host( 378 | Signature::new(&[ValueType::I32, ValueType::I32, ValueType::I32][..], None), 379 | BLOCKDATACOPY_FUNC_INDEX, 380 | ), 381 | "eth2_savePostStateRoot" => FuncInstance::alloc_host( 382 | Signature::new(&[ValueType::I32][..], None), 383 | SAVEPOSTSTATEROOT_FUNC_INDEX, 384 | ), 385 | "eth2_pushNewDeposit" => FuncInstance::alloc_host( 386 | Signature::new(&[ValueType::I32, ValueType::I32][..], None), 387 | PUSHNEWDEPOSIT_FUNC_INDEX, 388 | ), 389 | "debug_print32" => FuncInstance::alloc_host( 390 | Signature::new(&[ValueType::I32][..], None), 391 | DEBUG_PRINT32_FUNC, 392 | ), 393 | "debug_print64" => FuncInstance::alloc_host( 394 | Signature::new(&[ValueType::I64][..], None), 395 | DEBUG_PRINT64_FUNC, 396 | ), 397 | "debug_printMem" => FuncInstance::alloc_host( 398 | Signature::new(&[ValueType::I32, ValueType::I32][..], None), 399 | DEBUG_PRINTMEM_FUNC, 400 | ), 401 | "debug_printMemHex" => FuncInstance::alloc_host( 402 | Signature::new(&[ValueType::I32, ValueType::I32][..], None), 403 | DEBUG_PRINTMEMHEX_FUNC, 404 | ), 405 | "bignum_add256" => FuncInstance::alloc_host( 406 | Signature::new(&[ValueType::I32, ValueType::I32, ValueType::I32][..], None), 407 | BIGNUM_ADD256_FUNC, 408 | ), 409 | "bignum_sub256" => FuncInstance::alloc_host( 410 | Signature::new(&[ValueType::I32, ValueType::I32, ValueType::I32][..], None), 411 | BIGNUM_SUB256_FUNC, 412 | ), 413 | _ => { 414 | return Err(InterpreterError::Function(format!( 415 | "host module doesn't export function with name {}", 416 | field_name 417 | ))) 418 | } 419 | }; 420 | Ok(func_ref) 421 | } 422 | } 423 | 424 | struct Eth2ImportResolver; 425 | 426 | impl<'a> ModuleImportResolver for Eth2ImportResolver { 427 | fn resolve_func( 428 | &self, 429 | field_name: &str, 430 | _signature: &Signature, 431 | ) -> Result { 432 | let func_ref = match field_name { 433 | "useTicks" => FuncInstance::alloc_host( 434 | Signature::new(&[ValueType::I32][..], None), 435 | USETICKS_FUNC_INDEX, 436 | ), 437 | "loadPreStateRoot" => FuncInstance::alloc_host( 438 | Signature::new(&[ValueType::I32][..], None), 439 | LOADPRESTATEROOT_FUNC_INDEX, 440 | ), 441 | "blockDataSize" => FuncInstance::alloc_host( 442 | Signature::new(&[][..], Some(ValueType::I32)), 443 | BLOCKDATASIZE_FUNC_INDEX, 444 | ), 445 | "blockDataCopy" => FuncInstance::alloc_host( 446 | Signature::new(&[ValueType::I32, ValueType::I32, ValueType::I32][..], None), 447 | BLOCKDATACOPY_FUNC_INDEX, 448 | ), 449 | "savePostStateRoot" => FuncInstance::alloc_host( 450 | Signature::new(&[ValueType::I32][..], None), 451 | SAVEPOSTSTATEROOT_FUNC_INDEX, 452 | ), 453 | "pushNewDeposit" => FuncInstance::alloc_host( 454 | Signature::new(&[ValueType::I32, ValueType::I32][..], None), 455 | PUSHNEWDEPOSIT_FUNC_INDEX, 456 | ), 457 | _ => { 458 | return Err(InterpreterError::Function(format!( 459 | "host module doesn't export function with name {}", 460 | field_name 461 | ))) 462 | } 463 | }; 464 | Ok(func_ref) 465 | } 466 | } 467 | 468 | struct BignumImportResolver; 469 | 470 | impl<'a> ModuleImportResolver for BignumImportResolver { 471 | fn resolve_func( 472 | &self, 473 | field_name: &str, 474 | _signature: &Signature, 475 | ) -> Result { 476 | let func_ref = match field_name { 477 | "add256" => FuncInstance::alloc_host( 478 | Signature::new(&[ValueType::I32, ValueType::I32, ValueType::I32][..], None), 479 | BIGNUM_ADD256_FUNC, 480 | ), 481 | "sub256" => FuncInstance::alloc_host( 482 | Signature::new(&[ValueType::I32, ValueType::I32, ValueType::I32][..], None), 483 | BIGNUM_SUB256_FUNC, 484 | ), 485 | _ => { 486 | return Err(InterpreterError::Function(format!( 487 | "host module doesn't export function with name {}", 488 | field_name 489 | ))) 490 | } 491 | }; 492 | Ok(func_ref) 493 | } 494 | } 495 | 496 | struct DebugImportResolver; 497 | 498 | impl<'a> ModuleImportResolver for DebugImportResolver { 499 | fn resolve_func( 500 | &self, 501 | field_name: &str, 502 | _signature: &Signature, 503 | ) -> Result { 504 | let func_ref = match field_name { 505 | "print32" => FuncInstance::alloc_host( 506 | Signature::new(&[ValueType::I32][..], None), 507 | DEBUG_PRINT32_FUNC, 508 | ), 509 | "print64" => FuncInstance::alloc_host( 510 | Signature::new(&[ValueType::I64][..], None), 511 | DEBUG_PRINT64_FUNC, 512 | ), 513 | "printMem" => FuncInstance::alloc_host( 514 | Signature::new(&[ValueType::I32, ValueType::I32][..], None), 515 | DEBUG_PRINTMEM_FUNC, 516 | ), 517 | "printMemHex" => FuncInstance::alloc_host( 518 | Signature::new(&[ValueType::I32, ValueType::I32][..], None), 519 | DEBUG_PRINTMEMHEX_FUNC, 520 | ), 521 | _ => { 522 | return Err(InterpreterError::Function(format!( 523 | "host module doesn't export function with name {}", 524 | field_name 525 | ))) 526 | } 527 | }; 528 | Ok(func_ref) 529 | } 530 | } 531 | 532 | const BYTES_PER_SHARD_BLOCK_BODY: usize = 16384; 533 | const ZERO_HASH: Bytes32 = Bytes32 { bytes: [0u8; 32] }; 534 | 535 | #[derive(Default, PartialEq, Clone, Debug)] 536 | pub struct Hash([u8; 32]); 537 | 538 | #[derive(Clone)] 539 | pub struct BLSPubKey([u8; 48]); 540 | 541 | impl PartialEq for BLSPubKey { 542 | fn eq(&self, other: &Self) -> bool { 543 | self.0[..] == other.0[..] 544 | } 545 | } 546 | 547 | impl Default for BLSPubKey { 548 | fn default() -> Self { 549 | BLSPubKey { 0: [0u8; 48] } 550 | } 551 | } 552 | 553 | impl fmt::Debug for BLSPubKey { 554 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 555 | write!(f, "{}", self.0.to_hex()) 556 | } 557 | } 558 | 559 | #[derive(Clone)] 560 | pub struct BLSSignature([u8; 96]); 561 | 562 | impl PartialEq for BLSSignature { 563 | fn eq(&self, other: &Self) -> bool { 564 | self.0[..] == other.0[..] 565 | } 566 | } 567 | 568 | impl Default for BLSSignature { 569 | fn default() -> Self { 570 | BLSSignature { 0: [0u8; 96] } 571 | } 572 | } 573 | 574 | impl fmt::Debug for BLSSignature { 575 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 576 | write!(f, "{}", self.0.to_hex()) 577 | } 578 | } 579 | 580 | /// These are Phase 0 structures. 581 | /// https://github.com/ethereum/eth2.0-specs/blob/dev/specs/core/0_beacon-chain.md 582 | /// basically this is a little-endian tightly packed representation of those fields. 583 | #[derive(Default, PartialEq, Clone, Debug)] 584 | pub struct Deposit { 585 | pubkey: BLSPubKey, 586 | withdrawal_credentials: Hash, 587 | amount: u64, 588 | signature: BLSSignature, 589 | } 590 | 591 | impl TryFrom> for Deposit { 592 | type Error = String; 593 | fn try_from(input: Vec) -> Result { 594 | if input.len() != 184 { 595 | return Err("input be must exactly 184 bytes long".to_string()); 596 | } 597 | let mut raw_pubkey = [0u8; 48]; 598 | raw_pubkey.copy_from_slice(&input[0..48]); 599 | let mut raw_hash = [0u8; 32]; 600 | raw_hash.copy_from_slice(&input[48..80]); 601 | let mut raw_amount = [0u8; 8]; 602 | raw_amount.copy_from_slice(&input[80..88]); 603 | let mut raw_signature = [0u8; 96]; 604 | raw_signature.copy_from_slice(&input[88..184]); 605 | Ok(Deposit { 606 | pubkey: BLSPubKey(raw_pubkey), 607 | withdrawal_credentials: Hash(raw_hash), 608 | amount: u64::from_be_bytes(raw_amount), 609 | signature: BLSSignature(raw_signature), 610 | }) 611 | } 612 | } 613 | 614 | /// These are Phase 2 Proposal 2 structures. 615 | 616 | #[derive(Default, PartialEq, Clone, Debug)] 617 | pub struct ExecutionScript { 618 | code: Vec, 619 | } 620 | 621 | #[derive(Default, PartialEq, Clone, Debug)] 622 | pub struct BeaconState { 623 | execution_scripts: Vec, 624 | libraries: Vec, 625 | } 626 | 627 | /// Shards are Phase 1 structures. 628 | /// https://github.com/ethereum/eth2.0-specs/blob/dev/specs/core/1_shard-data-chains.md 629 | 630 | #[derive(Default, PartialEq, Clone, Debug)] 631 | pub struct ShardBlockHeader {} 632 | 633 | #[derive(Default, PartialEq, Clone, Debug)] 634 | pub struct ShardBlockBody { 635 | data: Vec, 636 | } 637 | 638 | #[derive(Default, PartialEq, Clone, Debug)] 639 | pub struct ShardBlock { 640 | env: u64, // This is added by Phase 2 Proposal 2 641 | data: ShardBlockBody, 642 | // TODO: add missing fields 643 | } 644 | 645 | #[derive(Default, PartialEq, Clone, Debug)] 646 | pub struct ShardState { 647 | exec_env_states: Vec, 648 | slot: u64, 649 | parent_block: ShardBlockHeader, 650 | // TODO: add missing field 651 | // latest_state_roots: [bytes32, LATEST_STATE_ROOTS_LEMGTH] 652 | } 653 | 654 | impl fmt::Display for ShardBlockBody { 655 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 656 | write!(f, "{}", self.data.to_hex()) 657 | } 658 | } 659 | 660 | impl fmt::Display for ShardBlock { 661 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 662 | write!( 663 | f, 664 | "Shard block for environment {} with data {}", 665 | self.env, self.data 666 | ) 667 | } 668 | } 669 | 670 | impl fmt::Display for ShardState { 671 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 672 | let states: Vec = self 673 | .exec_env_states 674 | .iter() 675 | .map(|x| x.bytes.to_hex()) 676 | .collect(); 677 | write!( 678 | f, 679 | "Shard slot {} with environment states: {:?}", 680 | self.slot, states 681 | ) 682 | } 683 | } 684 | 685 | pub fn execute_code( 686 | code: &[u8], 687 | libraries: &[Library], 688 | pre_state: &Bytes32, 689 | block_data: &ShardBlockBody, 690 | ) -> Result<(Bytes32, Vec), ScoutError> { 691 | debug!( 692 | "Executing codesize({}) and data: {}", 693 | code.len(), 694 | block_data 695 | ); 696 | 697 | let mut runtime = Runtime::new(&code, &libraries, pre_state, block_data); 698 | runtime.execute() 699 | } 700 | 701 | pub fn process_shard_block( 702 | state: &mut ShardState, 703 | beacon_state: &BeaconState, 704 | block: Option, 705 | ) -> Result, ScoutError> { 706 | // println!("Beacon state: {:#?}", beacon_state); 707 | 708 | info!("Pre-execution: {}", state); 709 | 710 | // TODO: implement state root handling 711 | 712 | let deposit_receipts = if let Some(block) = block { 713 | info!("Executing block: {}", block); 714 | 715 | // The execution environment identifier 716 | let env = block.env as usize; // FIXME: usize can be 32-bit 717 | let code = &beacon_state.execution_scripts[env].code; 718 | 719 | // Set post states to empty for any holes 720 | // for x in 0..env { 721 | // state.exec_env_states.push(ZERO_HASH) 722 | // } 723 | let pre_state = &state.exec_env_states[env]; 724 | let (post_state, deposits) = 725 | execute_code(code, &beacon_state.libraries, pre_state, &block.data)?; 726 | state.exec_env_states[env] = post_state; 727 | 728 | // Decode deposits. 729 | let deposits: Result, _> = deposits 730 | .into_iter() 731 | .map(|deposit| deposit.try_into()) 732 | .collect(); 733 | deposits? 734 | } else { 735 | Vec::new() 736 | }; 737 | 738 | // TODO: implement state + deposit root handling 739 | 740 | info!("Post-execution deposit receipts: {:?}", deposit_receipts); 741 | info!("Post-execution: {}", state); 742 | 743 | Ok(deposit_receipts) 744 | } 745 | 746 | #[derive(Debug, PartialEq, Serialize, Deserialize)] 747 | struct TestLibrary { 748 | name: String, 749 | file: String, 750 | } 751 | 752 | #[derive(Debug, PartialEq, Serialize, Deserialize)] 753 | struct TestBeaconState { 754 | execution_scripts: Vec, 755 | libraries: Option>, 756 | } 757 | 758 | #[derive(Debug, PartialEq, Serialize, Deserialize)] 759 | struct TestShardBlock { 760 | env: u64, 761 | data: String, 762 | } 763 | 764 | #[derive(Debug, PartialEq, Serialize, Deserialize)] 765 | struct TestShardState { 766 | exec_env_states: Vec, 767 | } 768 | 769 | #[derive(Debug, PartialEq, Serialize, Deserialize)] 770 | struct TestDeposit { 771 | pubkey: String, 772 | withdrawal_credentials: String, 773 | amount: u64, 774 | signature: String, 775 | } 776 | 777 | #[derive(Debug, PartialEq, Serialize, Deserialize)] 778 | struct TestFile { 779 | beacon_state: TestBeaconState, 780 | shard_blocks: Vec, 781 | shard_pre_state: TestShardState, 782 | shard_post_state: TestShardState, 783 | deposit_receipts: Vec, 784 | } 785 | 786 | fn hex_to_slice(input: &str, output: &mut [u8]) -> Result<(), ScoutError> { 787 | let tmp = input.from_hex()?; 788 | if tmp.len() != output.len() { 789 | return Err(ScoutError("Length mismatch from hex input".to_string())); 790 | } 791 | output.copy_from_slice(&tmp[..]); 792 | Ok(()) 793 | } 794 | 795 | impl TryFrom<&String> for Bytes32 { 796 | type Error = ScoutError; 797 | fn try_from(input: &String) -> Result { 798 | let mut ret = Bytes32::default(); 799 | hex_to_slice(input, &mut ret.bytes)?; 800 | Ok(ret) 801 | } 802 | } 803 | 804 | impl TryFrom for Hash { 805 | type Error = ScoutError; 806 | fn try_from(input: String) -> Result { 807 | let mut ret = Hash::default(); 808 | hex_to_slice(&input, &mut ret.0)?; 809 | Ok(ret) 810 | } 811 | } 812 | 813 | impl TryFrom for BLSPubKey { 814 | type Error = ScoutError; 815 | fn try_from(input: String) -> Result { 816 | let mut ret = BLSPubKey::default(); 817 | hex_to_slice(&input, &mut ret.0)?; 818 | Ok(ret) 819 | } 820 | } 821 | 822 | impl TryFrom for BLSSignature { 823 | type Error = ScoutError; 824 | fn try_from(input: String) -> Result { 825 | let mut ret = BLSSignature::default(); 826 | hex_to_slice(&input, &mut ret.0)?; 827 | Ok(ret) 828 | } 829 | } 830 | 831 | impl TryFrom for BeaconState { 832 | type Error = ScoutError; 833 | fn try_from(input: TestBeaconState) -> Result { 834 | let scripts: Result, ScoutError> = input 835 | .execution_scripts 836 | .iter() 837 | .map(|filename| { 838 | Ok(ExecutionScript { 839 | code: std::fs::read(filename)?, 840 | }) 841 | }) 842 | .collect(); 843 | let libraries: Result, ScoutError> = if let Some(libraries) = input.libraries { 844 | libraries 845 | .iter() 846 | .map(|library| { 847 | Ok(Library { 848 | name: library.name.to_string(), 849 | code: std::fs::read(&library.file)?, 850 | }) 851 | }) 852 | .collect() 853 | } else { 854 | Ok(Vec::new()) 855 | }; 856 | Ok(BeaconState { 857 | execution_scripts: scripts?, 858 | libraries: libraries?, 859 | }) 860 | } 861 | } 862 | 863 | impl TryFrom for ShardBlock { 864 | type Error = ScoutError; 865 | fn try_from(input: TestShardBlock) -> Result { 866 | Ok(ShardBlock { 867 | env: input.env, 868 | data: ShardBlockBody { 869 | data: input.data.from_hex()?, 870 | }, 871 | }) 872 | } 873 | } 874 | 875 | impl TryFrom for ShardState { 876 | type Error = ScoutError; 877 | fn try_from(input: TestShardState) -> Result { 878 | let states: Result, ScoutError> = input 879 | .exec_env_states 880 | .iter() 881 | .map(|state| state.try_into()) 882 | .collect(); 883 | 884 | Ok(ShardState { 885 | exec_env_states: states?, 886 | slot: 0, 887 | parent_block: ShardBlockHeader {}, 888 | }) 889 | } 890 | } 891 | 892 | impl TryFrom for Deposit { 893 | type Error = ScoutError; 894 | fn try_from(input: TestDeposit) -> Result { 895 | Ok(Deposit { 896 | pubkey: input.pubkey.try_into()?, 897 | withdrawal_credentials: input.withdrawal_credentials.try_into()?, 898 | amount: input.amount, 899 | signature: input.signature.try_into()?, 900 | }) 901 | } 902 | } 903 | 904 | fn process_yaml_test(filename: &str) -> Result<(), ScoutError> { 905 | info!("Processing {}...", filename); 906 | let content = std::fs::read(&filename)?; 907 | let test_file: TestFile = serde_yaml::from_slice::(&content[..])?; 908 | debug!("{:#?}", test_file); 909 | 910 | let beacon_state: BeaconState = test_file.beacon_state.try_into()?; 911 | let pre_state: ShardState = test_file.shard_pre_state.try_into()?; 912 | let post_state: ShardState = test_file.shard_post_state.try_into()?; 913 | let expected_deposit_receipts: Result, ScoutError> = test_file 914 | .deposit_receipts 915 | .into_iter() 916 | .map(|deposit| deposit.try_into()) 917 | .collect(); 918 | let expected_deposit_receipts = expected_deposit_receipts?; 919 | 920 | let mut shard_state = pre_state; 921 | let mut deposit_receipts = Vec::new(); 922 | for block in test_file.shard_blocks { 923 | deposit_receipts.append( 924 | process_shard_block(&mut shard_state, &beacon_state, Some(block.try_into()?))?.as_mut(), 925 | ); 926 | } 927 | 928 | if expected_deposit_receipts 929 | .iter() 930 | .all(|deposit| deposit_receipts.contains(deposit)) 931 | { 932 | println!("Matching deposit receipts.") 933 | } else { 934 | println!("Expected deposit receipts: {:?}", expected_deposit_receipts); 935 | println!("Got deposit receipts: {:?}", deposit_receipts); 936 | // TODO: make this an error? 937 | return Ok(()); 938 | } 939 | 940 | debug!("{}", shard_state); 941 | if shard_state != post_state { 942 | println!("Expected state: {}", post_state); 943 | println!("Got state: {}", shard_state); 944 | // TODO: make this an error? 945 | return Ok(()); 946 | } else { 947 | println!("Matching state."); 948 | } 949 | 950 | Ok(()) 951 | } 952 | 953 | fn main() { 954 | env_logger::init(); 955 | 956 | let args: Vec = env::args().collect(); 957 | let ret = process_yaml_test(if args.len() != 2 { 958 | "test.yaml" 959 | } else { 960 | &args[1] 961 | }); 962 | if ret.is_err() { 963 | println!("Unexpected test failure: {:?}", ret.err().unwrap()) 964 | } 965 | } 966 | -------------------------------------------------------------------------------- /src/types.rs: -------------------------------------------------------------------------------- 1 | /// An array of 256 bits. 2 | #[derive(Default, PartialEq, Copy, Clone, Debug)] 3 | pub struct Bytes32 { 4 | pub bytes: [u8; 32], 5 | } 6 | 7 | macro_rules! from_primitive_impl { 8 | ($f:ident, $size:expr, $to:ident) => { 9 | impl From<[$f; $size]> for $to { 10 | fn from(a: [$f; $size]) -> Self { 11 | $to { bytes: a } 12 | } 13 | } 14 | }; 15 | } 16 | 17 | macro_rules! from_primitive_ref_impl { 18 | ($f:ident, $size:expr, $to:ident) => { 19 | impl From<&[$f; $size]> for $to { 20 | fn from(a: &[$f; $size]) -> Self { 21 | $to { bytes: a.clone() } 22 | } 23 | } 24 | }; 25 | } 26 | 27 | macro_rules! from_type_for_primitive_impl { 28 | ($f:ident, $to:ident, $size:expr) => { 29 | impl From<$f> for [$to; $size] { 30 | fn from(a: $f) -> Self { 31 | a.bytes 32 | } 33 | } 34 | }; 35 | } 36 | from_primitive_impl!(u8, 32, Bytes32); 37 | 38 | from_primitive_ref_impl!(u8, 32, Bytes32); 39 | 40 | from_type_for_primitive_impl!(Bytes32, u8, 32); 41 | 42 | #[cfg(test)] 43 | mod tests { 44 | use super::Bytes32; 45 | 46 | macro_rules! test_conversions { 47 | ($type: ident, $size: expr, $test_name: ident) => { 48 | #[test] 49 | fn $test_name() { 50 | let raw = [1; $size]; 51 | 52 | let uint = $type::from(raw); 53 | assert_eq!(uint.bytes[$size - 1], 1); 54 | let uint = $type::from(&raw); 55 | assert_eq!(uint.bytes[$size - 1], 1); 56 | 57 | let uint: $type = raw.into(); 58 | assert_eq!(uint.bytes[$size - 1], 1); 59 | let uint: $type = (&raw).into(); 60 | assert_eq!(uint.bytes[$size - 1], 1); 61 | 62 | let r: [u8; $size] = uint.into(); 63 | assert_eq!(r[$size - 1], 1); 64 | } 65 | }; 66 | } 67 | 68 | test_conversions!(Bytes32, 32, test_bytes32); 69 | } 70 | -------------------------------------------------------------------------------- /test.yaml: -------------------------------------------------------------------------------- 1 | beacon_state: 2 | execution_scripts: 3 | - scripts/helloworld/target/wasm32-unknown-unknown/release/phase2_helloworld.wasm 4 | shard_pre_state: 5 | exec_env_states: 6 | - "0000000000000000000000000000000000000000000000000000000000000000" 7 | shard_blocks: 8 | - env: 0 9 | data: "" 10 | - env: 0 11 | data: "" 12 | shard_post_state: 13 | exec_env_states: 14 | - "0000000000000000000000000000000000000000000000000000000000000000" 15 | deposit_receipts: 16 | - pubkey: "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" 17 | withdrawal_credentials: "0000000000000000000000000000000000000000000000000000000000000000" 18 | amount: 0 19 | signature: "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" --------------------------------------------------------------------------------