├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── bin ├── interpret │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── cli.rs │ │ ├── context.rs │ │ ├── ether.rs │ │ ├── filter.rs │ │ ├── juncture.rs │ │ ├── lib.rs │ │ ├── main.rs │ │ ├── opcode.rs │ │ └── processed.rs ├── operator │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── cli.rs │ │ ├── droplet.rs │ │ └── main.rs └── stator │ ├── Cargo.toml │ ├── README.md │ └── src │ ├── cli.rs │ └── main.rs ├── crates ├── inventory │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── cache.rs │ │ ├── lib.rs │ │ ├── oracle.rs │ │ ├── overlap.rs │ │ ├── rpc.rs │ │ ├── transferrable.rs │ │ ├── types.rs │ │ └── utils.rs ├── multiproof │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── eip1186.rs │ │ ├── lib.rs │ │ ├── node.rs │ │ ├── oracle.rs │ │ ├── proof.rs │ │ └── utils.rs ├── tracer │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── evm.rs │ │ ├── lib.rs │ │ ├── state.rs │ │ └── trace.rs ├── types │ ├── Cargo.toml │ └── src │ │ ├── alias.rs │ │ ├── constants.rs │ │ ├── execution.rs │ │ ├── lib.rs │ │ ├── oracle.rs │ │ ├── proof.rs │ │ ├── state.rs │ │ └── utils.rs └── verify │ ├── Cargo.toml │ ├── README.md │ ├── data │ ├── test_proof_1.json │ ├── test_proof_2.json │ └── test_proof_3.json │ └── src │ ├── eip1186.rs │ ├── lib.rs │ ├── node.rs │ ├── path.rs │ ├── proof.rs │ └── utils.rs ├── data └── blocks │ ├── 17190873 │ ├── block_accessed_state_deduplicated.json │ ├── block_accessed_state_deduplicated.snappy │ ├── block_prestate_trace.json │ ├── block_state_proofs.json │ ├── block_with_transactions.json │ ├── blockhash_opcode_use.json │ ├── prior_block_state_proofs.json │ ├── prior_block_state_proofs.snappy │ └── prior_block_transferrable_state_proofs.ssz_snappy │ ├── 17193183 │ ├── block_accessed_state_deduplicated.json │ ├── block_accessed_state_deduplicated.snappy │ ├── block_prestate_trace.json │ ├── block_state_proofs.json │ ├── block_state_proofs.snappy │ ├── block_with_transactions.json │ ├── prior_block_state_proofs.json │ ├── prior_block_state_proofs.snappy │ └── prior_block_transferrable_state_proofs.ssz_snappy │ ├── 17193270 │ ├── block_accessed_state_deduplicated.json │ ├── block_accessed_state_deduplicated.snappy │ ├── block_prestate_trace.json │ ├── block_state_proofs.json │ ├── block_state_proofs.snappy │ ├── block_with_transactions.json │ ├── prior_block_state_proofs.json │ ├── prior_block_state_proofs.snappy │ └── prior_block_transferrable_state_proofs.ssz_snappy │ ├── 17640079 │ ├── block_accessed_state_deduplicated.json │ ├── block_accessed_state_deduplicated.snappy │ ├── block_prestate_trace.json │ ├── block_with_transactions.json │ ├── blockhash_opcode_use.json │ ├── prior_block_state_proofs.json │ ├── prior_block_state_proofs.snappy │ └── prior_block_transferrable_state_proofs.ssz_snappy │ └── 17683184 │ ├── block_with_transactions.json │ └── prior_block_transferrable_state_proofs.ssz_snappy ├── examples ├── 00_cache_get_block_by_number.rs ├── 01_cache_trace_block.rs ├── 02_deduplicate_state_accesses.rs ├── 03_compress_state_accesses.rs ├── 04_get_proofs.rs ├── 05_compress_proofs.rs ├── 06_measure_inter_proof_overlap.rs ├── 07_stats.rs ├── 08_verify_proofs.rs ├── 09_cache_required_state.rs └── 10_use_proof_to_trace.rs ├── spec ├── interpreted_trace.md ├── required_block_state_format.md └── required_block_state_subprotocol.md ├── src └── lib.rs └── tests └── post_block_state_root.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | Cargo.lock 3 | 4 | # These are backup files generated by rustfmt 5 | **/*.rs.bk 6 | 7 | /target 8 | /Cargo.lock 9 | 10 | # longer example series 11 | /data/blocks/1737* 12 | 13 | # trace demo 14 | /data/blocks/1753* 15 | 16 | # stator demo 17 | /data/blocks/17869* 18 | 19 | notes.md 20 | 21 | debug_data/ -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "archors" 3 | version = "0.1.0" 4 | edition = "2021" 5 | description = "Tools for single Ethereum archival blocks and state proofs" 6 | 7 | [workspace] 8 | members = ["crates/*", "bin/*"] 9 | 10 | [dependencies] 11 | archors_inventory = { path = "crates/inventory" } 12 | archors_multiproof = { path = "crates/multiproof" } 13 | archors_tracer = { path = "crates/tracer" } 14 | archors_types = { path = "crates/types" } 15 | archors_verify = { path = "crates/verify" } 16 | 17 | [workspace.dependencies] 18 | log = "0.4.19" 19 | env_logger = "0.10.0" 20 | 21 | [dev-dependencies] 22 | anyhow = "1.0.69" 23 | ethers = "2.0.4" 24 | log = { workspace = true } 25 | env_logger = { workspace = true } 26 | tokio = { version = "1.26.0", features = ["full"] } 27 | revm = { version = "3.3.0", features = ["serde"] } 28 | -------------------------------------------------------------------------------- /bin/interpret/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "archors_interpret" 3 | version = "0.1.0" 4 | edition = "2021" 5 | description = "Create a readable summary of an EIP-3155 Ethereum transaction trace" 6 | 7 | [dependencies] 8 | alloy-primitives = "0.2.0" 9 | anyhow = "1.0.69" 10 | clap = { version = "4.3.19", features = ["derive"] } 11 | serde = { version = "1.0.152", features = ["derive"] } 12 | serde_json = "1.0.94" 13 | thiserror = "1.0.40" 14 | -------------------------------------------------------------------------------- /bin/interpret/README.md: -------------------------------------------------------------------------------- 1 | ## Archors interpret 2 | 3 | Takes a stream of a transaction trace from stdin and produces 4 | a stream to stdout. 5 | 6 | The output is readable so one can see what the transaction did. 7 | 8 | The function is pure in that only the transaction trace is used 9 | 10 | ```command 11 | cargo run --release -p archors_interpret -- --help 12 | ``` 13 | ## Flags 14 | 15 | The interpreter may be passed different trace styles, as long as they are NDJSON. 16 | 17 | ### `debug_traceTransaction` and `debug_traceBlockByNumber` 18 | 19 | This will be a JSON object and may be converted to NDJSON as follows: 20 | ``` 21 | | jq '.["result"]["structLogs"][]' -c 22 | ``` 23 | The `--debug` flag must be used for this kind of trace. 24 | For example: 25 | ``` 26 | curl -X POST -H "Content-Type: application/json" --data '{"jsonrpc": "2.0", "method": "debug_traceTransaction", "params": ["0x8f5dd8107e2efce82759c9bbf34ac7bab49a2992b2f2ee6fc9d510f5e2490680", {"disableMemory": true}], "id":1}' http://127.0.0.1:8545 \ 27 | | jq '.["result"]["structLogs"][]' -c \ 28 | | cargo run --release -p archors_interpret 29 | ``` 30 | 31 | ### EIP-3155 32 | 33 | This will be NDJSON by default. The `eip3155` flag is required for this style. 34 | 35 | This command gets a trace then interprets it. 36 | ```command 37 | cargo run --release --example 10_use_proof_to_trace | cargo run --release -p archors_interpret eip3155 38 | ``` 39 | 40 | ## Examples 41 | 42 | ### Multiple contract creations 43 | 44 | Transaction (index 185) in block 17190873. 45 | 46 | Unreadable trace (via REVM EIP3155 inspector): 47 | ``` 48 | {"pc":0,"op":96,"gas":"0xe636b","gasCost":"0x3","memSize":0,"stack":[],"depth":1,"opName":"PUSH1"} 49 | {"pc":2,"op":96,"gas":"0xe6368","gasCost":"0x3","memSize":0,"stack":["0x80"],"depth":1,"opName":"PUSH1"} 50 | {"pc":4,"op":82,"gas":"0xe6365","gasCost":"0xc","memSize":0,"stack":["0x80","0x40"],"depth":1,"opName":"MSTORE"} 51 | ... 52 | ~7000 lines omitted 53 | ... 54 | {"pc":133,"op":86,"gas":"0x4ec9a","gasCost":"0x8","memSize":384,"stack":["0x8467be0d","0x43"],"depth":1,"opName":"JUMP"} 55 | {"pc":67,"op":91,"gas":"0x4ec92","gasCost":"0x1","memSize":384,"stack":["0x8467be0d"],"depth":1,"opName":"JUMPDEST"} 56 | {"pc":68,"op":0,"gas":"0x4ec91","gasCost":"0x0","memSize":384,"stack":["0x8467be0d"],"depth":1,"opName":"STOP"} 57 | {"output":"0x","gasUsed":"0x4ec91"} 58 | ``` 59 | 60 | Interpretation: 61 | ``` 62 | Function 0x8467be0d 63 | Deploy contract (CREATE) 64 | Contract (CALL) using code and storage at created contract (index 0), message.sender is tx.from 65 | Function 0x84bc8c48 66 | Function 0x84bc8c48 67 | Log3 created (0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef) 68 | Stopped 69 | Contract (STATICCALL) using code and storage at created contract (index 0), message.sender is tx.from 70 | Function 0x70a08231 71 | Function 0x70a08231 72 | Function 0x70a08231 73 | Returned 74 | Contract (CALL) using code and storage at created contract (index 0), message.sender is tx.from 75 | Function 0xa9059cbb 76 | Function 0xa9059cbb 77 | Log3 created (0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef) 78 | Returned 79 | Returned. Created contract (index 0) has address 0xba47611fb35365ffea81803c7163aa9a49b01110 80 | Deploy contract (CREATE) 81 | Contract (CALL) using code and storage at created contract (index 1), message.sender is tx.from 82 | Function 0x84bc8c48 83 | Function 0x84bc8c48 84 | Log3 created (0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef) 85 | Stopped 86 | Contract (STATICCALL) using code and storage at created contract (index 1), message.sender is tx.from 87 | Function 0x70a08231 88 | Function 0x70a08231 89 | Function 0x70a08231 90 | Returned 91 | Contract (CALL) using code and storage at created contract (index 1), message.sender is tx.from 92 | Function 0xa9059cbb 93 | Function 0xa9059cbb 94 | Log3 created (0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef) 95 | Returned 96 | Returned. Created contract (index 1) has address 0x9e1c11e99f9a51171defc026134a6c08f95da292 97 | Deploy contract (CREATE) 98 | Contract (CALL) using code and storage at created contract (index 2), message.sender is tx.from 99 | Function 0x84bc8c48 100 | Function 0x84bc8c48 101 | Log3 created (0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef) 102 | Stopped 103 | Contract (STATICCALL) using code and storage at created contract (index 2), message.sender is tx.from 104 | Function 0x70a08231 105 | Function 0x70a08231 106 | Function 0x70a08231 107 | Returned 108 | Contract (CALL) using code and storage at created contract (index 2), message.sender is tx.from 109 | Function 0xa9059cbb 110 | Function 0xa9059cbb 111 | Log3 created (0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef) 112 | Returned 113 | Returned. Created contract (index 2) has address 0x7766e2545ca92a0b7918a67f3ef2a05aa9198664 114 | Deploy contract (CREATE) 115 | Contract (CALL) using code and storage at created contract (index 3), message.sender is tx.from 116 | Function 0x84bc8c48 117 | Function 0x84bc8c48 118 | Log3 created (0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef) 119 | Stopped 120 | Contract (STATICCALL) using code and storage at created contract (index 3), message.sender is tx.from 121 | Function 0x70a08231 122 | Function 0x70a08231 123 | Function 0x70a08231 124 | Returned 125 | Contract (CALL) using code and storage at created contract (index 3), message.sender is tx.from 126 | Function 0xa9059cbb 127 | Function 0xa9059cbb 128 | Log3 created (0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef) 129 | Returned 130 | Returned. Created contract (index 3) has address 0xcca843a73558b87ea92c288cbbcdaf0323a49033 131 | Deploy contract (CREATE) 132 | Contract (CALL) using code and storage at created contract (index 4), message.sender is tx.from 133 | Function 0x84bc8c48 134 | Function 0x84bc8c48 135 | Log3 created (0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef) 136 | Stopped 137 | Contract (STATICCALL) using code and storage at created contract (index 4), message.sender is tx.from 138 | Function 0x70a08231 139 | Function 0x70a08231 140 | Function 0x70a08231 141 | Returned 142 | Contract (CALL) using code and storage at created contract (index 4), message.sender is tx.from 143 | Function 0xa9059cbb 144 | Function 0xa9059cbb 145 | Log3 created (0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef) 146 | Returned 147 | Returned. Created contract (index 4) has address 0xe135896367ca4140789ce18145e24e945f682466 148 | Transaction finished (STOP) 149 | Transaction 0 complete. Transaction summary, gas used: 0x4ec91, output: 0x 150 | ``` 151 | 152 | ### Example: Multiple ether sends via CALL 153 | Transaction (index 95) in block 17190873. 154 | 155 | Trace not shown (see above for example). 156 | 157 | Interpretation 158 | ``` 159 | Function 0x1a1da075 160 | Function 0x1a1da075 161 | Function 0x1a1da075 162 | 0.003 ether paid to 0xfb48076ec5726fe0865e7c91ef0e4077a5040c7a (CALL to codeless account) from tx.from 163 | 0.006 ether paid to 0x2d9258a8eae7753b1990846de39b740bc04f25a1 (CALL to codeless account) from tx.from 164 | 0.013 ether paid to 0xa5730f3b442024d66c2ca7f6cc37e696edba9663 (CALL to codeless account) from tx.from 165 | 0.013 ether paid to 0xb47ece059072c144d3ac84d041d390be02f57478 (CALL to codeless account) from tx.from 166 | 0.018 ether paid to 0xaf94dd6de9b0d68e6dbf7eb43060885db218d9f4 (CALL to codeless account) from tx.from 167 | 0.023 ether paid to 0xde4fd23d4c0bb543799fc1aebf02044166be1e45 (CALL to codeless account) from tx.from 168 | 0.026 ether paid to 0xbd5678481143b4f757d240deef122a954f480a05 (CALL to codeless account) from tx.from 169 | 0.031 ether paid to 0x5be8bdc96c423f265fc589ff8611e4c72ee94dca (CALL to codeless account) from tx.from 170 | 0.032 ether paid to 0x13839ef097d42dab0a15164f13af6774748b7682 (CALL to codeless account) from tx.from 171 | 0.053 ether paid to 0xc96e4ea42a5ccacc75a2f10a6bacdf4d93401486 (CALL to codeless account) from tx.from 172 | 0.099 ether paid to 0xf9471a1af8836373208ebb96cdb2f14091b61c57 (CALL to codeless account) from tx.from 173 | 0.12 ether paid to 0x86e5781d43334b5dc892ca1c35ad15b82dc2df3b (CALL to codeless account) from tx.from 174 | 0.12 ether paid to 0x83988265eb9dfac380575fb2c37f72422aac3df6 (CALL to codeless account) from tx.from 175 | 0.15 ether paid to 0x5d516888c067e6176d148357bf5adffef263e262 (CALL to codeless account) from tx.from 176 | 0.25 ether paid to 0x4103532d8f262218db143fc2747e836c6044fa22 (CALL to codeless account) from tx.from 177 | 0.43 ether paid to 0xa36f06fc5a28768ebe9715c787122995d80dec0 (CALL to codeless account) from tx.from 178 | 0.58 ether paid to 0xd9e1d0ff2a71891f22b638015921d61ef0fcce41 (CALL to codeless account) from tx.from 179 | 0.59 ether paid to 0x13a161e0742f601a16b765abb510149e4b5a3d77 (CALL to codeless account) from tx.from 180 | 0.65 ether paid to 0xcc54441169904c7330660bf07770c6e66bbaff4f (CALL to codeless account) from tx.from 181 | 0.83 ether paid to 0xa158b6bed1c4bc657568b2e5136328a3638a71dd (CALL to codeless account) from tx.from 182 | 1.2 ether paid to 0x30a4639850b3ddeaaca4f06280aa751682f11382 (CALL to codeless account) from tx.from 183 | 1.5 ether paid to 0x68388d48b5baf99755ea9c685f15b0528edf90b6 (CALL to codeless account) from tx.from 184 | Transaction finished (STOP) 185 | Transaction 0 complete. Transaction summary, gas used: 0x1aa70e, output: 0x 186 | ``` 187 | 188 | ## Trace naming differences 189 | One must be aware of the difference between the fields in different tracing kinds: 190 | 191 | `debug_traceBlockByNumber` 192 | ``` 193 | {"pc":25,"op":"JUMPI","gas":630770,"gasCost":10,"depth":2,"stack":["0x0","0x454"]} 194 | ``` 195 | EIP-3155 trace: 196 | ``` 197 | {"pc":133,"op":86,"gas":"0x4ec9a","gasCost":"0x8","memSize":384,"stack":["0x8467be0d","0x43"],"depth":1,"opName":"JUMP"} 198 | ``` 199 | -------------------------------------------------------------------------------- /bin/interpret/src/cli.rs: -------------------------------------------------------------------------------- 1 | //! For Command Line Interface for archors_interpret 2 | 3 | use clap::{Parser, ValueEnum}; 4 | 5 | /// Interpret an EVM trace. To use: Pipe NDJSON trace to the app. 6 | /// 7 | /// NDJSON can be made from JSON-RPC by: 8 | /// ``` 9 | /// | jq '.["result"]["structLogs"][]' -c | 10 | /// ``` 11 | /// (for a single transaction) or 12 | /// ``` 13 | /// | jq '.["result"][]["result"]["structLogs"][]' -c | 14 | /// ``` 15 | /// (for a whole block) 16 | #[derive(Parser, Debug)] 17 | #[command(author, version, about, long_about = None)] 18 | pub struct AppArgs { 19 | #[clap(value_enum, default_value_t=ModeFlag::Debug)] 20 | pub trace_style: ModeFlag, 21 | } 22 | 23 | /// Different traces have different fields (e.g., op vs opName) 24 | /// 25 | /// For example 26 | /// - revm with EIP-3155 tracer inspector 27 | /// - debug_traceTransaction 28 | #[derive(ValueEnum, Clone, Debug, PartialEq)] 29 | pub enum ModeFlag { 30 | /// EIP-3155 style trace 31 | Eip3155, 32 | /// For debug_traceBlockByNumber or debug_traceTransaction (see NDJSON instructions) 33 | Debug, 34 | } 35 | -------------------------------------------------------------------------------- /bin/interpret/src/context.rs: -------------------------------------------------------------------------------- 1 | //! Information important at the time of a single opcode step in the EVM. 2 | 3 | use serde::{Deserialize, Serialize}; 4 | use std::fmt::Display; 5 | 6 | use crate::processed::{is_precompile, ProcessedStep}; 7 | 8 | use thiserror::Error; 9 | 10 | #[derive(Debug, Error)] 11 | pub enum ContextError { 12 | #[error("No parent call context present to access")] 13 | AbsentContext, 14 | #[error("serde_json error {0}")] 15 | SerdeJson(#[from] serde_json::Error), 16 | } 17 | 18 | /// Context during EVM execution. 19 | #[derive(Clone, Debug, Deserialize, Serialize)] 20 | #[serde(rename_all = "camelCase")] 21 | pub struct Context { 22 | /// Address where the code being executed resides 23 | pub code_address: Address, 24 | /// Address message.sender resolves at this time. 25 | pub message_sender: Address, 26 | /// Address that storage modifications affect. 27 | pub storage_address: Address, 28 | /// Create-related information 29 | pub create_data: Option, 30 | } 31 | 32 | /// Create-related context 33 | #[derive(Clone, Debug, Deserialize, Serialize)] 34 | #[serde(rename_all = "camelCase")] 35 | pub struct CreateData { 36 | /// Contract creation index for transaction (first = 0, ...) 37 | /// 38 | /// Every use of CREATE/CREATE2 increments the index. Keeps track of addresses 39 | /// when they are returned. 40 | pub index: usize, 41 | } 42 | 43 | /// A contract may 44 | #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] 45 | #[serde(rename_all = "camelCase")] 46 | pub enum Address { 47 | /// Either existing or deployed via tx.to being nil (not using CREATE/CREATE2) 48 | Standard(String), 49 | /// New contract via CREATE/CREATE2, address not yet visible on the stack. 50 | /// 51 | /// The first created contract has index 0, increments thereafter. 52 | CreatedPending { index: usize }, 53 | } 54 | 55 | /// An opcode may cause a change to the context that will apply to the next 56 | /// EVM step. 57 | pub enum ContextUpdate { 58 | /// Context does not need changing. 59 | None, 60 | /// Context needs to be added (e.g., entering a contract call). 61 | Add(Context), 62 | /// Current context needs to be removed (e.g., returning from a contract call). 63 | Remove, 64 | /// A new context is needed for the next transaction. 65 | Reset, 66 | } 67 | 68 | /// If a opcode affects the call context, determine the new context it would create. 69 | /// 70 | /// It may ultimetely not be applied (e.g., CALL to EOA). 71 | pub fn get_pending_context_update( 72 | context: &[Context], 73 | step: &ProcessedStep, 74 | create_counter: &mut usize, 75 | ) -> Result { 76 | match step { 77 | ProcessedStep::Call { to, value: _ } | ProcessedStep::StaticCall { to } => { 78 | if is_precompile(to) { 79 | return Ok(ContextUpdate::None); 80 | } 81 | let previous = context.last().ok_or(ContextError::AbsentContext)?; 82 | Ok(ContextUpdate::Add(Context { 83 | code_address: Address::Standard(to.clone()), 84 | message_sender: previous.message_sender.clone(), 85 | storage_address: Address::Standard(to.clone()), 86 | create_data: None, 87 | })) 88 | } 89 | ProcessedStep::CallCode { to, value: _ } => { 90 | if is_precompile(to) { 91 | return Ok(ContextUpdate::None); 92 | } 93 | let previous = context.last().ok_or(ContextError::AbsentContext)?; 94 | Ok(ContextUpdate::Add(Context { 95 | code_address: Address::Standard(to.clone()), 96 | message_sender: previous.code_address.clone(), // important 97 | storage_address: previous.storage_address.clone(), 98 | create_data: None, 99 | })) 100 | } 101 | ProcessedStep::DelegateCall { to, value: _ } => { 102 | if is_precompile(to) { 103 | return Ok(ContextUpdate::None); 104 | } 105 | let previous = context.last().ok_or(ContextError::AbsentContext)?; 106 | Ok(ContextUpdate::Add(Context { 107 | code_address: Address::Standard(to.clone()), 108 | message_sender: previous.message_sender.clone(), // important 109 | storage_address: previous.storage_address.clone(), 110 | create_data: None, 111 | })) 112 | } 113 | ProcessedStep::Create | ProcessedStep::Create2 => { 114 | let previous = context.last().ok_or(ContextError::AbsentContext)?; 115 | let update = ContextUpdate::Add(Context { 116 | code_address: Address::CreatedPending { 117 | index: *create_counter, 118 | }, 119 | message_sender: previous.message_sender.clone(), 120 | storage_address: Address::CreatedPending { 121 | index: *create_counter, 122 | }, 123 | create_data: Some(CreateData { 124 | index: *create_counter, 125 | }), 126 | }); 127 | // The next contract created will have a different index. 128 | *create_counter += 1; 129 | Ok(update) 130 | } 131 | ProcessedStep::Invalid 132 | | ProcessedStep::Return { stack_top_next: _ } 133 | | ProcessedStep::Revert 134 | | ProcessedStep::SelfDestruct 135 | | ProcessedStep::Stop { stack_top_next: _ } => Ok(ContextUpdate::Remove), 136 | ProcessedStep::TxFinished(_) => Ok(ContextUpdate::Reset), 137 | _ => Ok(ContextUpdate::None), 138 | } 139 | } 140 | 141 | /// Apply pending context to the current step. 142 | pub fn apply_pending_context(context: &mut Vec, pending_context: &mut ContextUpdate) { 143 | match &pending_context { 144 | ContextUpdate::None => {} 145 | ContextUpdate::Add(pending) => { 146 | context.push(pending.clone()); 147 | } 148 | ContextUpdate::Remove => { 149 | context.pop().unwrap(); 150 | } 151 | ContextUpdate::Reset => { 152 | context.clear(); 153 | context.push(Context::default()); 154 | } 155 | } 156 | *pending_context = ContextUpdate::None; 157 | } 158 | 159 | impl Default for Context { 160 | fn default() -> Self { 161 | Self { 162 | code_address: Address::Standard("tx.to".to_string()), 163 | message_sender: Address::Standard("tx.from".to_string()), 164 | storage_address: Address::Standard("tx.to".to_string()), 165 | create_data: None, 166 | } 167 | } 168 | } 169 | 170 | impl Display for Context { 171 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 172 | if self.code_address == self.storage_address { 173 | write!( 174 | f, 175 | "using code and storage at {}, message.sender is {}", 176 | self.code_address, self.message_sender 177 | ) 178 | } else { 179 | write!( 180 | f, 181 | "using code at {}, storage at {}, message.sender is {}", 182 | self.code_address, self.storage_address, self.message_sender 183 | ) 184 | } 185 | } 186 | } 187 | 188 | impl Display for Address { 189 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 190 | match self { 191 | Address::Standard(address) => write!(f, "{address}"), 192 | Address::CreatedPending { index } => write!(f, "created contract (index {index})"), 193 | } 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /bin/interpret/src/ether.rs: -------------------------------------------------------------------------------- 1 | //! For representing a ether value. 2 | 3 | use alloy_primitives::U256; 4 | use serde::{Deserialize, Serialize}; 5 | use std::{fmt::Display, str::FromStr}; 6 | 7 | use thiserror::Error; 8 | 9 | #[derive(Debug, Error)] 10 | pub enum EtherError { 11 | #[error("serde_json error {0}")] 12 | SerdeJson(#[from] serde_json::Error), 13 | #[error("Unable to get next char from string")] 14 | EndOfString, 15 | } 16 | 17 | /// Quantity in wei 18 | /// 19 | /// As hex-value like 0x1 (1 wei) 20 | #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] 21 | #[serde(rename_all = "camelCase")] 22 | pub struct Ether(pub String); 23 | 24 | impl Display for Ether { 25 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 26 | write!( 27 | f, 28 | "{}", 29 | to_ether_pretty(&self.0).map_err(|_| std::fmt::Error)? 30 | ) 31 | } 32 | } 33 | 34 | /// Human relatable approximate amount. 35 | /// 36 | /// E.g., ~1.2 ether, ~0.0020 ether (no rounding) 37 | /// 38 | /// Input is hex-string (0x-prefix) in wei. 39 | fn to_ether_pretty(hex_wei: &str) -> Result { 40 | let val = U256::from_str(hex_wei).unwrap(); 41 | let num = val.to_string(); 42 | let mut chars = num.chars(); 43 | let decimals = val.approx_log10() as u64; 44 | let ether = match decimals { 45 | 0_u64..=14_u64 => "<0.001".to_string(), 46 | 15 => format!("0.00{}", next_char(&mut chars)?), 47 | 16 => format!("0.0{}{}", next_char(&mut chars)?, next_char(&mut chars)?), 48 | 17 => format!("0.{}{}", next_char(&mut chars)?, next_char(&mut chars)?), 49 | 18 => format!("{}.{}", next_char(&mut chars)?, next_char(&mut chars)?), 50 | 19 => format!( 51 | "{}{}.{}", 52 | next_char(&mut chars)?, 53 | next_char(&mut chars)?, 54 | next_char(&mut chars)? 55 | ), 56 | 20 => format!( 57 | "{}{}{}.{}", 58 | next_char(&mut chars)?, 59 | next_char(&mut chars)?, 60 | next_char(&mut chars)?, 61 | next_char(&mut chars)? 62 | ), 63 | x @ 21_u64..=u64::MAX => format!("{val:.*}", x as usize), 64 | }; 65 | Ok(ether) 66 | } 67 | 68 | /// Gets next char from a string. 69 | fn next_char(chars: &mut std::str::Chars<'_>) -> Result { 70 | chars.next().ok_or(EtherError::EndOfString) 71 | } 72 | 73 | #[cfg(test)] 74 | mod test { 75 | use super::*; 76 | 77 | #[test] 78 | fn test_to_approx_ether() { 79 | // 12648828125000 wei (0.000012) 80 | assert_eq!(to_ether_pretty("0xb8108e83f48").unwrap(), "<0.001"); 81 | // 202381250000000 wei (0.00020) 82 | assert_eq!(to_ether_pretty("0xb8108e83f480").unwrap(), "<0.001"); 83 | // 3238100000000000 wei (0.0032) 84 | assert_eq!(to_ether_pretty("0xb8108e83f4800").unwrap(), "0.003"); 85 | // 6267810000000000 wei 86 | assert_eq!(to_ether_pretty("0x16448a3c91d400").unwrap(), "0.006"); 87 | // 13234510000000000 wei 88 | assert_eq!(to_ether_pretty("0x2f04b77b538c00").unwrap(), "0.013"); 89 | // 657311630000000000 wei 90 | assert_eq!(to_ether_pretty("0x91f3d71e4e20c00").unwrap(), "0.65"); 91 | // 839991880000000000 wei 92 | assert_eq!(to_ether_pretty("0xba8402a159cd000").unwrap(), "0.83"); 93 | // 1597427080000000000 wei 94 | assert_eq!(to_ether_pretty("0x162b337739fd5000").unwrap(), "1.5"); 95 | // 1597427080000000000 wei 96 | assert_eq!(to_ether_pretty("0x162b337739fd5000").unwrap(), "1.5"); 97 | // 25558833280000000000 wei 98 | assert_eq!(to_ether_pretty("0x162b337739fd50000").unwrap(), "25.5"); 99 | // 408941332480000000000 wei 100 | assert_eq!(to_ether_pretty("0x162b337739fd500000").unwrap(), "408.9"); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /bin/interpret/src/filter.rs: -------------------------------------------------------------------------------- 1 | //! Processes a transaction trace to produce a summary. The use case is 2 | //! to be able to pipe any EIP-3155 output to this library. 3 | //! 4 | //! The input is new-line delineated JSON stream of EVM steps from stdin. 5 | //! 6 | //! Note that a line in a trace is pre-application of the opcode. E.g., the opcode will 7 | //! use the values in the stack on the same line. 8 | 9 | // Resources 10 | /// - EVMOne: https://github.com/ethereum/evmone/pull/325 11 | /// - Test case: https://github.com/Arachnid/EIPs/commit/28e73864f72d66b5dd31fdb5f7502f0327075131 12 | use std::io::{BufRead, Write}; 13 | 14 | use thiserror::Error; 15 | 16 | use crate::{ 17 | cli::ModeFlag, 18 | context::{apply_pending_context, get_pending_context_update, Context, ContextUpdate}, 19 | juncture::Juncture, 20 | opcode::{EvmOutput, EvmStepDebug, EvmStepEip3155, TraceLine}, 21 | processed::ProcessedStep, 22 | }; 23 | 24 | #[derive(Debug, Error)] 25 | pub enum FilterError { 26 | #[error("serde_json error {0}")] 27 | SerdeJson(#[from] serde_json::Error), 28 | } 29 | 30 | pub fn process_trace(trace_style: ModeFlag) { 31 | let stdin = std::io::stdin(); 32 | let reader = stdin.lock(); 33 | 34 | let mut transaction_counter = 0; 35 | 36 | let mut peekable_lines = reader 37 | .lines() 38 | .filter_map(|line| match line { 39 | Ok(l) => Some(l), 40 | Err(_) => None, // Bad stdin line 41 | }) 42 | .filter_map(|line| { 43 | match trace_style { 44 | ModeFlag::Eip3155 => { 45 | let json = serde_json::from_str::(&line); 46 | match json { 47 | Ok(step) => Some(TraceLine::StepEip3155(step)), 48 | Err(_) => { 49 | // Not an EvmStep (e.g., output) 50 | match serde_json::from_str::(&line) { 51 | Ok(output) => Some(TraceLine::Output(output)), 52 | Err(_) => None, // Not an EvmStep or Output 53 | } 54 | } 55 | } 56 | } 57 | ModeFlag::Debug => { 58 | let json = serde_json::from_str::(&line); 59 | match json { 60 | Ok(step) => Some(TraceLine::StepDebug(step)), 61 | Err(_) => { 62 | // Not an EvmStep (e.g., output) 63 | match serde_json::from_str::(&line) { 64 | Ok(output) => Some(TraceLine::Output(output)), 65 | Err(_) => None, // Not an EvmStep or Output 66 | } 67 | } 68 | } 69 | } 70 | } 71 | }) 72 | .peekable(); 73 | 74 | let mut context: Vec = vec![Context::default()]; 75 | let mut pending_context = ContextUpdate::None; 76 | let mut create_counter: usize = 0; 77 | 78 | let mut stdout = Box::new(std::io::stdout()); 79 | 80 | while let Some(unprocessed_step) = peekable_lines.next() { 81 | // Add processed information to step. 82 | // Exclude uninteresting steps (ADD, ISZERO, ...) 83 | let Some(mut processed) = process_step(&unprocessed_step) else { 84 | continue; 85 | }; 86 | 87 | // Get the stack from the peek and include it in the processed step. 88 | if let Some(peek) = peekable_lines.peek() { 89 | processed.add_peek(&unprocessed_step, peek); 90 | } 91 | // Update transaction counter. 92 | let tx_count = transaction_counter; 93 | if let ProcessedStep::TxSummary { .. } = processed { 94 | transaction_counter += 1; 95 | }; 96 | 97 | // Update context 98 | apply_pending_context(&mut context, &mut pending_context); 99 | pending_context = 100 | get_pending_context_update(&context, &processed, &mut create_counter).unwrap(); 101 | 102 | // Group processed and raw information together. 103 | if context.is_empty() {continue} 104 | let juncture = Juncture::create(&processed, &unprocessed_step, &context, tx_count); 105 | //juncture.print_json(); 106 | //juncture.print_pretty(); 107 | match writeln!(stdout, "{}", juncture) { 108 | Ok(_) => {} 109 | Err(_) => { 110 | // Could not write to stdout 111 | break; 112 | } 113 | } 114 | } 115 | } 116 | 117 | /// If a line from the trace is of interest, a new representation is created. 118 | fn process_step(step: &TraceLine) -> Option { 119 | match step { 120 | TraceLine::StepEip3155(evm_step) => match ProcessedStep::try_from_evm_step(evm_step) { 121 | Ok(ProcessedStep::Uninteresting) => None, 122 | Ok(processed_step) => Some(processed_step), 123 | Err(_) => None, 124 | }, 125 | TraceLine::StepDebug(evm_step) => match ProcessedStep::try_from_evm_step(evm_step) { 126 | Ok(ProcessedStep::Uninteresting) => None, 127 | Ok(processed_step) => Some(processed_step), 128 | Err(_) => None, 129 | }, 130 | TraceLine::Output(evm_output) => Some(ProcessedStep::from(evm_output)), 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /bin/interpret/src/juncture.rs: -------------------------------------------------------------------------------- 1 | //! For an important occurence during a tranasction. Includes things 2 | //! that a human might find worthwhile being aware of. 3 | 4 | use serde::Serialize; 5 | use serde_json::json; 6 | use std::fmt::Display; 7 | 8 | use crate::{ 9 | context::Context, 10 | opcode::TraceLine, 11 | processed::{ProcessedStep, StackTopNext}, 12 | }; 13 | 14 | use thiserror::Error; 15 | 16 | #[derive(Debug, Error)] 17 | pub enum JunctureError { 18 | #[error("serde_json error {0}")] 19 | SerdeJson(#[from] serde_json::Error), 20 | } 21 | 22 | /// A noteworthy occurrence whose summary might be meaningful. 23 | #[derive(Debug, Serialize)] 24 | #[serde(rename_all = "camelCase")] 25 | pub(crate) struct Juncture<'a> { 26 | pub action: &'a ProcessedStep, 27 | #[serde(skip_serializing)] 28 | pub _raw_trace: &'a TraceLine, 29 | pub current_context: &'a Context, 30 | pub context_depth: Option, 31 | pub tx_count: usize, 32 | } 33 | 34 | impl Juncture<'_> { 35 | /// Prints to stdout ina minimal, human readable format. 36 | pub fn _print_pretty(&self) { 37 | println!("{self}"); 38 | } 39 | /// Prints in newline delimited JSON. 40 | /// 41 | /// Useful if another system will ingest the stream from stdout. 42 | pub fn _print_json(&self) { 43 | println!("{}", json!(self)); 44 | } 45 | /// Prints dense information, useful for debugging. 46 | pub fn _print_debug(&self) { 47 | println!("{self:?}"); 48 | } 49 | pub fn create<'a>( 50 | processed: &'a ProcessedStep, 51 | unprocessed_step: &'a TraceLine, 52 | context: &'a [Context], 53 | tx_count: usize, 54 | ) -> Juncture<'a> { 55 | Juncture { 56 | action: processed, 57 | _raw_trace: unprocessed_step, 58 | current_context: context.last().unwrap(), 59 | context_depth: { 60 | let depth = unprocessed_step.depth(); 61 | if depth == 0 { 62 | None 63 | } else { 64 | Some(depth as usize) 65 | } 66 | }, 67 | tx_count, 68 | } 69 | } 70 | } 71 | 72 | impl Display for Juncture<'_> { 73 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 74 | use ProcessedStep::*; 75 | if let Some(depth) = self.context_depth { 76 | for _ in 0..depth { 77 | write!(f, "\t")?; 78 | } 79 | } 80 | 81 | match self.action { 82 | Call { to: _, value: _ } 83 | | CallCode { to: _, value: _ } 84 | | DelegateCall { to: _, value: _ } 85 | | StaticCall { to: _ } => { 86 | write!(f, "{} {}", self.action, self.current_context) 87 | } 88 | PayCall { 89 | to: _, 90 | value: _, 91 | opcode: _, 92 | } => write!( 93 | f, 94 | "{} from {}", 95 | self.action, self.current_context.message_sender 96 | ), 97 | TxFinished(_) => write!(f, "{}", self.action), 98 | TxSummary { 99 | output: _, 100 | gas_used: _, 101 | } => write!(f, "Transaction {} complete. {}", self.tx_count, self.action), 102 | Return { stack_top_next } | Stop { stack_top_next } => { 103 | // Check if in create context. If so, display created contract address. 104 | match &self.current_context.create_data { 105 | Some(create_data) => { 106 | let address = match stack_top_next { 107 | StackTopNext::Some(addr) => addr, 108 | _ => todo!("Error, expected address on stack"), 109 | }; 110 | write!( 111 | f, 112 | "{}. Created contract (index {}) has address {}", 113 | self.action, create_data.index, address 114 | ) 115 | } 116 | None => write!(f, "{}", self.action), 117 | } 118 | } 119 | _ => write!(f, "{}", self.action), 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /bin/interpret/src/lib.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /bin/interpret/src/main.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | 3 | pub(crate) mod cli; 4 | pub(crate) mod context; 5 | pub(crate) mod ether; 6 | pub mod filter; 7 | pub(crate) mod juncture; 8 | pub(crate) mod opcode; 9 | pub(crate) mod processed; 10 | 11 | use clap::Parser; 12 | use cli::AppArgs; 13 | pub use filter::process_trace; 14 | /// Produces a summary of a transaction trace by processing it as a stream 15 | /// ```command 16 | /// cargo run --release --example 09_use_proof | cargo run --release -p archors_interpret 17 | /// ``` 18 | fn main() -> Result<()> { 19 | let args = AppArgs::parse(); 20 | process_trace(args.trace_style); 21 | Ok(()) 22 | } 23 | -------------------------------------------------------------------------------- /bin/interpret/src/opcode.rs: -------------------------------------------------------------------------------- 1 | //! For single EVM instruction/opcode representations from a transaction trace. 2 | 3 | use serde::{Deserialize, Serialize}; 4 | 5 | #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] 6 | #[serde(rename_all = "camelCase")] 7 | pub(crate) enum TraceLine { 8 | StepDebug(EvmStepDebug), 9 | StepEip3155(EvmStepEip3155), 10 | Output(EvmOutput), 11 | } 12 | impl TraceLine { 13 | pub(crate) fn depth(&self) -> u64 { 14 | match self { 15 | TraceLine::StepDebug(s) => *s.depth(), 16 | TraceLine::StepEip3155(s) => *s.depth(), 17 | TraceLine::Output(_) => 0, 18 | } 19 | } 20 | 21 | pub(crate) fn same_depth(&self, other: &TraceLine) -> bool { 22 | self.depth() == other.depth() 23 | } 24 | } 25 | 26 | #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] 27 | #[serde(rename_all = "camelCase")] 28 | pub(crate) struct EvmStepEip3155 { 29 | pub(crate) pc: u64, 30 | pub(crate) op: u64, 31 | pub(crate) gas: String, 32 | pub(crate) gas_cost: String, 33 | pub(crate) mem_size: u64, 34 | pub(crate) stack: Vec, 35 | pub(crate) depth: u64, 36 | pub(crate) op_name: String, 37 | pub(crate) memory: Option>, 38 | } 39 | 40 | #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] 41 | #[serde(rename_all = "camelCase")] 42 | pub(crate) struct EvmOutput { 43 | pub(crate) output: String, 44 | pub(crate) gas_used: String, 45 | } 46 | 47 | #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] 48 | #[serde(rename_all = "camelCase")] 49 | pub(crate) struct EvmStepDebug { 50 | pub(crate) pc: u64, 51 | pub(crate) op: String, 52 | pub(crate) gas: u64, 53 | pub(crate) gas_cost: u64, 54 | pub(crate) depth: u64, 55 | pub(crate) stack: Vec, 56 | pub(crate) memory: Option>, 57 | } 58 | 59 | pub trait EvmStep { 60 | fn op_name(&self) -> &str; 61 | 62 | fn stack(&self) -> &[String]; 63 | 64 | fn depth(&self) -> &u64; 65 | } 66 | 67 | impl EvmStep for EvmStepDebug { 68 | fn op_name(&self) -> &str { 69 | &self.op 70 | } 71 | 72 | fn stack(&self) -> &[String] { 73 | &self.stack 74 | } 75 | 76 | fn depth(&self) -> &u64 { 77 | &self.depth 78 | } 79 | } 80 | 81 | impl EvmStep for EvmStepEip3155 { 82 | fn op_name(&self) -> &str { 83 | &self.op_name 84 | } 85 | 86 | fn stack(&self) -> &[String] { 87 | &self.stack 88 | } 89 | 90 | fn depth(&self) -> &u64 { 91 | &self.depth 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /bin/operator/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "archors_operator" 3 | version = "0.1.0" 4 | edition = "2021" 5 | description = "Redirect stdin lines to fall from the top of the terminal" 6 | 7 | [dependencies] 8 | anyhow = "1.0.69" 9 | clap = { version = "4.3.19", features = ["derive"] } 10 | crossterm = "0.26.1" 11 | ctrlc = "3.4.0" 12 | rand = "0.8.5" 13 | -------------------------------------------------------------------------------- /bin/operator/README.md: -------------------------------------------------------------------------------- 1 | ## Operator 2 | 3 | Takes lines from stdin and makes them trickle down the terminal. 4 | 5 | 6 | ### EVM data 7 | EVM tracing can be sent to stdin, so this can be piped for an aesthetic representation 8 | of real data. 9 | 10 | This works if your node is on a different computer too. In this case, 11 | open a separate terminal and run the following. This will redirect any JSON-RPC 12 | calls on the current computer to the node. 13 | ```command 14 | ssh -N -L 8545:127.0.0.1:8545 15 | ``` 16 | Leave that terminal and open a separate terminal, the operator can be run there. 17 | 18 | 19 | ### Flags 20 | See flags here: 21 | ```command 22 | cargo run --release -p archors_operator -- --help 23 | ``` 24 | 25 | ### `debug_traceBlockByNumber` and `debug_traceBlockByNumber` 26 | 27 | 1. Call the node 28 | - Be careful to use `{"disableMemory": true}` or `{"enableMemory": false}` depending on the client. 29 | - Use curl with `--silent`/`-s` 30 | - Use cargo with `--quiet`/`-q` 31 | 2. Convert to NDJSON, compact form (one trace per line) 32 | 3. Send to archors_interpret 33 | 4. Send to archors_operator 34 | 35 | Trace a particular transaction 36 | ```command 37 | curl -X POST -H "Content-Type: application/json" --data '{"jsonrpc": "2.0", "method": "debug_traceTransaction", "params": ["0x8f5dd8107e2efce82759c9bbf34ac7bab49a2992b2f2ee6fc9d510f5e2490680", {"disableMemory": true}], "id":1}' http://127.0.0.1:8545 \ 38 | | jq '.["result"]["structLogs"][]' -c \ 39 | | cargo run -qr -p archors_interpret \ 40 | | cargo run -qr -p archors_operator 41 | ``` 42 | 43 | Trace a whole block 44 | ```command 45 | curl -s -X POST -H "Content-Type: application/json" --data '{"jsonrpc": "2.0", "method": "debug_traceBlockByNumber", "params": ["finalized", {"disableMemory": true}], "id":1}' http://127.0.0.1:8545 \ 46 | | jq '.["result"][]["result"]["structLogs"][]' -c \ 47 | | cargo run -qr -p archors_interpret \ 48 | | cargo run -qr -p archors_operator 49 | ``` 50 | ### Looping 51 | 52 | This calls the next latest block once the current one is finished. Note that there is some blank 53 | screen while the next block is fetched. As displaying at a relaxed speed takes longer than 14 54 | seconds, this will skip blocks. 55 | 56 | When a block has finished displaying, get the latest block and start again. 57 | ```command 58 | while true; do curl -s -X POST -H "Content-Type: application/json" --data \ 59 | '{"jsonrpc": "2.0", "method": "debug_traceBlockByNumber", "params": ["latest", {"disableMemory": true}], "id":1}' \ 60 | http://127.0.0.1:8545 \ 61 | | jq '.["result"][]["result"]["structLogs"][]' -c \ 62 | | cargo run -qr -p archors_interpret \ 63 | | cargo run -qr -p archors_operator; done 64 | ``` 65 | 66 | ### EIP-3155 trace 67 | In the example below, revm executes a block producing an EIP3155 trace, which is then 68 | filtered and interpreted. That is then passed to operator to display. 69 | 70 | ```command 71 | cargo run -qr --example 09_use_proof | cargo run -qr -p archors_interpret eip3155 | cargo run -qr -p archors_operator 72 | ``` 73 | Or for the raw trace: 74 | ```command 75 | cargo run -qr --example 09_use_proof | cargo run -qr -p archors_operator 76 | ``` 77 | -------------------------------------------------------------------------------- /bin/operator/src/cli.rs: -------------------------------------------------------------------------------- 1 | //! For Command Line Interface for archors_interpret 2 | 3 | use clap::Parser; 4 | 5 | #[derive(Parser, Debug)] 6 | #[command(author, version, about, long_about = None)] 7 | pub struct AppArgs { 8 | /// Delay between drawing each droplet, in microseconds 9 | #[clap(short, long, default_value_t = 2000)] 10 | pub delay: u64, 11 | } 12 | -------------------------------------------------------------------------------- /bin/operator/src/droplet.rs: -------------------------------------------------------------------------------- 1 | //! For representing a vertical lines of text. 2 | 3 | use crossterm::{ 4 | cursor, 5 | style::{Color, Print, SetForegroundColor}, 6 | QueueableCommand, 7 | }; 8 | 9 | /// A single line of text 10 | pub struct Droplet { 11 | pub text: String, 12 | /// Length of string 13 | pub length: usize, 14 | /// Greyscale (fades further within a group) 15 | pub shade: u8, 16 | /// Col 17 | pub x_pos: u16, 18 | /// Row 19 | pub y_pos: u16, 20 | /// Index of character being drawn 21 | pub current_char: usize, 22 | } 23 | 24 | impl Droplet { 25 | pub fn is_final_char(&self) -> bool { 26 | self.current_char == self.length - 1 27 | } 28 | pub fn draw_droplet(&self, max_height: u16) -> anyhow::Result<()> { 29 | for (index, char) in self.text.char_indices() { 30 | match self 31 | .draw_char(char, index, max_height) 32 | .expect("Could not draw char") 33 | { 34 | DrawInfo::ContinueDroplet => continue, 35 | DrawInfo::EndDroplet => break, 36 | }; 37 | } 38 | Ok(()) 39 | } 40 | /// Draw a character, which has an index in the droplet, and which has a position 41 | /// relative to the char that the droplet is up to (leading edge in the animation). 42 | pub fn draw_char( 43 | &self, 44 | original_char: char, 45 | index: usize, 46 | max_height: u16, 47 | ) -> anyhow::Result { 48 | let (letter, colour) = match (Status::get(index, self.current_char), self.is_final_char()) { 49 | (_, true) => (' ', Color::Green), 50 | (Status::NormalUndrawn, false) => { 51 | let (r, g, b) = (0, self.shade, 0); 52 | (original_char, Color::Rgb { r, g, b }) 53 | } 54 | (Status::NormalDrawn, false) => return Ok(DrawInfo::ContinueDroplet), 55 | (Status::ToErase, false) => (' ', Color::Green), 56 | (Status::TooEarly, false) => return Ok(DrawInfo::EndDroplet), 57 | (Status::Stale, false) => return Ok(DrawInfo::ContinueDroplet), 58 | (Status::BrightestDrawn, false) => return Ok(DrawInfo::ContinueDroplet), 59 | (Status::BrightestUndrawn, false) => { 60 | let (r, g, b) = (self.shade, self.shade, self.shade); 61 | (original_char, Color::Rgb { r, g, b }) 62 | } 63 | }; 64 | let y_pos_abs = self.y_pos + index as u16; 65 | let y_pos = match y_pos_abs >= max_height { 66 | true => y_pos_abs.wrapping_rem(max_height), 67 | false => y_pos_abs, 68 | }; 69 | std::io::stdout() 70 | .queue(SetForegroundColor(colour))? 71 | .queue(cursor::MoveTo(self.x_pos, y_pos))? 72 | .queue(Print(letter))?; 73 | Ok(DrawInfo::ContinueDroplet) 74 | } 75 | } 76 | 77 | /// Whether to proceed with the rest of the drop. 78 | pub enum DrawInfo { 79 | /// No action required for current charactor 80 | ContinueDroplet, 81 | /// No further drawing required for droplet. 82 | EndDroplet, 83 | } 84 | 85 | /// Every droplet is visited multiple times. Each time, characters have 86 | /// a status, relative to the phase of the droplet. 87 | pub enum Status { 88 | /// Char is not yet ready to be drawn for this droplet, nothing to be done 89 | TooEarly, 90 | /// White char, needs to be drawn 91 | BrightestUndrawn, 92 | /// White char, has been drawn, nothing to be done. 93 | BrightestDrawn, 94 | /// Needs to be drawn in normal colour 95 | NormalUndrawn, 96 | /// Has been drawn, nothing to be done 97 | NormalDrawn, 98 | /// Has been drawn, now ready to erase 99 | ToErase, 100 | /// Has been erased, now is far in the past, nothing to be done 101 | Stale, 102 | } 103 | 104 | impl Status { 105 | fn get(char_index: usize, draw_index: usize) -> Self { 106 | let diff = char_index.abs_diff(draw_index); 107 | let after = char_index > draw_index; 108 | match (after, diff) { 109 | (false, 0) => Status::BrightestUndrawn, 110 | (false, 1 | 2) => Status::BrightestDrawn, 111 | (false, 3) => Status::NormalUndrawn, 112 | (false, 4..=40) => Status::NormalDrawn, 113 | (false, 41) => Status::ToErase, 114 | (false, _) => Status::Stale, 115 | (true, _) => Status::TooEarly, 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /bin/operator/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | io::{BufRead, Write}, 3 | sync::{ 4 | atomic::{AtomicBool, Ordering}, 5 | mpsc::{channel, Receiver, Sender}, 6 | Arc, 7 | }, 8 | thread, 9 | time::Duration, 10 | }; 11 | 12 | use clap::Parser; 13 | use cli::AppArgs; 14 | use crossterm::{ 15 | cursor::{self}, 16 | terminal, ExecutableCommand, 17 | }; 18 | use ctrlc::set_handler; 19 | use droplet::Droplet; 20 | use rand::Rng; 21 | 22 | mod cli; 23 | mod droplet; 24 | 25 | fn main() { 26 | let args = AppArgs::parse(); 27 | let (tx, rx) = channel::(); 28 | thread::spawn(move || read_from_stdin(tx)); 29 | write_to_terminal(rx, args.delay).expect("App could not be run."); 30 | } 31 | 32 | /// Reads lines from terminal, and sends them in a channel as Droplet. 33 | fn read_from_stdin(tx: Sender) -> anyhow::Result<()> { 34 | let stdin = std::io::stdin(); 35 | let reader = stdin.lock(); 36 | let mut rng = rand::thread_rng(); 37 | let (col, row) = terminal::size()?; 38 | let lowest_draw = row / 3; 39 | 40 | for line in reader.lines() { 41 | let x_pos = rng.gen_range(0..col); 42 | let y_pos = rng.gen_range(0..lowest_draw); 43 | let mut text = line?.to_string(); 44 | text.push_str(" "); 45 | let length = text.len(); 46 | if length == 0 { 47 | continue; 48 | } 49 | let droplet = Droplet { 50 | text, 51 | length, 52 | shade: rng.gen_range(20..255), 53 | x_pos, 54 | y_pos, 55 | current_char: 0, 56 | }; 57 | tx.send(droplet)?; 58 | } 59 | Ok(()) 60 | } 61 | 62 | /// Recevies Droplets in a channel and displays them in the terminal. 63 | fn write_to_terminal(rx: Receiver, delay: u64) -> anyhow::Result<()> { 64 | let mut stdout = std::io::stdout(); 65 | stdout.execute(terminal::EnterAlternateScreen)?; 66 | stdout.execute(cursor::Hide)?; 67 | let (_, max_height) = terminal::size()?; 68 | 69 | let mut droplets: Vec = vec![]; 70 | 71 | // Set up Ctrl+C signal handler 72 | let running = Arc::new(AtomicBool::new(true)); 73 | let r = running.clone(); 74 | set_handler(move || { 75 | r.store(false, Ordering::SeqCst); 76 | }) 77 | .expect("Error setting Ctrl+C handler"); 78 | 79 | // Accept new droplets as they arrive. 80 | // Hold droplets until they are finished. 81 | // Draw all held droplets at the same time. 82 | while running.load(Ordering::SeqCst) { 83 | match rx.recv() { 84 | Ok(droplet) => droplets.push(droplet), 85 | Err(_) => { 86 | if droplets.is_empty() { 87 | thread::sleep(Duration::from_millis(10)); 88 | continue; 89 | } 90 | } 91 | } 92 | 93 | // Draw droplets 94 | droplets.retain_mut(|droplet| { 95 | droplet 96 | .draw_droplet(max_height) 97 | .expect("Couldn't draw droplet."); 98 | // All droplets have been queued, draw them. 99 | stdout.flush().expect("Could not flush stdout"); 100 | thread::sleep(Duration::from_micros(delay)); 101 | // Get retention info prior to updating status. 102 | let retain_droplet = !droplet.is_final_char(); 103 | droplet.current_char += 1; 104 | retain_droplet 105 | }); 106 | if droplets.is_empty() { 107 | break; 108 | } 109 | } 110 | stdout.execute(terminal::LeaveAlternateScreen)?; 111 | stdout.execute(terminal::Clear(terminal::ClearType::All))?; 112 | stdout.execute(cursor::Show)?; 113 | Ok(()) 114 | } 115 | 116 | #[cfg(test)] 117 | mod test { 118 | use std::io::Write; 119 | 120 | /// Generates a few lines to pass to the operator app. Use as follows: 121 | /// ``` 122 | /// cargo -q test -p archors_operator -- std | cargo run -p archors_operator 123 | /// ``` 124 | /// Repeater: 125 | /// ``` 126 | /// for i in {1..3}; do cargo test -p archors_operator -- std; sleep 1; done | cargo run -p archors_operator 127 | /// while true; do cargo test -q -p archors_operator -- std; done | cargo run -q -p archors_operator 128 | /// ``` 129 | #[test] 130 | #[ignore] 131 | fn test_write_to_std_out() { 132 | let mut stdout = Box::new(std::io::stdout()); 133 | for line in 0..100 { 134 | writeln!(stdout, "Line abcde 123456789 {}", line).unwrap(); 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /bin/stator/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "archors_stator" 3 | version = "0.1.0" 4 | edition = "2021" 5 | description = "Generates test cases for eth_getRequiredBlockState" 6 | 7 | [dependencies] 8 | anyhow = "1.0.69" 9 | archors_inventory = { path = "../../crates/inventory"} 10 | archors_types = { path = "../../crates/types"} 11 | clap = { version = "4.3.19", features = ["derive"] } 12 | env_logger = {workspace = true} 13 | hex = "0.4.3" 14 | log = { workspace = true } 15 | tokio = { version = "1.26.0", features = ["full"] } 16 | url = "2.4.0" 17 | -------------------------------------------------------------------------------- /bin/stator/README.md: -------------------------------------------------------------------------------- 1 | ## Stator 2 | 3 | Uses an Ethereum node to generate test cases for the `eth_getRequiredBlockState` JSON-RPC method. 4 | 5 | ### Flags 6 | See flags here: 7 | ```command 8 | cargo run --release -p archors_stator -- --help 9 | ``` 10 | 11 | ### Format 12 | 13 | The data returned in response to the `eth_getRequiredBlockState` method is the 14 | `RequiredBlockState`. It is ssz-encoded and snappy-compressed, and is delivered over JSON-RPC 15 | as a hex encoded string "0xabcde...". 16 | 17 | The .ssz_snappy binary data is approximately ~176kB/Mgas, which is about ~2.5MB per block. 18 | The test generator outputs a hex string by default, and so is about ~5MB per block. The 19 | binary data can also be generated. 20 | 21 | ### Use 22 | 23 | The `RequiredBlockState` is used as follows: 24 | 25 | 1. Acquire a block with transactions (via `eth_getBlockByNumber`) 26 | 2. Acquire block prestate (via `eth_getRequiredBlockState`) 27 | 3. Load block and state into an EVM (e.g., revm) 28 | 4. Execute the block (and any tracing/parsing as needed) 29 | 30 | In this way, one can generate a full trace of every opcode, and including EVM stack and memory. 31 | From the starting data (~5MB), the derived trace can be up to 10s of GB. 32 | 33 | A full working example of using a proof to run revm can be seen in: 34 | - [../../examples](../../examples/09_use_proof.rs) library examples showing generation of a raw trace 35 | - [../interpret](../interpret/README.md) binary for parsed trace 36 | - [../operator](../operator/README.md) binary for a visual representation of a parsed trace 37 | 38 | ### Example 39 | To create hex-string file containing the RequiredBlockState for block 17190873. 40 | ```command 41 | RUST_LOG=info cargo run --release -p archors_stator -- -b 17190873 42 | ``` 43 | A file is created, containing the following (truncated) text. This is ssz-snappy encoded 44 | data that can be used to re-execute that block. 45 | ``` 46 | 0xff060000734e6150705900b07d007316...f5f2490ff2ae79390000000001064fd8 47 | ``` 48 | ### Running time 49 | The application calls `eth_getProof` a number of times (one per account accessed in that block). 50 | This can amount to hundreds of calls. Logging (`RUST_LOG=info cargo run ...`) will 51 | show the stages of completion, including how many `eth_getProof` calls are being made. -------------------------------------------------------------------------------- /bin/stator/src/cli.rs: -------------------------------------------------------------------------------- 1 | //! For Command Line Interface for archors_stator 2 | 3 | use clap::{Parser, ValueEnum}; 4 | use url::Url; 5 | 6 | pub const LOCALHOST: &str = "http://127.0.0.1:8545/"; 7 | 8 | #[derive(Parser, Debug)] 9 | #[command(author, version, about, long_about = None)] 10 | pub struct AppArgs { 11 | /// File to create. E.g., _.txt / _.ssz_snappy 12 | #[clap(short, long, default_value_t = String::from("required_block_state"))] 13 | pub filename_prefix: String, 14 | /// Kind of data to write to file. 15 | #[clap(value_enum, default_value_t=OutputKind::HexString)] 16 | pub output: OutputKind, 17 | /// Url of node for eth_getProof requests 18 | #[clap(short, long, default_value_t = Url::parse(LOCALHOST).expect("Couldn't read node"))] 19 | pub get_proof_node: Url, 20 | /// Url of node for eth_getBlockByNumber and debug_traceBlock requests 21 | #[clap(short, long, default_value_t = Url::parse(LOCALHOST).expect("Couldn't read node"))] 22 | pub trace_block_node: Url, 23 | /// Block number to get state information for (the block that will be re-executed) 24 | #[clap(short, long)] 25 | pub block_number: u64, 26 | } 27 | 28 | /// Format of data to be written to file. 29 | #[derive(ValueEnum, Clone, Debug, PartialEq)] 30 | pub enum OutputKind { 31 | /// Create .txt with 0x-prefixed hex-string 32 | HexString, 33 | /// Create .ssz_snappy binary 34 | Binary, 35 | } 36 | -------------------------------------------------------------------------------- /bin/stator/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fs::{self, File}, 3 | io::Write, 4 | }; 5 | 6 | use anyhow::{bail, Result}; 7 | use archors_inventory::{cache::fetch_required_block_state, utils::compress}; 8 | use archors_types::state::RequiredBlockState; 9 | use clap::Parser; 10 | use cli::OutputKind; 11 | 12 | use crate::cli::AppArgs; 13 | 14 | mod cli; 15 | 16 | /// Create RequiredBlockState data type. 17 | /// 18 | /// Calls an archive node and gets all information that is required to trace a block locally. 19 | /// Discards intermediate data. Resulting RequiredBlockState data type can be sent to a 20 | /// peer who can use it to trustlessly trace an historical Ethereum block. 21 | /// 22 | /// Involves: 23 | /// - debug_traceBlock for state accesses 24 | /// - debug_traceBlock for blockhash use 25 | /// - eth_getProof for proof of historical state 26 | #[tokio::main] 27 | async fn main() -> Result<()> { 28 | env_logger::init(); 29 | 30 | let args = AppArgs::parse(); 31 | 32 | let mut file = prepare_file(&args).expect("Could not prepare file"); 33 | 34 | let required_block_state = fetch_required_block_state( 35 | args.trace_block_node.as_ref(), 36 | args.get_proof_node.as_ref(), 37 | args.block_number, 38 | ) 39 | .await?; 40 | 41 | let bytes = encode_ssz_snappy(required_block_state)?; 42 | match args.output { 43 | OutputKind::HexString => { 44 | let string = format!("0x{}", hex::encode(&bytes)); 45 | file.write_all(string.as_bytes())?; 46 | } 47 | OutputKind::Binary => { 48 | file.write_all(&bytes)?; 49 | } 50 | }; 51 | Ok(()) 52 | } 53 | 54 | /// required block state -> .ssz_snappy 55 | fn encode_ssz_snappy(state: RequiredBlockState) -> anyhow::Result> { 56 | let ssz = state.to_ssz_bytes()?; 57 | let ssz_snappy = compress(ssz)?; 58 | Ok(ssz_snappy) 59 | } 60 | 61 | /// Creates a file and returns the handle to write to. 62 | fn prepare_file(args: &AppArgs) -> Result { 63 | let mut filename = format!("{}_{}", args.filename_prefix, args.block_number); 64 | match args.output { 65 | OutputKind::HexString => filename.push_str(".txt"), 66 | OutputKind::Binary => filename.push_str(".ssz_snappy"), 67 | }; 68 | if fs::metadata(&filename).is_ok() { 69 | bail!("{} file aleady exists", filename); 70 | }; 71 | Ok(File::create(filename)?) 72 | } 73 | -------------------------------------------------------------------------------- /crates/inventory/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "archors_inventory" 3 | version = "0.1.0" 4 | edition = "2021" 5 | description = "A tool for discovering which state was accessed for an Ethereum block" 6 | 7 | [dependencies] 8 | archors_multiproof= {path = "../multiproof"} 9 | archors_types = {path = "../types"} 10 | archors_verify = { path = "../verify" } 11 | ethers = "2.0.4" 12 | futures = "0.3.28" 13 | hex = "0.4.3" 14 | log = { workspace = true} 15 | reqwest = { version = "0.11.14", features = ["json", "stream"] } 16 | serde = { version = "1.0.152", features = ["derive"] } 17 | serde_json = "1.0.94" 18 | snap = "1.1.0" 19 | ssz_rs = "0.8.0" 20 | ssz_rs_derive = "0.8.0" 21 | thiserror = "1.0.40" 22 | url = "2.3.1" 23 | -------------------------------------------------------------------------------- /crates/inventory/README.md: -------------------------------------------------------------------------------- 1 | # archors-inventory 2 | 3 | For discovering which state was accessed for an Ethereum block. 4 | 5 | ## Why 6 | 7 | When a block is executed, it may access any state. However, once included blocks 8 | only access a tiny subset of all state. 9 | 10 | That information is useful because one can get a proof for each of these state 11 | values using `eth_getProof`. As a finite list, this inventory of accessed state 12 | can be used to create a collection of state proofs. 13 | 14 | With the state proofs, anyone could trustlessly replay the block because every 15 | state is anchored the state root in the block header. 16 | 17 | ## How 18 | 19 | For every transaction in a block, call `debug_traceTransaction` and specify the `prestateTracer`. 20 | This returns the state prior to each transaction execution. 21 | 22 | Detect if a transaction state access was already accessed in a prior transaction and ignore these 23 | values. 24 | 25 | For each state call `eth_getProof`, verify the proofs, then store them. -------------------------------------------------------------------------------- /crates/inventory/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod cache; 2 | pub mod oracle; 3 | pub mod overlap; 4 | pub mod rpc; 5 | pub mod transferrable; 6 | pub mod types; 7 | pub mod utils; 8 | -------------------------------------------------------------------------------- /crates/inventory/src/overlap.rs: -------------------------------------------------------------------------------- 1 | //! Compares two cached transferrable block proofs and quantifies the degree 2 | //! of data overlap (contracts, nodes). This represents data that a node 3 | //! would not have to duplicate on disk. 4 | 5 | use std::{collections::HashSet, fmt::Display}; 6 | 7 | use thiserror::Error; 8 | 9 | use crate::cache::{get_required_state_from_cache, CacheError}; 10 | 11 | #[derive(Debug, Error)] 12 | pub enum OverlapError { 13 | #[error("CacheError {0}")] 14 | CacheError(#[from] CacheError), 15 | } 16 | 17 | /// Overlap between two transferrable proofs, measured in bytes. 18 | pub struct DataSaved { 19 | /// This excludes proof account and storage values. 20 | nodes_and_contracts_to_store: usize, 21 | contracts: usize, 22 | accounts: usize, 23 | storage: usize, 24 | } 25 | 26 | impl Display for DataSaved { 27 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 28 | write!( 29 | f, 30 | "Overlap: Contracts {}KB, Account nodes {}KB, Storage nodes {}KB. Savings {}%", 31 | self.contracts / 1000, 32 | self.accounts / 1000, 33 | self.storage / 1000, 34 | self.percentage_savings(), 35 | ) 36 | } 37 | } 38 | 39 | impl DataSaved { 40 | /// Percentage of code and nodes data that does not need to be stored twice 41 | /// due to overlap between blocks. 42 | pub fn percentage_savings(&self) -> usize { 43 | let savings = self.contracts + self.accounts + self.storage; 44 | let naive_sum = self.nodes_and_contracts_to_store + savings; 45 | 100 * savings / naive_sum 46 | } 47 | /// Bytes saved byt not duplicating repeated code and account/storage nodes 48 | /// between different blocks. 49 | pub fn total_savings(&self) -> usize { 50 | self.contracts + self.accounts + self.storage 51 | } 52 | } 53 | 54 | pub fn measure_proof_overlap(blocks: Vec) -> Result { 55 | let mut contract_saved_bytes = 0usize; 56 | let mut accounts_saved_bytes = 0usize; 57 | let mut storage_saved_bytes = 0usize; 58 | let mut to_store = 0usize; 59 | 60 | let mut contract_set: HashSet> = HashSet::new(); 61 | let mut accounts_set: HashSet> = HashSet::new(); 62 | let mut storage_set: HashSet> = HashSet::new(); 63 | 64 | for block in blocks { 65 | let proof = get_required_state_from_cache(block)?; 66 | for contract in proof.contracts.iter() { 67 | check_bytes( 68 | &mut contract_saved_bytes, 69 | &mut contract_set, 70 | contract.to_vec(), 71 | &mut to_store, 72 | ); 73 | } 74 | todo!("Overlap stats not updated after implementing node bag") 75 | /* 76 | // TODO Note: The node bag contains account and storage nodes all together. 77 | for node in proof.account_nodes.iter() { 78 | check_bytes( 79 | &mut accounts_saved_bytes, 80 | &mut accounts_set, 81 | node.to_vec(), 82 | &mut to_store, 83 | ); 84 | } 85 | for node in proof.storage_nodes.iter() { 86 | check_bytes( 87 | &mut storage_saved_bytes, 88 | &mut storage_set, 89 | node.to_vec(), 90 | &mut to_store, 91 | ) 92 | } 93 | */ 94 | } 95 | 96 | Ok(DataSaved { 97 | contracts: contract_saved_bytes, 98 | accounts: accounts_saved_bytes, 99 | storage: storage_saved_bytes, 100 | nodes_and_contracts_to_store: to_store, 101 | }) 102 | } 103 | 104 | /// Either records data, or if already present registers saved bytes. 105 | fn check_bytes( 106 | saved_bytes: &mut usize, 107 | set: &mut HashSet>, 108 | data: Vec, 109 | to_store: &mut usize, 110 | ) { 111 | if set.contains(&data) { 112 | *saved_bytes += data.len(); 113 | } else { 114 | *to_store += data.len(); 115 | set.insert(data); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /crates/inventory/src/rpc.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | 3 | use ethers::types::{Block, EIP1186ProofResponse, Transaction}; 4 | use serde::{Deserialize, Serialize}; 5 | use serde_json::{json, Value}; 6 | 7 | use crate::types::{AccountToProve, TransactionAccountStates}; 8 | 9 | #[derive(Debug, Deserialize, Serialize)] 10 | pub(crate) struct AccountProofResponse { 11 | id: u32, 12 | jsonrpc: String, 13 | pub(crate) result: EIP1186ProofResponse, 14 | } 15 | 16 | #[derive(Debug, Serialize)] 17 | pub struct JsonRpcRequest { 18 | jsonrpc: String, 19 | method: String, 20 | params: Vec, 21 | id: u64, 22 | } 23 | 24 | impl Display for JsonRpcRequest { 25 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 26 | write!(f, "method: {}, params: {:?}", self.method, self.params) 27 | } 28 | } 29 | 30 | #[derive(Deserialize, Serialize)] 31 | pub(crate) struct BlockResponse { 32 | id: u32, 33 | jsonrpc: String, 34 | pub(crate) result: Block, 35 | } 36 | 37 | #[derive(Debug, Deserialize, Serialize)] 38 | pub(crate) struct TxPrestateResponse { 39 | id: u32, 40 | jsonrpc: String, 41 | pub(crate) result: TransactionAccountStates, 42 | } 43 | 44 | #[derive(Debug, Deserialize, Serialize)] 45 | pub(crate) struct BlockPrestateResponse { 46 | id: u32, 47 | jsonrpc: String, 48 | pub(crate) result: Vec, 49 | } 50 | 51 | #[derive(Debug, Deserialize, Serialize)] 52 | pub(crate) struct BlockPrestateTransactions { 53 | pub(crate) result: TransactionAccountStates, 54 | } 55 | 56 | /// Generates a JSON-RPC request for debug_traceBlockByNumber for 57 | /// the given block with the prestateTracer. 58 | pub(crate) fn debug_trace_block_prestate(block: &str) -> JsonRpcRequest { 59 | JsonRpcRequest { 60 | jsonrpc: "2.0".to_owned(), 61 | method: "debug_traceBlockByNumber".to_owned(), 62 | params: vec![json!(block), json!({"tracer": "prestateTracer"})], 63 | id: 1, 64 | } 65 | } 66 | 67 | /// Generates a JSON-RPC request for debug_traceBlockByNumber for 68 | /// the given block with the default tracer. 69 | pub(crate) fn debug_trace_block_default(block: &str) -> JsonRpcRequest { 70 | JsonRpcRequest { 71 | jsonrpc: "2.0".to_owned(), 72 | method: "debug_traceBlockByNumber".to_owned(), 73 | params: vec![json!(block), json!({"disableMemory": true})], 74 | id: 1, 75 | } 76 | } 77 | 78 | /// Generates a JSON-RPC request for eth_getBlockByNumber for 79 | /// the specified block (e.g., "0xabc", "latest", "finalized"). 80 | pub(crate) fn get_block_by_number(block: &str) -> JsonRpcRequest { 81 | JsonRpcRequest { 82 | jsonrpc: "2.0".to_owned(), 83 | method: "eth_getBlockByNumber".to_owned(), 84 | params: vec![json!(block), Value::Bool(true)], 85 | id: 1, 86 | } 87 | } 88 | 89 | /// Generates a JSON-RPC request for eth_getProof for 90 | /// the given account and storage slots at the specified block. 91 | pub(crate) fn eth_get_proof(account: &AccountToProve, block_number: &str) -> JsonRpcRequest { 92 | JsonRpcRequest { 93 | jsonrpc: "2.0".to_owned(), 94 | method: "eth_getProof".to_owned(), 95 | params: vec![ 96 | json!(account.address), 97 | json!(account.slots), 98 | json!(block_number), 99 | ], 100 | id: 1, 101 | } 102 | } 103 | 104 | #[derive(Debug, Deserialize, Serialize)] 105 | pub(crate) struct BlockDefaultTraceResponse { 106 | id: u32, 107 | jsonrpc: String, 108 | pub(crate) result: Vec, 109 | } 110 | 111 | #[derive(Debug, Deserialize, Serialize)] 112 | pub(crate) struct TxDefaultTraceResult { 113 | pub(crate) result: DefaultTxTrace, 114 | } 115 | 116 | #[derive(Debug, Deserialize, Serialize)] 117 | #[serde(rename_all = "camelCase")] 118 | pub(crate) struct DefaultTxTrace { 119 | pub(crate) struct_logs: Vec, 120 | } 121 | 122 | #[derive(Debug, Deserialize, Serialize)] 123 | #[serde(rename_all = "camelCase")] 124 | pub(crate) struct EvmStep { 125 | pub(crate) pc: u64, 126 | pub(crate) op: String, 127 | pub(crate) gas: u64, 128 | pub(crate) gas_cost: u64, 129 | pub(crate) depth: u64, 130 | pub(crate) stack: Vec, 131 | pub(crate) memory: Option>, 132 | } 133 | -------------------------------------------------------------------------------- /crates/inventory/src/transferrable.rs: -------------------------------------------------------------------------------- 1 | //! For representing critical state data while minimising duplication (intra-block and inter-block). State data is to be transmitted between untrusted parties. 2 | //! 3 | //! ## Encoding 4 | //! 5 | //! The data is SSZ encoded for consistency between implementations. 6 | //! 7 | //! ## Contents 8 | //! 9 | //! Large data items (contract code and merkle trie nodes) may be repeated within a block or 10 | //! between blocks. 11 | //! - Contract is called in different transactions/blocks without changes to its bytecode. 12 | //! - Merkle trie nodes where state is accessed in the leafy-end of the proof. Such as very 13 | //! populated accounts and storage items. 14 | //! 15 | //! Such an item can be referred to by the position in a separate list. 16 | 17 | use std::collections::{HashMap, HashSet}; 18 | 19 | use archors_types::state::{ 20 | BlockHashes, CompactEip1186Proof, CompactEip1186Proofs, CompactStorageProof, 21 | CompactStorageProofs, Contract, Contracts, NodeIndices, RecentBlockHash, RequiredBlockState, 22 | }; 23 | use ethers::types::{EIP1186ProofResponse, StorageProof, H160, H256, U64}; 24 | use ssz_rs::prelude::*; 25 | use thiserror::Error; 26 | 27 | use crate::{ 28 | cache::ContractBytes, 29 | types::{BlockHashAccesses, BlockProofs}, 30 | utils::{ 31 | h160_to_ssz_h160, h256_to_ssz_h256, u256_to_ssz_u256, u64_to_ssz_u64, usize_to_u16, 32 | UtilsError, 33 | }, 34 | }; 35 | 36 | #[derive(Debug, Error)] 37 | pub enum TransferrableError { 38 | #[error("Derialize Error {0}")] 39 | DerializeError(#[from] ssz_rs::DeserializeError), 40 | #[error("SSZ Error {0}")] 41 | SszError(#[from] SerializeError), 42 | #[error("SimpleSerialize Error {0}")] 43 | SimpleSerializeError(#[from] SimpleSerializeError), 44 | #[error("Utils error {0}")] 45 | UtilsError(#[from] UtilsError), 46 | #[error("Unable to find index for node")] 47 | NoIndexForNode, 48 | } 49 | 50 | /// Creates a compact proof by separating trie nodes and contract code from the proof data. 51 | pub fn state_from_parts( 52 | block_proofs: BlockProofs, 53 | accessed_contracts_sorted: Vec, 54 | accessed_blockhashes: BlockHashAccesses, 55 | ) -> Result { 56 | let node_set = get_trie_node_set(&block_proofs.proofs); 57 | 58 | let proof = RequiredBlockState { 59 | compact_eip1186_proofs: get_compact_eip1186_proofs(block_proofs)?, 60 | contracts: contracts_to_ssz(accessed_contracts_sorted), 61 | trie_nodes: bytes_collection_to_ssz(node_set.0), 62 | // account_nodes: bytes_collection_to_ssz(node_set.account), 63 | // storage_nodes: bytes_collection_to_ssz(node_set.storage), 64 | blockhashes: blockhashes_to_ssz(accessed_blockhashes.to_unique_pairs_sorted())?, 65 | }; 66 | Ok(proof) 67 | } 68 | 69 | /// Replace every account proof node with a reference to the index in a list. 70 | /// 71 | /// Results are sorted by address. Contains storage proofs, that 72 | /// are sorted by key. 73 | fn get_compact_eip1186_proofs( 74 | block_proofs: BlockProofs, 75 | ) -> Result { 76 | let mut block_proofs: Vec<(H160, EIP1186ProofResponse)> = 77 | block_proofs.proofs.into_iter().collect(); 78 | // Sort account proofs by address 79 | block_proofs.sort_by_key(|x| x.0); 80 | 81 | let mut ssz_eip1186_proofs = CompactEip1186Proofs::default(); 82 | 83 | for proof in block_proofs { 84 | // Storage 85 | let storage_proofs = get_compact_storage_proofs(proof.1.storage_proof)?; 86 | 87 | let ssz_eip1186_proof = CompactEip1186Proof { 88 | address: h160_to_ssz_h160(proof.1.address)?, 89 | balance: u256_to_ssz_u256(proof.1.balance), 90 | code_hash: h256_to_ssz_h256(proof.1.code_hash)?, 91 | nonce: u64_to_ssz_u64(proof.1.nonce), 92 | storage_hash: h256_to_ssz_h256(proof.1.storage_hash)?, 93 | storage_proofs, 94 | }; 95 | ssz_eip1186_proofs.push(ssz_eip1186_proof); 96 | } 97 | 98 | Ok(ssz_eip1186_proofs) 99 | } 100 | 101 | /// Replace every storage proof node with a reference to the index in a list. 102 | /// 103 | /// Results are sorted by key. 104 | fn get_compact_storage_proofs( 105 | storage_proofs: Vec, 106 | ) -> Result { 107 | // Sort storage proofs by key 108 | let mut storage_proofs = storage_proofs; 109 | storage_proofs.sort_by_key(|x| x.key); 110 | 111 | let mut compact_storage_proofs = CompactStorageProofs::default(); 112 | for storage_proof in storage_proofs { 113 | // key, value 114 | let compact_storage_proof = CompactStorageProof { 115 | key: h256_to_ssz_h256(storage_proof.key)?, 116 | value: u256_to_ssz_u256(storage_proof.value), 117 | }; 118 | compact_storage_proofs.push(compact_storage_proof); 119 | } 120 | Ok(compact_storage_proofs) 121 | } 122 | 123 | /// Turns a list of nodes in to a list of indices. The indices 124 | /// come from a mapping (node -> index) 125 | /// 126 | /// The node order is unchanged, and will already be sorted (closest to root = first) 127 | fn nodes_to_node_indices( 128 | proof: Vec, 129 | map: &HashMap, 130 | ) -> Result { 131 | let mut indices = NodeIndices::default(); 132 | // Substitute proof nodes with indices. 133 | for node in proof { 134 | // Find the index 135 | let index: &usize = map 136 | .get(node.0.as_ref()) 137 | .ok_or(TransferrableError::NoIndexForNode)?; 138 | // Insert the index 139 | indices.push(usize_to_u16(*index)?); 140 | } 141 | Ok(indices) 142 | } 143 | 144 | /// Holds all nodes present in a block state proof. Used to construct 145 | /// deduplicated compact proof. 146 | /// 147 | /// Account and storage nodes are separate. Members are lexicographically sorted ([0xa.., 0xb..]) 148 | #[derive(Clone)] 149 | struct TrieNodesSet(Vec); 150 | 151 | /// Maps node -> index for all nodes present in a block state proof. Used to construct 152 | /// deduplicated compact proof. 153 | struct TrieNodesIndices { 154 | account: HashMap, 155 | storage: HashMap, 156 | } 157 | 158 | type NodeBytes = Vec; 159 | 160 | /// Finds all trie nodes and uses a HashSet to remove duplicates. 161 | /// 162 | /// The aggregated account and storage nodes are sorted. 163 | fn get_trie_node_set(proofs: &HashMap) -> TrieNodesSet { 164 | let mut node_set: HashSet> = HashSet::default(); 165 | 166 | for proof in proofs.values() { 167 | for node in &proof.account_proof { 168 | node_set.insert(node.0.clone().into()); 169 | } 170 | for storage_proof in &proof.storage_proof { 171 | for node in &storage_proof.proof { 172 | node_set.insert(node.0.clone().into()); 173 | } 174 | } 175 | } 176 | let mut nodes: Vec> = node_set.into_iter().collect(); 177 | nodes.sort(); 178 | TrieNodesSet(nodes) 179 | } 180 | 181 | /// Turns a collection of contracts into an SSZ format. 182 | fn contracts_to_ssz(input: Vec) -> Contracts { 183 | let mut contracts = Contracts::default(); 184 | input 185 | .into_iter() 186 | .map(|c| { 187 | let mut list = Contract::default(); 188 | c.into_iter().for_each(|byte| list.push(byte)); 189 | list 190 | }) 191 | .for_each(|contract| contracts.push(contract)); 192 | contracts 193 | } 194 | 195 | /// Turns a collection of accessed blockhashes into an SSZ format. 196 | fn blockhashes_to_ssz( 197 | accessed_blockhashes: Vec<(U64, H256)>, 198 | ) -> Result { 199 | let mut blockhashes = BlockHashes::default(); 200 | for (num, hash) in accessed_blockhashes { 201 | let block_hash = h256_to_ssz_h256(hash).map_err(TransferrableError::UtilsError)?; 202 | let pair = RecentBlockHash { 203 | block_number: u64_to_ssz_u64(num), 204 | block_hash, 205 | }; 206 | blockhashes.push(pair); 207 | } 208 | Ok(blockhashes) 209 | } 210 | 211 | /// Turns a collection of bytes into an SSZ format. 212 | fn bytes_collection_to_ssz( 213 | collection: Vec>, 214 | ) -> List, OUTER> { 215 | let mut ssz_collection = List::, OUTER>::default(); 216 | collection 217 | .into_iter() 218 | .map(|bytes| { 219 | let mut ssz_bytes = List::::default(); 220 | bytes.into_iter().for_each(|byte| ssz_bytes.push(byte)); 221 | ssz_bytes 222 | }) 223 | .for_each(|contract| ssz_collection.push(contract)); 224 | ssz_collection 225 | } 226 | -------------------------------------------------------------------------------- /crates/inventory/src/types.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, fmt::Display}; 2 | 3 | // use archors_verify::eip1186::{verify_proof, VerifyProofError}; 4 | use ethers::types::{EIP1186ProofResponse, H160, H256, U64}; 5 | use serde::{Deserialize, Serialize}; 6 | 7 | use crate::rpc::BlockPrestateTransactions; 8 | 9 | /// Helper for caching 10 | #[derive(Deserialize, Serialize)] 11 | pub struct BlockProofs { 12 | /// Map of account -> proof 13 | pub proofs: HashMap, 14 | } 15 | 16 | #[derive(Deserialize, Serialize)] 17 | pub struct BasicBlockState { 18 | pub state_root: H256, 19 | pub transactions: Vec, 20 | } 21 | 22 | /// BLOCKASH opcode use for a whole block, obtained by tracing a node and filtering 23 | /// for the the opcode. 24 | #[derive(Deserialize, Serialize)] 25 | pub struct BlockHashAccesses { 26 | pub blockhash_accesses: Vec, 27 | } 28 | 29 | impl BlockHashAccesses { 30 | pub fn to_hashmap(self) -> HashMap { 31 | let mut map = HashMap::new(); 32 | for access in self.blockhash_accesses { 33 | map.insert(access.block_number, access.block_hash); 34 | } 35 | map 36 | } 37 | pub fn to_unique_pairs_sorted(self) -> Vec<(U64, H256)> { 38 | let mut pairs: Vec<(U64, H256)> = self.to_hashmap().into_iter().collect(); 39 | pairs.sort(); 40 | pairs 41 | } 42 | } 43 | 44 | /// Single BLOCKASH opcode use. 45 | #[derive(Deserialize, Serialize)] 46 | pub struct BlockHashAccess { 47 | pub block_number: U64, 48 | pub block_hash: H256, 49 | } 50 | 51 | /// BLOCKASH opcode use for a whole block, obtained by tracing a node and filtering 52 | /// for the the opcode. 53 | #[derive(Deserialize, Serialize)] 54 | pub struct BlockHashStrings { 55 | pub blockhash_accesses: Vec, 56 | } 57 | 58 | /// Single BLOCKASH opcode use. 59 | #[derive(Deserialize, Serialize)] 60 | pub struct BlockHashString { 61 | pub block_number: String, 62 | pub block_hash: String, 63 | } 64 | 65 | /// Prestate tracer for all transactions in a block, as returned by 66 | /// a node. 67 | #[derive(Deserialize, Serialize)] 68 | pub struct BlockPrestateTrace { 69 | pub block_number: u64, 70 | pub prestate_traces: Vec, 71 | } 72 | 73 | pub struct InputData { 74 | pub transactions: Vec, 75 | /// State associated with an each account. 76 | /// 77 | /// If state is written two after first being accessed, that action 78 | /// is ignored. It only records "pristine/unaltered" state as of earliest 79 | /// access in that block. 80 | pub first_state: HashMap, 81 | } 82 | 83 | /// Records the account states for a single transaction. A transaction 84 | /// may involve multiple accounts, each with it's own state. 85 | /// 86 | /// The mapping is of account -> account_state. 87 | pub type TransactionAccountStates = HashMap; 88 | 89 | /// Records the state for a particular account. 90 | /// 91 | /// A prestate tracer 92 | /// only includes state that was accessed, hence the optional fields. 93 | #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] 94 | pub struct AccountState { 95 | // Every account will have a balance. 96 | pub balance: String, 97 | pub code: Option, 98 | pub nonce: Option, 99 | pub storage: Option, 100 | } 101 | 102 | pub type StorageSlot = HashMap; 103 | 104 | #[derive(Clone, Debug, Deserialize, Serialize)] 105 | pub struct BlockStateAccesses { 106 | /// Mapping of account addresses to accessed states. 107 | /// An account may have slots accessed in different 108 | /// transactions, they are aggregated here. 109 | pub(crate) access_data: HashMap, 110 | } 111 | 112 | impl Display for BlockStateAccesses { 113 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 114 | write!( 115 | f, 116 | "Block state proof with {} accounts accessed)", 117 | self.access_data.keys().count() 118 | ) 119 | } 120 | } 121 | 122 | impl BlockStateAccesses { 123 | /// Filters state accessed by multiple transactions, removing duplicates. 124 | pub(crate) fn from_prestate_accesses(block: Vec) -> Self { 125 | let mut state_accesses = Self::new(); 126 | for tx_state in block { 127 | state_accesses.include_new_state_accesses_for_tx(&tx_state.result); 128 | } 129 | state_accesses 130 | } 131 | /// Adds new state to the state access record if the state has not previously 132 | /// been added by a prior transaction. 133 | /// 134 | /// When an account has been accessed, any unseen aspects of that account 135 | /// are included (e.g., if balance was first accessed, then code later, the code 136 | /// is added to the record). 137 | pub fn include_new_state_accesses_for_tx( 138 | &mut self, 139 | tx_prestate_trace: &TransactionAccountStates, 140 | ) -> &mut Self { 141 | for (account, accessed_state) in tx_prestate_trace { 142 | // Check for an entry for the account 143 | match self.access_data.get_key_value(account) { 144 | Some((_, existing)) => { 145 | let updated = include_unseen_states(existing, accessed_state); 146 | self.access_data 147 | .insert(account.to_string(), updated.to_owned()); 148 | } 149 | None => { 150 | // Add whole state data if no entry for the account. 151 | self.access_data 152 | .insert(account.to_string(), accessed_state.to_owned()); 153 | } 154 | } 155 | } 156 | self 157 | } 158 | /// Returns a vector of accounts with storage slots that can be used to query 159 | /// eth_getProof for a specific block. 160 | /// 161 | /// The storage slots have been aggregated and may have been accessed in different transactions 162 | /// within a block. 163 | pub fn get_all_accounts_to_prove(&self) -> Vec { 164 | let mut accounts: Vec = vec![]; 165 | for account in &self.access_data { 166 | let address = account.0.to_owned(); 167 | let mut slots: Vec = vec![]; 168 | if let Some(storage) = &account.1.storage { 169 | for slot in storage.keys() { 170 | slots.push(slot.to_owned()) 171 | } 172 | } 173 | accounts.push(AccountToProve { address, slots }); 174 | } 175 | accounts 176 | } 177 | pub fn new() -> Self { 178 | BlockStateAccesses { 179 | access_data: HashMap::new(), 180 | } 181 | } 182 | } 183 | 184 | #[derive(Clone, Debug, Deserialize, Serialize)] 185 | pub struct AccountToProve { 186 | pub address: String, 187 | pub slots: Vec, 188 | } 189 | 190 | impl Default for BlockStateAccesses { 191 | fn default() -> Self { 192 | Self::new() 193 | } 194 | } 195 | 196 | /// For an initial set of state values for one account, looks for state accesses to 197 | /// previously untouched state. 198 | /// 199 | /// Adds the new state into the old one. 200 | fn include_unseen_states(existing: &AccountState, accessed_state: &AccountState) -> AccountState { 201 | let mut updated = existing.clone(); 202 | // Work out if any parts are new. 203 | if existing.code.is_none() { 204 | updated.code = accessed_state.code.clone(); 205 | } 206 | if existing.nonce.is_none() { 207 | updated.nonce = accessed_state.nonce; 208 | } 209 | let updated_storage = match existing.storage.clone() { 210 | None => accessed_state.storage.clone(), 211 | Some(mut existing_storage) => { 212 | // Look up each new storage key. 213 | if let Some(new_storage) = &accessed_state.storage { 214 | for (k, v) in new_storage { 215 | if !existing_storage.contains_key(k) { 216 | existing_storage.insert(k.clone(), v.clone()); 217 | } 218 | } 219 | } 220 | Some(existing_storage) 221 | } 222 | }; 223 | updated.storage = updated_storage; 224 | updated 225 | } 226 | 227 | #[cfg(test)] 228 | mod tests { 229 | use super::*; 230 | 231 | fn dummy_state_1() -> AccountState { 232 | let mut slots = HashMap::new(); 233 | slots.insert("555".to_string(), "333".to_string()); 234 | AccountState { 235 | balance: "123".to_string(), 236 | code: Some("608040".to_string()), 237 | nonce: Some(2), 238 | storage: Some(slots), 239 | } 240 | } 241 | fn dummy_state_2() -> AccountState { 242 | AccountState { 243 | balance: "123".to_string(), 244 | code: None, 245 | nonce: None, 246 | storage: None, 247 | } 248 | } 249 | 250 | /// Tests that changes to previously accessed state are ignored. 251 | #[test] 252 | fn test_modified_states_ignored() { 253 | let initial = dummy_state_1(); 254 | let mut slots = HashMap::new(); 255 | // New storage key. 256 | slots.insert("777".to_string(), "333".to_string()); 257 | // Previously seen storage key. 258 | slots.insert("555".to_string(), "444".to_string()); 259 | let accessed = AccountState { 260 | balance: "456".to_string(), 261 | code: Some("11111".to_string()), 262 | nonce: Some(200), 263 | storage: Some(slots), 264 | }; 265 | let result = include_unseen_states(&initial, &accessed); 266 | assert_ne!(initial, result); 267 | assert_eq!(result.balance, "123"); 268 | assert_eq!(result.code, Some("608040".to_string())); 269 | assert_eq!(result.nonce, Some(2)); 270 | assert_eq!(result.storage.unwrap().len(), 2); 271 | } 272 | #[test] 273 | fn test_seen_states_ignored() { 274 | let a = dummy_state_1(); 275 | let b = a.clone(); 276 | let result = include_unseen_states(&a, &b); 277 | assert_eq!(a, result); 278 | } 279 | #[test] 280 | fn test_seen_states_ignored_2() { 281 | let a = dummy_state_2(); 282 | let b = a.clone(); 283 | let result = include_unseen_states(&a, &b); 284 | assert_eq!(a, result); 285 | } 286 | /// Tests that if a state has not been seen yet that it is included. 287 | #[test] 288 | fn test_new_states_included() { 289 | let initial = dummy_state_2(); 290 | let mut slots = HashMap::new(); 291 | slots.insert("111".to_string(), "999".to_string()); 292 | let accessed = AccountState { 293 | balance: "123".to_string(), 294 | code: Some("608040".to_string()), 295 | nonce: Some(2), 296 | storage: Some(slots), 297 | }; 298 | let result = include_unseen_states(&initial, &accessed); 299 | assert_ne!(initial, result); 300 | assert_eq!(result.balance, "123"); 301 | assert_eq!(result.code, Some("608040".to_string())); 302 | assert_eq!(result.nonce, Some(2)); 303 | assert_eq!(result.storage.unwrap().len(), 1); 304 | } 305 | } 306 | -------------------------------------------------------------------------------- /crates/inventory/src/utils.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | array::TryFromSliceError, 3 | io::{self, Read}, 4 | num::TryFromIntError, 5 | }; 6 | 7 | use ethers::types::{H160, H256, U256, U64}; 8 | use hex::FromHexError; 9 | use ssz_rs::SimpleSerializeError; 10 | use thiserror::Error; 11 | 12 | use archors_types::alias::{SszH160, SszH256, SszU256, SszU64}; 13 | 14 | #[derive(Debug, Error)] 15 | pub enum UtilsError { 16 | #[error("IO error {0}")] 17 | IoError(#[from] io::Error), 18 | #[error("Hex utils error {0}")] 19 | HexUtils(#[from] FromHexError), 20 | #[error("SimpleSerialize Error {0}")] 21 | SimpleSerializeError(#[from] SimpleSerializeError), 22 | #[error("TryFromIntError {0}")] 23 | TryFromIntError(#[from] TryFromIntError), 24 | #[error("TryFromSlice utils error {0}")] 25 | TryFromSlice(#[from] TryFromSliceError), 26 | #[error("Hash must be 32 bytes")] 27 | InvalidHashLength, 28 | } 29 | 30 | /// Converts bytes to 0x-prefixed hex string. 31 | pub fn hex_encode>(bytes: T) -> String { 32 | format!("0x{}", hex::encode(bytes)) 33 | } 34 | 35 | /// Converts 0x-prefixed hex string to bytes. 36 | pub fn hex_decode>(string: T) -> Result, UtilsError> { 37 | let s = string.as_ref().trim_start_matches("0x"); 38 | Ok(hex::decode(s)?) 39 | } 40 | 41 | /// Performs snappy compression on bytes. 42 | /// 43 | /// Takes ssz bytes, returns ssz_snappy bytes. 44 | pub fn compress(ssz_bytes: Vec) -> Result, UtilsError> { 45 | /* 46 | Raw encoder (no frames): 47 | let mut snap_encoder = snap::raw::Encoder::new(); 48 | let compressed_vec = snap_encoder.compress_vec(ssz_bytes.as_slice())?; 49 | */ 50 | let mut buffer = vec![]; 51 | snap::read::FrameEncoder::new(ssz_bytes.as_slice()).read_to_end(&mut buffer)?; 52 | Ok(buffer) 53 | } 54 | 55 | /// Performs snappy decompression on bytes. 56 | /// 57 | /// Takes ssz_snappy bytes, returns ssz bytes. 58 | pub fn decompress(ssz_snappy_bytes: Vec) -> Result, UtilsError> { 59 | /* 60 | Raw decoder (no frames): 61 | let mut snap_decoder = snap::raw::Decoder::new(); 62 | let decompressed_vec = snap_decoder.decompress_vec(ssz_snappy_bytes.as_slice())?; 63 | */ 64 | let mut buffer = vec![]; 65 | snap::read::FrameDecoder::new(ssz_snappy_bytes.as_slice()).read_to_end(&mut buffer)?; 66 | Ok(buffer) 67 | } 68 | 69 | /// Convert ethers H256 to SSZ equivalent. 70 | pub fn h256_to_ssz_h256(input: H256) -> Result { 71 | Ok(SszH256::try_from(input.0.to_vec()).map_err(|e| e.1)?) 72 | } 73 | 74 | /// Convert ethers H160 to SSZ equivalent. 75 | pub fn h160_to_ssz_h160(input: H160) -> Result { 76 | Ok(SszH160::try_from(input.0.to_vec()).map_err(|e| e.1)?) 77 | } 78 | 79 | /// Convert ethers U256 to SSZ equivalent. 80 | /// 81 | /// Output is big endian. 82 | pub fn u256_to_ssz_u256(input: U256) -> SszU256 { 83 | let mut bytes = [0u8; 32]; 84 | input.to_big_endian(&mut bytes); 85 | 86 | let mut output = SszU256::default(); 87 | for byte in bytes.into_iter() { 88 | output.push(byte) 89 | } 90 | output 91 | } 92 | 93 | /// Convert ethers U64 to SSZ equivalent. 94 | /// 95 | /// Output is big endian. 96 | pub fn u64_to_ssz_u64(input: U64) -> SszU64 { 97 | let mut output = SszU64::default(); 98 | for byte in input.0[0].to_be_bytes() { 99 | output.push(byte) 100 | } 101 | output 102 | } 103 | 104 | /// Converts usize to u16 and prevents overflow. 105 | pub fn usize_to_u16(input: usize) -> Result { 106 | let num: u16 = input.try_into()?; 107 | Ok(num) 108 | } 109 | 110 | /// Converts an 0x-prefixed string to ethers H256. 111 | pub fn string_to_h256>(input: T) -> Result { 112 | let bytes = hex_decode(&input)?; 113 | if bytes.len() != 32 { 114 | return Err(UtilsError::InvalidHashLength); 115 | } 116 | Ok(H256::from_slice(&bytes)) 117 | } 118 | -------------------------------------------------------------------------------- /crates/multiproof/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "archors_multiproof" 3 | version = "0.1.0" 4 | edition = "2021" 5 | description = "Tool for working with EIP-1186 as multiproofs that cover a single block state" 6 | 7 | 8 | [dependencies] 9 | archors_verify = { path = "../verify" } 10 | archors_types = { path = "../types" } 11 | ethers = "2.0.4" 12 | hex = "0.4.3" 13 | log = { workspace = true } 14 | revm = { version = "3.3.0", features = ["serde"] } 15 | rlp = "0.5.2" 16 | rlp-derive = "0.1.0" 17 | serde = { version = "1.0.152", features = ["derive"] } 18 | serde_json = "1.0.94" 19 | thiserror = "1.0.40" 20 | -------------------------------------------------------------------------------- /crates/multiproof/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod eip1186; 2 | pub use eip1186::EIP1186MultiProof; 3 | 4 | pub mod proof; 5 | pub mod oracle; 6 | pub mod node; 7 | pub mod utils; 8 | 9 | // Re-export trait for executing using the multiproof. 10 | pub use archors_types::execution::StateForEvm; 11 | -------------------------------------------------------------------------------- /crates/multiproof/src/node.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | 3 | use archors_verify::path::{NibblePath, PathError, PrefixEncoding}; 4 | use ethers::types::H256; 5 | use thiserror::Error; 6 | 7 | use crate::utils::hex_encode; 8 | 9 | #[derive(Debug, Error)] 10 | pub enum NodeError { 11 | #[error("Node has no items")] 12 | NodeEmpty, 13 | #[error("Node item has no encoding")] 14 | NoEncoding, 15 | #[error("PathError {0}")] 16 | PathError(#[from] PathError), 17 | #[error("Node has invalid item count {0}")] 18 | NodeHasInvalidItemCount(usize), 19 | } 20 | 21 | #[derive(Debug, PartialEq)] 22 | pub enum NodeKind { 23 | Branch, 24 | Extension, 25 | Leaf, 26 | } 27 | 28 | impl NodeKind { 29 | pub fn deduce(node: &[Vec]) -> Result { 30 | match node.len() { 31 | 17 => Ok(NodeKind::Branch), 32 | 2 => { 33 | // Leaf or extension 34 | let partial_path = node.first().ok_or(NodeError::NodeEmpty)?; 35 | let encoding = partial_path.first().ok_or(NodeError::NoEncoding)?; 36 | Ok(match PrefixEncoding::try_from(encoding)? { 37 | PrefixEncoding::ExtensionEven | PrefixEncoding::ExtensionOdd(_) => { 38 | NodeKind::Extension 39 | } 40 | PrefixEncoding::LeafEven | PrefixEncoding::LeafOdd(_) => NodeKind::Leaf, 41 | }) 42 | } 43 | num => Err(NodeError::NodeHasInvalidItemCount(num)), 44 | } 45 | } 46 | } 47 | 48 | /// A cache of the nodes visited. If the trie is modified, then 49 | /// this can be used to update hashes back to the root. 50 | #[derive(Debug)] 51 | pub struct VisitedNode { 52 | pub kind: NodeKind, 53 | pub node_hash: H256, 54 | /// Item within the node that was followed to get to the next node. 55 | pub item_index: usize, 56 | /// The path that was followed to get to the node. 57 | /// 58 | /// This allows new nodes to be added/removed as needed during proof modification. 59 | pub traversal_record: NibblePath, 60 | } 61 | 62 | impl Display for VisitedNode { 63 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 64 | write!( 65 | f, 66 | "Visited {:?} node (hash: {}), followed index {} in node", 67 | self.kind, 68 | hex_encode(self.node_hash), 69 | self.item_index 70 | ) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /crates/multiproof/src/oracle.rs: -------------------------------------------------------------------------------- 1 | //! Module for resolving the situation where a storage value is set to zero by a block. 2 | //! 3 | //! When a value is set to zero, the key is removed from the trie. If the parent was a branch 4 | //! with only two children, the branch now has one child and must be removed. To update the 5 | //! grandparent, either the sibling RLP must be known (infeasible using eth_getProof), or the 6 | //! grandparent can be obtained from an oracle. The oracle is constructed by selecting data from 7 | //! proofs from the block-post state. 8 | //! 9 | //! The oracle contains a map of key -> (traversal path, grandparent post-state). 10 | //! 11 | //! There are two scenarios where the oracle is used: 12 | //! - Whenever a parent branch needs to be deleted, the traversal path is noted and the grandparent 13 | //! fetched from the oracle. 14 | //! - Whenever a traversal is made along an already-oracle-fetched grandparent. The traversal 15 | //! changes are assumed to be valid. 16 | //! - The changes could be validated by following the outdated grandparent and when all changes 17 | //! in that subtree are made, check that they match the hash in the grandparent. This is currently 18 | //! not implemented and so an EVM bug in these values is possible. This is likely a very small 19 | //! number of storage values. 20 | //! 21 | //! 22 | //! The scenario is also described in the multiproof crate readme. 23 | //! 24 | //! ### Algorithm 25 | //! 1. When traversing and the need for the oracle arises, the grandparent that needs updating is 26 | //! recorded, including the traversal within it that was affected (e.g., branch node item index 5). 27 | //! 2. It is not immediately updated. 28 | //! 3. The remaining storage changes are applied for the account. 29 | //! 4. Then the grandparent is fetched. 30 | //! 5. The members of the node that are not the affected item are checked against the existing 31 | //! equivalent node. 32 | //! 6. If they are the same, the new item is accepted and the changes updated to the storage root. 33 | 34 | use std::fmt::Display; 35 | 36 | use archors_types::oracle::TrieNodeOracle; 37 | use ethers::types::{H160, H256}; 38 | use serde::{Deserialize, Serialize}; 39 | 40 | use crate::{proof::Node, utils::hex_encode}; 41 | 42 | /// A node that has been flagged as requiring an oracle to be updated. 43 | /// 44 | /// This is a consequence of a node restructuring following a node change from an inclusion 45 | /// proof to exclusion proof and subsequent removal of a parent branch. An oracle task is generated 46 | /// for the granparent. 47 | /// 48 | /// Tasks are completed once all other possible storage updates have been applied. Tasks are then 49 | /// fulfilled starting with the task with the highest traversal index. 50 | #[derive(Clone, Debug, Deserialize, Serialize)] 51 | pub struct OracleTask { 52 | /// Address involved 53 | pub address: H160, 54 | /// Storage key involved 55 | pub key: H256, 56 | /// The index into the trie path that matches the node that needs to be looked up. 57 | pub traversal_index: usize, 58 | /// Whether the task is for a key that is going to be included or excluded. 59 | pub purpose: TaskType, 60 | } 61 | 62 | /// The reason an oracle is required. The key that requires an oracle: is it being added/included or 63 | /// removed/excluded from the trie? 64 | /// 65 | /// This flag allows for testing that after the oracle data is used for a key, that the key has 66 | /// a valid proof. 67 | #[derive(Clone, Debug, Deserialize, Serialize)] 68 | pub enum TaskType { 69 | ForInclusion(Vec), 70 | ForExclusion, 71 | } 72 | 73 | impl Display for OracleTask { 74 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 75 | write!( 76 | f, 77 | "oracle task for address {}, storage key {} and traversal index {}", 78 | hex_encode(self.address), 79 | hex_encode(self.key), 80 | self.traversal_index 81 | ) 82 | } 83 | } 84 | 85 | impl OracleTask { 86 | /// Generate a new task. 87 | pub fn new(address: H160, key: H256, traversal_index: usize, purpose: TaskType) -> Self { 88 | OracleTask { 89 | address, 90 | key, 91 | traversal_index, 92 | purpose, 93 | } 94 | } 95 | /// Gets the node from the oracle, performs checks and returns the node hash. 96 | /// 97 | /// Checks that the non-salient (not on the task path) items match the partially updated node 98 | /// items. 99 | pub fn complete_task(&self, partially_updated: Node, oracle: &TrieNodeOracle) -> H256 { 100 | todo!() 101 | } 102 | /// Returns the item index in the node that was the reason for needing an oracle. 103 | fn get_salient_item_index() -> usize { 104 | todo!() 105 | } 106 | /// Fetches the node from the oracle 107 | fn fetch_from_oracle(&self, oracle: TrieNodeOracle) -> Node { 108 | todo!() 109 | } 110 | /// Returns the path for the task 111 | fn path(&self) { 112 | todo!() 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /crates/multiproof/src/utils.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | array::TryFromSliceError, 3 | io::{self}, 4 | }; 5 | 6 | use hex::FromHexError; 7 | use thiserror::Error; 8 | 9 | #[derive(Debug, Error)] 10 | pub enum UtilsError { 11 | #[error("IO error {0}")] 12 | IoError(#[from] io::Error), 13 | #[error("Hex utils error {0}")] 14 | HexUtils(#[from] FromHexError), 15 | #[error("TryFromSlice utils error {0}")] 16 | TryFromSlice(#[from] TryFromSliceError), 17 | } 18 | 19 | /// Converts bytes to 0x-prefixed hex string. 20 | pub fn hex_encode>(bytes: T) -> String { 21 | format!("0x{}", hex::encode(bytes)) 22 | } 23 | 24 | /// Converts 0x-prefixed hex string to bytes. 25 | pub fn hex_decode>(string: T) -> Result, UtilsError> { 26 | let s = string.as_ref().trim_start_matches("0x"); 27 | Ok(hex::decode(s)?) 28 | } 29 | -------------------------------------------------------------------------------- /crates/tracer/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "archors_tracer" 3 | version = "0.1.0" 4 | edition = "2021" 5 | description = "Tool for tracing an Ethereum block using state from EIP-1186 proofs" 6 | 7 | [dependencies] 8 | archors_types = { path = "../types" } 9 | ethers = "2.0.4" 10 | hex = "0.4.3" 11 | log = { workspace = true } 12 | revm = { version = "3.3.0", features = ["serde"] } 13 | serde = { version = "1.0.152", features = ["derive"] } 14 | serde_json = "1.0.94" 15 | thiserror = "1.0.40" 16 | 17 | [dev-dependencies] 18 | archors_inventory = { path = "../inventory" } 19 | -------------------------------------------------------------------------------- /crates/tracer/README.md: -------------------------------------------------------------------------------- 1 | # archors-tracer 2 | 3 | For replaying Ethereum blocks using state from EIP-1186 proofs. 4 | 5 | Sort of like a tiny trustless archive node that can only do one block. 6 | 7 | ## Why 8 | 9 | When an ethereum block is retrieved with transaction details via `eth_getBlockByNumber` it doesn't have everything in it. 10 | 11 | 12 | There are things that the EVM did that you cannot ascertain without tracing the block. 13 | An archive node can trace the block because it can work out the state at any block. 14 | 15 | However, if you have a proof for every state that you need in the block, you can trustlessly 16 | trace the block. 17 | 18 | If there is a data source of these proofs, then you could trace individual blocks. 19 | 20 | ## How 21 | 22 | First verify the proof data (see archors-verify) against the block state root present 23 | in the previous header. Then, start executing the first transaction. 24 | 25 | Each time the EVM needs access to state (E.g., storage load via `SLOAD` for some key), 26 | look up the key in the proof data and get the value there. 27 | 28 | If the transaction modifies state data (E.g., storage store via `SSTORE` for some key), 29 | make a note of what the new value is (in case it is used later in the block). 30 | 31 | Output the transaction EIP-3155 compliant traces in a way that conforms to `eth_debugTraceTransaction`. 32 | 33 | -------------------------------------------------------------------------------- /crates/tracer/src/evm.rs: -------------------------------------------------------------------------------- 1 | //! For creation and use of an EVM for a single block. 2 | 3 | use std::io::stdout; 4 | 5 | use archors_types::utils::{ 6 | access_list_e_to_r, eu256_to_ru256, eu256_to_u64, eu64_to_ru256, ru256_to_u64, UtilsError, 7 | }; 8 | use ethers::types::{Block, Transaction}; 9 | use revm::{ 10 | db::{CacheDB, EmptyDB}, 11 | inspectors::{NoOpInspector, TracerEip3155}, 12 | primitives::{EVMError, ResultAndState, TransactTo, TxEnv, U256}, 13 | EVM, 14 | }; 15 | use thiserror::Error; 16 | 17 | /// An error with tracing a block 18 | #[derive(Debug, Error, PartialEq)] 19 | pub enum EvmError { 20 | #[error("Attempted to set block environment twice")] 21 | BlockEnvAlreadySet, 22 | #[error("Expected a block author (coinbase) to set up the EVM, found none")] 23 | NoBlockAuthor, 24 | #[error("Expected a block number to set up the EVM, found none")] 25 | NoBlockNumber, 26 | #[error("Attempted to execute transaction before setting environment")] 27 | TxNotSet, 28 | #[error("Attempted to set transaction environment twice")] 29 | TxAlreadySet, 30 | #[error("UtilsError {0}")] 31 | UtilsError(#[from] UtilsError), 32 | #[error("revm Error {0}")] 33 | RevmError(String), 34 | } 35 | 36 | // A wrapper to implement handy methods for working with the revm EVM. 37 | #[derive(Clone)] 38 | pub struct BlockEvm { 39 | pub evm: EVM>, 40 | tx_env_status: TxStatus, 41 | block_env_status: BlockStatus, 42 | } 43 | 44 | impl BlockEvm { 45 | /// Create the EVM and insert a populated database of state values. 46 | /// 47 | /// The DB should contain the states required to execute the intended transactions. 48 | pub fn init_from_db(db: CacheDB) -> Self { 49 | let mut evm = EVM::new(); 50 | evm.database(db); 51 | Self { 52 | evm, 53 | tx_env_status: TxStatus::NotLoaded, 54 | block_env_status: BlockStatus::NotSet, 55 | } 56 | } 57 | /// Set the chain ID (mainnet = 1). 58 | pub fn add_chain_id(&mut self, id: U256) -> &mut Self { 59 | self.evm.env.cfg.chain_id = U256::from(id); 60 | self 61 | } 62 | /// Set initial block values (BaseFee, GasLimit, ..., Etc.). 63 | pub fn add_block_environment( 64 | &mut self, 65 | block: &Block, 66 | ) -> Result<&mut Self, EvmError> { 67 | if self.block_env_status == BlockStatus::Set { 68 | return Err(EvmError::BlockEnvAlreadySet); 69 | } 70 | let env = &mut self.evm.env.block; 71 | 72 | env.number = eu64_to_ru256(block.number.ok_or(EvmError::NoBlockNumber)?); 73 | env.coinbase = block.author.ok_or(EvmError::NoBlockAuthor)?.into(); 74 | env.timestamp = block.timestamp.into(); 75 | env.gas_limit = block.gas_limit.into(); 76 | env.basefee = block.base_fee_per_gas.unwrap_or_default().into(); 77 | env.difficulty = block.difficulty.into(); 78 | env.prevrandao = Some(block.difficulty.into()); 79 | self.block_env_status = BlockStatus::Set; 80 | Ok(self) 81 | } 82 | /// Set the spec id (hard fork definition). 83 | pub fn add_spec_id(&mut self, _block: &Block) -> Result<&mut Self, EvmError> { 84 | // TODO. E.g., 85 | // if block x < block.number < y, 86 | // self.env.cfg.spec_id = SpecId::Constantinople 87 | Ok(self) 88 | } 89 | /// Add a single transaction environment (index, sender, recipient, etc.). 90 | pub fn add_transaction_environment(&mut self, tx: Transaction) -> Result<&mut Self, EvmError> { 91 | self.tx_env_status.ready_to_set()?; 92 | 93 | let caller = tx.from.into(); 94 | let gas_limit = eu256_to_u64(tx.gas); 95 | let gas_price = match tx.gas_price { 96 | Some(price) => eu256_to_ru256(price)?, 97 | None => todo!("handle Type II transaction gas price"), 98 | }; 99 | let gas_priority_fee = match tx.max_priority_fee_per_gas { 100 | Some(fee) => Some(eu256_to_ru256(fee)?), 101 | None => None, 102 | }; 103 | let transact_to = match tx.to { 104 | Some(to) => TransactTo::Call(to.into()), 105 | None => todo!("handle tx create scheme"), // TransactTo::Create(), 106 | }; 107 | let value = tx.value.into(); 108 | let data = tx.input.0; 109 | let chain_id = Some(ru256_to_u64(self.evm.env.cfg.chain_id)); 110 | let nonce = Some(eu256_to_u64(tx.nonce)); 111 | let access_list = match tx.access_list { 112 | Some(list_in) => access_list_e_to_r(list_in), 113 | None => vec![], 114 | }; 115 | 116 | let new_tx_env: TxEnv = TxEnv { 117 | caller, 118 | gas_limit, 119 | gas_price, 120 | gas_priority_fee, 121 | transact_to, 122 | value, 123 | data, 124 | chain_id, 125 | nonce, 126 | access_list, 127 | }; 128 | self.evm.env.tx = new_tx_env; 129 | self.tx_env_status.set()?; 130 | Ok(self) 131 | } 132 | /// Execute a loaded transaction with an inspector to produce an EIP-3155 style trace. 133 | /// Runs the transaction twice (once for state change, once to commit). 134 | /// 135 | /// This applies the transaction, monitors the output and leaves the EVM ready for the 136 | /// next transaction to be added. 137 | pub fn execute_with_inspector_eip3155(&mut self) -> Result { 138 | self.tx_env_status.ready_to_execute()?; 139 | // Run the tx to get the state changes, but don't commit to the EVM env yet. 140 | // The changes will be used to compute the post-tx state root. 141 | // Use a dummy inspector. 142 | let noop_inspector = NoOpInspector {}; 143 | let state_changes = self.evm.inspect_ref(noop_inspector)?; 144 | 145 | // Now run the tx again and this time commit the changes. 146 | // see: https://github.com/bluealloy/revm/blob/main/bins/revme/src/statetest/runner.rs#L259 147 | // Initialize the inspector 148 | let inspector = TracerEip3155::new(Box::new(stdout()), true, true); 149 | let _outcome = self.evm.inspect_commit(inspector).map_err(EvmError::from)?; 150 | self.tx_env_status.executed()?; 151 | Ok(state_changes) 152 | } 153 | /// Execute a loaded transaction without an inspector. 154 | /// 155 | /// This applies the transaction and leaves the EVM ready for the 156 | /// next transaction to be added. 157 | pub fn execute_without_inspector(&mut self) -> Result { 158 | self.tx_env_status.ready_to_execute()?; 159 | // Run the tx to get the state changes, but don't commit to the EVM env yet. 160 | // The changes will be used to compute the post-tx state root. 161 | // Use a dummy inspector. 162 | let noop_inspector = NoOpInspector {}; 163 | let state_changes = self.evm.inspect_ref(noop_inspector)?; 164 | 165 | // Now run the tx again, this time to commit the changes. 166 | let _outcome = self.evm.transact_commit().map_err(EvmError::from)?; 167 | self.tx_env_status.executed()?; 168 | Ok(state_changes) 169 | } 170 | } 171 | 172 | /// Transactions are executed individually, this status prevents accidental 173 | /// double-loading. 174 | #[derive(Clone, Debug, Eq, PartialEq)] 175 | enum TxStatus { 176 | Loaded, 177 | NotLoaded, 178 | } 179 | 180 | /// This status prevents accidental double-loading of block between transactions. 181 | #[derive(Clone, Debug, Eq, PartialEq)] 182 | enum BlockStatus { 183 | Set, 184 | NotSet, 185 | } 186 | 187 | /// Readable state manager for whether a transaction is set or not. 188 | impl TxStatus { 189 | fn ready_to_execute(&self) -> Result<(), EvmError> { 190 | match self { 191 | TxStatus::Loaded => Ok(()), 192 | TxStatus::NotLoaded => Err(EvmError::TxNotSet), 193 | } 194 | } 195 | fn ready_to_set(&self) -> Result<(), EvmError> { 196 | match self { 197 | TxStatus::Loaded => Err(EvmError::TxAlreadySet), 198 | TxStatus::NotLoaded => Ok(()), 199 | } 200 | } 201 | fn executed(&mut self) -> Result<(), EvmError> { 202 | self.ready_to_execute()?; 203 | *self = TxStatus::NotLoaded; 204 | Ok(()) 205 | } 206 | fn set(&mut self) -> Result<(), EvmError> { 207 | self.ready_to_set()?; 208 | *self = TxStatus::Loaded; 209 | Ok(()) 210 | } 211 | } 212 | 213 | /// Convert revm Error type (no Display impl) to local error type. 214 | impl From> for EvmError { 215 | fn from(value: EVMError) -> Self { 216 | let e = match value { 217 | EVMError::Transaction(t) => { 218 | match serde_json::to_string(&t).map_err(|e| e.to_string()) { 219 | Ok(tx_err) => tx_err, 220 | Err(serde_err) => serde_err, 221 | } 222 | } 223 | // _d is Infallible - ignore. 224 | EVMError::Database(_d) => "database error".to_string(), 225 | EVMError::PrevrandaoNotSet => String::from("prevrandao error"), 226 | }; 227 | EvmError::RevmError(e) 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /crates/tracer/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod evm; 2 | pub mod state; 3 | pub mod trace; 4 | -------------------------------------------------------------------------------- /crates/tracer/src/state.rs: -------------------------------------------------------------------------------- 1 | //! For representing state for an historical block. 2 | 3 | use std::collections::HashMap; 4 | 5 | use archors_types::{ 6 | execution::{EvmStateError, StateForEvm}, 7 | utils::{eh256_to_ru256, eu256_to_ru256, eu64_to_ru256, hex_encode}, proof::{DisplayProof, DisplayStorageProof}, 8 | }; 9 | use ethers::types::{EIP1186ProofResponse, H160, H256, U64}; 10 | use revm::{ 11 | db::{CacheDB, EmptyDB}, 12 | primitives::{Account, AccountInfo, Bytecode, Bytes, HashMap as rHashMap, B160, B256, U256}, 13 | }; 14 | 15 | /// A basic map of accounts to proofs. Includes all state required to trace a block. 16 | /// 17 | /// 'Basic' referring to the presence of duplicated trie nodes throughout the data. 18 | /// 19 | /// ### Code 20 | /// The account proofs refer to code by keccack(code), this map provides the code 21 | /// that was obtained with the prestate tracer. 22 | /// 23 | /// ### Blockhash 24 | /// The EVM opcode BLOCKHASH accesses old block hashes. These are detected and 25 | /// cached using eth_traceBlock and then included here. 26 | pub struct BlockProofsBasic { 27 | /// Map of account -> proof 28 | pub proofs: HashMap, 29 | /// Map of codehash -> code 30 | pub code: HashMap>, 31 | /// Map of block number -> block hash 32 | pub block_hashes: HashMap, 33 | } 34 | 35 | impl StateForEvm for BlockProofsBasic { 36 | fn get_account_info(&self, address: &B160) -> Result { 37 | let account = self 38 | .proofs 39 | .get(&address.0.into()) 40 | .ok_or_else(|| EvmStateError::NoProofForAddress(hex_encode(address)))?; 41 | 42 | let code: Option = self.code.get(&account.code_hash).map(|data| { 43 | let revm_bytes = Bytes::copy_from_slice(data); 44 | Bytecode::new_raw(revm_bytes) 45 | }); 46 | let info = AccountInfo { 47 | balance: account.balance.into(), 48 | nonce: account.nonce.as_u64(), 49 | code_hash: account.code_hash.0.into(), 50 | code, 51 | }; 52 | Ok(info) 53 | } 54 | fn get_account_storage(&self, address: &B160) -> Result, EvmStateError> { 55 | let account = self 56 | .proofs 57 | .get(&address.0.into()) 58 | .ok_or_else(|| EvmStateError::NoProofForAddress(hex_encode(address)))?; 59 | 60 | // Storage key-val pairs for the account. 61 | let mut storage: rHashMap = rHashMap::new(); 62 | 63 | for storage_data in &account.storage_proof { 64 | // U256 ethers -> U256 revm 65 | let key = eh256_to_ru256(storage_data.key); 66 | let value = eu256_to_ru256(storage_data.value)?; 67 | 68 | storage.insert(key, value); 69 | } 70 | 71 | Ok(storage) 72 | } 73 | fn addresses(&self) -> Vec { 74 | self.proofs 75 | .keys() 76 | .map(|address| B160::from(address.0)) 77 | .collect() 78 | } 79 | 80 | fn get_blockhash_accesses(&self) -> Result, EvmStateError> { 81 | let mut accesses = rHashMap::new(); 82 | for access in self.block_hashes.iter() { 83 | let num: U256 = eu64_to_ru256(*access.0); 84 | let hash: B256 = access.1 .0.into(); 85 | accesses.insert(num, hash); 86 | } 87 | Ok(accesses) 88 | } 89 | 90 | fn state_root_post_block( 91 | &mut self, 92 | _changes: HashMap, 93 | ) -> Result { 94 | unimplemented!("Post execution root check is not implemented for basic proof data format.") 95 | } 96 | 97 | fn print_account_proof>(&self, _account_address: T) -> Result { 98 | todo!() 99 | } 100 | 101 | fn print_storage_proof>( 102 | &self, 103 | _account_address: T, 104 | _storage_key: T, 105 | ) -> Result { 106 | todo!() 107 | } 108 | } 109 | 110 | /// Inserts state from a collection of EIP-1186 proof into an in-memory DB. 111 | /// The DB can then be used by the EVM to read/write state during execution. 112 | pub fn build_state_from_proofs(block_proofs: &T) -> Result, EvmStateError> 113 | where 114 | T: StateForEvm, 115 | { 116 | let mut db = CacheDB::new(EmptyDB::default()); 117 | 118 | for address in block_proofs.addresses() { 119 | let info = block_proofs.get_account_info(&address)?; 120 | db.insert_account_info(address, info); 121 | 122 | let storage = block_proofs.get_account_storage(&address)?; 123 | db.replace_account_storage(address, storage) 124 | .map_err(|source| EvmStateError::AccountStorageInit { 125 | error: source.to_string(), 126 | address: hex_encode(address), 127 | })?; 128 | } 129 | Ok(db) 130 | } 131 | 132 | #[cfg(test)] 133 | mod test { 134 | use std::str::FromStr; 135 | 136 | use revm::primitives::B256; 137 | 138 | use super::*; 139 | 140 | #[test] 141 | fn test_block_proofs_basic_get_account_info() { 142 | let mut state = BlockProofsBasic { 143 | proofs: HashMap::default(), 144 | code: HashMap::default(), 145 | block_hashes: HashMap::default(), 146 | }; 147 | let mut proof = EIP1186ProofResponse::default(); 148 | let address = H160::from_str("0x0300000000000000000000000000000000000000").unwrap(); 149 | proof.address = address; 150 | let balance = "0x0000000000000000000000000000000000000000000000000000000000000009"; 151 | let nonce = 7u64; 152 | proof.balance = ethers::types::U256::from_str(balance).unwrap(); 153 | proof.nonce = nonce.into(); 154 | state.proofs.insert(address, proof); 155 | 156 | let retreived_account = state.get_account_info(&address.0.into()).unwrap(); 157 | let expected_account = AccountInfo { 158 | balance: U256::from_str(balance).unwrap(), 159 | nonce, 160 | code_hash: B256::zero(), 161 | code: None, 162 | }; 163 | assert_eq!(retreived_account, expected_account); 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /crates/types/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "archors_types" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | ethers = "2.0.4" 8 | hex = "0.4.3" 9 | revm = { version = "3.3.0", features = ["serde"] } 10 | thiserror = "1.0.40" 11 | ssz_rs = "0.8.0" 12 | ssz_rs_derive = "0.8.0" 13 | -------------------------------------------------------------------------------- /crates/types/src/alias.rs: -------------------------------------------------------------------------------- 1 | //! Useful Simple Serialize (SSZ) aliases 2 | 3 | use ssz_rs::{List, Vector}; 4 | 5 | // Variable (uint) 6 | /// U64 equivalent 7 | pub type SszU64 = List; 8 | /// U256 equivalent 9 | pub type SszU256 = List; 10 | 11 | // Fixed (hash, address) 12 | /// H256 Equivalent 13 | pub type SszH256 = Vector; 14 | /// H160 Equivalent 15 | pub type SszH160 = Vector; 16 | -------------------------------------------------------------------------------- /crates/types/src/constants.rs: -------------------------------------------------------------------------------- 1 | //! Useful Simple Serialize (SSZ) constants 2 | 3 | 4 | /// Number of prior blockhashes a block could access via the BLOCKHASH opcode. 5 | pub const MAX_BLOCKHASH_READS_PER_BLOCK: usize = 256; 6 | 7 | /// Maximum number of bytes permitted for an RLP encoded trie node. Set to 2**15. 8 | pub const MAX_BYTES_PER_NODE: usize = 32768; 9 | 10 | /// Maximum number of bytes a contract bytecode is permitted to be. Set to 2**15. 11 | pub const MAX_BYTES_PER_CONTRACT: usize = 32768; 12 | 13 | /// Maximum number of contract that can be accessed in a block. Set to 2**11. 14 | pub const MAX_CONTRACTS_PER_BLOCK: usize = 2048; 15 | 16 | /// Maximum number of nodes permitted in a merkle patricia proof. Set to 2**6. 17 | pub const MAX_NODES_PER_PROOF: usize = 64; 18 | 19 | /// Maximum number of intermediate nodes permitted for all proofs. 20 | /// Proofs are for the execution of a single block. Set to 2**15. 21 | pub const MAX_NODES_PER_BLOCK: usize = 32768; 22 | 23 | /// Maximum number of account proofs permitted. Proofs are for the execution 24 | /// of a single block. Set to 2**13. 25 | pub const MAX_ACCOUNT_PROOFS_PER_BLOCK: usize = 8192; 26 | 27 | /// Maximum number of storage proofs permitted per account. 28 | /// Proofs are for the execution of a single block. Set to 2**13. 29 | pub const MAX_STORAGE_PROOFS_PER_ACCOUNT: usize = 8192; 30 | -------------------------------------------------------------------------------- /crates/types/src/execution.rs: -------------------------------------------------------------------------------- 1 | //! Interface types for making a data structure executable in the revm. 2 | 3 | use std::collections::HashMap; 4 | 5 | use revm::primitives::{Account, AccountInfo, HashMap as rHashMap, B160, B256, U256}; 6 | use thiserror::Error; 7 | 8 | use crate::{utils::UtilsError, proof::{DisplayProof, DisplayStorageProof}}; 9 | 10 | /// An error with tracing a block 11 | #[derive(Debug, Error, PartialEq)] 12 | pub enum EvmStateError { 13 | #[error("Unable to get account state proof for address")] 14 | NoProofForAddress(String), 15 | #[error("Could not initialise storage for account {address}, error {error}")] 16 | AccountStorageInit { error: String, address: String }, 17 | #[error("Utils Error {0}")] 18 | UtilsError(#[from] UtilsError), 19 | #[error("Unable to verify post-execution state root: {0}")] 20 | PostRoot(String), 21 | #[error("Unable to parse address: {0}")] 22 | InvalidAddress(String), 23 | #[error("Unable to parse storage key: {0}")] 24 | InvalidStorageKey(String), 25 | #[error("Unable to display proof: {0}")] 26 | DisplayError(String), 27 | } 28 | 29 | /// Behaviour that any proof-based format must provide to be convertible into 30 | /// a revm DB. In other words, behaviour that makes the state data extractable for re-execution. 31 | /// 32 | /// Returned types are revm-based. 33 | pub trait StateForEvm { 34 | /// Gets account information in a format that can be inserted into a 35 | /// revm db. This includes contract bytecode. 36 | fn get_account_info(&self, address: &B160) -> Result; 37 | /// Gets all the addresses. 38 | fn addresses(&self) -> Vec; 39 | /// Gets the storage key-val pairs for the account of the address. 40 | fn get_account_storage(&self, address: &B160) -> Result, EvmStateError>; 41 | /// Gets BLOCKAHSH opcode accesses required for the block. 42 | /// Pairs are (block_number, block_hash). 43 | fn get_blockhash_accesses(&self) -> Result, EvmStateError>; 44 | /// Apply account changes received from the EVM for the entire block, compute the state 45 | /// root and return it. 46 | /// 47 | /// This function updates the proofs, but does not necessarily update the block prestate 48 | /// values in the data. That is, one should not assume that post-execution that the 49 | /// non-proof values are up to date. 50 | /// 51 | /// Note that some account updates may require additional information. Key deletion may 52 | /// remove nodes and restructure the trie. In this case, some additional nodes must be 53 | /// provided. 54 | fn state_root_post_block( 55 | &mut self, 56 | changes: HashMap, 57 | ) -> Result; 58 | /// Print an account proof. 59 | fn print_account_proof>(&self, account_address: T) -> Result; 60 | /// Print a storage proof for a given account. 61 | fn print_storage_proof>( 62 | &self, 63 | account_address: T, 64 | storage_key: T, 65 | ) -> Result; 66 | } 67 | -------------------------------------------------------------------------------- /crates/types/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod alias; 2 | pub mod constants; 3 | pub mod execution; 4 | pub mod proof; 5 | pub mod oracle; 6 | pub mod state; 7 | pub mod utils; 8 | -------------------------------------------------------------------------------- /crates/types/src/oracle.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use ethers::types::H160; 4 | 5 | /// Behaviour that defines an oracle for post-state trie data. When a block updates state 6 | /// in a way that removes nodes and reorganises the trie, more information may be required. 7 | /// The oracle provides this information. The information is obtained and cached with the 8 | /// block pre-state proofs so that post-state proofs can be computed. 9 | /// 10 | /// The oracle stores for each key, the proof nodes at and below the traversal index. 11 | #[derive(Debug, Default, Clone)] 12 | pub struct TrieNodeOracle(HashMap>>); 13 | 14 | impl TrieNodeOracle { 15 | /// Make an addition to the oracle. 16 | pub fn insert_nodes( 17 | &mut self, 18 | address: H160, 19 | traversal_to_target: Vec, 20 | nodes: Vec>, 21 | ) { 22 | self.0.insert( 23 | OracleTarget { 24 | address, 25 | traversal_to_target, 26 | }, 27 | nodes, 28 | ); 29 | } 30 | /// Retrieve data from the oracle for a particular address and key. 31 | /// 32 | /// The node returned will be the specific node that requires the oracle. This 33 | /// will be the grandparent of a removed node. 34 | pub fn lookup(&self, address: H160, traversal_to_target: Vec) -> Option>> { 35 | self.0 36 | .get(&OracleTarget { 37 | address, 38 | traversal_to_target, 39 | }) 40 | .map(|x| x.to_owned()) 41 | } 42 | } 43 | 44 | /// The key used to look up items in the oracle. Two storage key lookups are permitted to 45 | /// result in the same oracle result. 46 | /// 47 | /// Different storage keys may have common paths, and can share oracle data. For example, their 48 | /// traversals may both start with `[a,b,f]`. It may be that both are removed (e.g. they are the two 49 | /// items in the branch being removed) and both require an oracle. The oracle target can then 50 | /// be agnostic about which order the oracle updates occur in as both can be obtained by following 51 | /// the path. 52 | #[derive(Debug, Default, Clone, Eq, PartialEq, Hash)] 53 | struct OracleTarget { 54 | address: H160, 55 | /// Traversal to the target node, as nibbles. 56 | /// 57 | /// E.g., If the path is 0xa4fcb... and the node at traversal index 2, then the traversal 58 | /// is [0xa, 0x4, 0xf]. 59 | traversal_to_target: Vec, 60 | } 61 | -------------------------------------------------------------------------------- /crates/types/src/proof.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | 3 | use crate::utils::hex_encode; 4 | 5 | /// A display helper type for storage proofs. Contains of account proof that secures 6 | /// the storage. 7 | #[derive(Debug, Clone, PartialEq)] 8 | pub struct DisplayStorageProof { 9 | pub account: DisplayProof, 10 | pub storage: DisplayProof, 11 | } 12 | 13 | impl Display for DisplayStorageProof { 14 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 15 | write!( 16 | f, 17 | "Account proof:{}Storage proof:{}", 18 | self.account, self.storage 19 | ) 20 | } 21 | } 22 | 23 | /// A display helper type for proofs. 24 | /// 25 | /// Proofs consists of a vector of nodes, where nodes are vectors of rlp-encoded bytes. 26 | #[derive(Debug, Clone, PartialEq)] 27 | pub struct DisplayProof(Vec>); 28 | 29 | impl DisplayProof { 30 | pub fn init(proof: Vec>) -> Self { 31 | Self(proof) 32 | } 33 | /// Returns true if the proofs have a different final node. 34 | pub fn different_final_node(&self, second_proof: &DisplayProof) -> bool { 35 | self.0.last() != second_proof.0.last() 36 | } 37 | /// Returns the node index where two proofs differ. The root is checked last. 38 | pub fn divergence_point(&self, second_proof: &DisplayProof) -> Option { 39 | for i in (0..self.0.len()).rev() { 40 | if self.0.get(i) != second_proof.0.get(i) { 41 | return Some(i); 42 | } 43 | } 44 | None 45 | } 46 | /// Return the proof data. 47 | pub fn inner(&self) -> &[Vec] { 48 | &self.0 49 | } 50 | } 51 | 52 | impl Display for DisplayProof { 53 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 54 | write!(f, "\n----begin proof----\n")?; 55 | for node in &self.0 { 56 | write!(f, "\n{}\n", hex_encode(&node))?; 57 | } 58 | write!(f, "\n----end proof----\n")?; 59 | Ok(()) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /crates/types/src/state.rs: -------------------------------------------------------------------------------- 1 | //! Main data types defined by the spec, for transferrable parcels required for historical 2 | //! state execution. 3 | 4 | use std::collections::HashMap; 5 | 6 | use ssz_rs::prelude::*; 7 | use ssz_rs_derive::SimpleSerialize; 8 | use thiserror::Error; 9 | 10 | use crate::{ 11 | alias::{SszH160, SszH256, SszU256, SszU64}, 12 | constants::{ 13 | MAX_ACCOUNT_PROOFS_PER_BLOCK, MAX_BYTES_PER_CONTRACT, MAX_BYTES_PER_NODE, 14 | MAX_CONTRACTS_PER_BLOCK, MAX_NODES_PER_BLOCK, MAX_NODES_PER_PROOF, 15 | MAX_STORAGE_PROOFS_PER_ACCOUNT, 16 | }, 17 | execution::{EvmStateError, StateForEvm}, 18 | proof::{DisplayProof, DisplayStorageProof}, 19 | utils::{ssz_h256_to_rb256, ssz_h256_to_ru256, ssz_u256_to_ru256, ssz_u64_to_u64, UtilsError}, 20 | }; 21 | 22 | use revm::primitives::{ 23 | keccak256, Account, AccountInfo, Bytecode, BytecodeState, Bytes, HashMap as rHashMap, B160, 24 | B256, U256, 25 | }; 26 | 27 | #[derive(Debug, Error)] 28 | pub enum StateError { 29 | #[error("Deserialize Error {0}")] 30 | DerializeError(#[from] ssz_rs::DeserializeError), 31 | #[error("SSZ Error {0}")] 32 | SszError(#[from] SerializeError), 33 | #[error("SimpleSerialize Error {0}")] 34 | SimpleSerializeError(#[from] SimpleSerializeError), 35 | #[error("Utils Error {0}")] 36 | UtilsError(#[from] UtilsError), 37 | #[error("Unable to find index for node")] 38 | NoIndexForNode, 39 | } 40 | 41 | /// State that has items referred to using indices to deduplicate data. 42 | /// 43 | /// This store represents the minimum 44 | /// set of information that a peer should send to enable a block holder (eth_getBlockByNumber) 45 | /// to trace the block. 46 | /// 47 | /// Consists of: 48 | /// - A collection of EIP-1186 style proofs with intermediate nodes referred to in a separate list. 49 | /// EIP-1186 proofs consist of: 50 | /// - address, balance, codehash, nonce, storagehash, accountproofnodeindices, storageproofs 51 | /// - storageproofs: key, value, storageproofnodeindices 52 | /// - contract code. 53 | /// - account trie node. 54 | /// - storage trie node. 55 | #[derive(PartialEq, Eq, Debug, Default, SimpleSerialize)] 56 | pub struct RequiredBlockState { 57 | pub compact_eip1186_proofs: CompactEip1186Proofs, 58 | pub contracts: Contracts, 59 | pub trie_nodes: NodeBag, 60 | pub blockhashes: BlockHashes, 61 | } 62 | 63 | pub type CompactEip1186Proofs = List; 64 | /// A collection of trie nodes. Ordered lexicographically. 65 | /// 66 | /// Includes nodes from different tries (account, storage). 67 | /// 68 | /// The trie may be walked by: 69 | /// 1. Start with the state root hash or account storage hash 70 | /// 2. Hash all the nodes in the bag (into a dictionary) and select the one that matches (is the root node) 71 | /// 3. Hash the key/account to get the path 72 | /// 4. Follow the path, starting with the root node and decode the nodes and traverse them 73 | pub type NodeBag = List; 74 | pub type BlockHashes = List; 75 | 76 | /// RLP-encoded Merkle PATRICIA Trie node. 77 | pub type TrieNode = List; 78 | 79 | // Multiple contracts 80 | pub type Contracts = List; 81 | 82 | /// Contract bytecode. 83 | pub type Contract = List; 84 | 85 | /// A block hash for a recent block, for use by the BLOCKHASH opcode. 86 | #[derive(PartialEq, Eq, Debug, Default, SimpleSerialize)] 87 | pub struct RecentBlockHash { 88 | pub block_number: SszU64, 89 | pub block_hash: SszH256, 90 | } 91 | 92 | /// An EIP-1186 style proof with the trie nodes replaced by their keccak hashes. 93 | #[derive(PartialEq, Eq, Debug, Default, SimpleSerialize)] 94 | pub struct CompactEip1186Proof { 95 | pub address: SszH160, 96 | pub balance: SszU256, 97 | pub code_hash: SszH256, 98 | pub nonce: SszU64, 99 | pub storage_hash: SszH256, 100 | pub storage_proofs: CompactStorageProofs, 101 | } 102 | 103 | pub type CompactStorageProofs = List; 104 | 105 | /// An EIP-1186 style proof with the trie nodes replaced by their keccak hashes. 106 | #[derive(PartialEq, Eq, Debug, Default, SimpleSerialize)] 107 | pub struct CompactStorageProof { 108 | pub key: SszH256, 109 | pub value: SszU256, 110 | } 111 | 112 | /// An ordered list of indices that point to specific 113 | /// trie nodes in a different ordered list. 114 | /// 115 | /// The purpose is deduplication as some nodes appear in different proofs within 116 | /// the same block. 117 | pub type NodeIndices = List; 118 | 119 | impl RequiredBlockState { 120 | pub fn to_ssz_bytes(self) -> Result, StateError> { 121 | let mut buf = vec![]; 122 | let _ssz_bytes_len = self.serialize(&mut buf)?; 123 | Ok(buf) 124 | } 125 | pub fn from_ssz_bytes(ssz_data: Vec) -> Result { 126 | let proofs = self::deserialize(&ssz_data)?; 127 | Ok(proofs) 128 | } 129 | } 130 | 131 | impl StateForEvm for RequiredBlockState { 132 | fn get_account_info(&self, address: &B160) -> Result { 133 | let target = SszH160::try_from(address.0.to_vec()).unwrap(); 134 | for account in self.compact_eip1186_proofs.iter() { 135 | if account.address == target { 136 | let code_hash = ssz_h256_to_rb256(&account.code_hash); 137 | 138 | let code = self 139 | .contracts 140 | .iter() 141 | .find(|contract| keccak256(contract).eq(&code_hash)) 142 | .map(|ssz_bytes| { 143 | let bytes = ssz_bytes.to_vec(); 144 | let len = bytes.len(); 145 | Bytecode { 146 | bytecode: Bytes::from(bytes), 147 | hash: code_hash, 148 | state: BytecodeState::Checked { len }, 149 | } 150 | }); 151 | 152 | let account = AccountInfo { 153 | balance: ssz_u256_to_ru256(account.balance.to_owned())?, 154 | nonce: ssz_u64_to_u64(account.nonce.to_owned())?, 155 | code_hash, 156 | code, 157 | }; 158 | return Ok(account); 159 | } 160 | } 161 | Err(EvmStateError::NoProofForAddress(address.to_string())) 162 | } 163 | 164 | fn addresses(&self) -> Vec { 165 | self.compact_eip1186_proofs 166 | .iter() 167 | .map(|proof| B160::from_slice(&proof.address)) 168 | .collect() 169 | } 170 | 171 | fn get_account_storage(&self, address: &B160) -> Result, EvmStateError> { 172 | let target = SszH160::try_from(address.0.to_vec()).unwrap(); 173 | let mut storage_map = rHashMap::default(); 174 | for account in self.compact_eip1186_proofs.iter() { 175 | if account.address == target { 176 | for storage in account.storage_proofs.iter() { 177 | let key: U256 = ssz_h256_to_ru256(storage.key.to_owned())?; 178 | let value: U256 = ssz_u256_to_ru256(storage.value.to_owned())?; 179 | storage_map.insert(key, value); 180 | } 181 | } 182 | } 183 | Ok(storage_map) 184 | } 185 | 186 | fn get_blockhash_accesses(&self) -> Result, EvmStateError> { 187 | let mut accesses = rHashMap::default(); 188 | for access in self.blockhashes.iter() { 189 | let num = U256::from(ssz_u64_to_u64(access.block_number.to_owned())?); 190 | let hash: B256 = ssz_h256_to_rb256(&access.block_hash); 191 | accesses.insert(num, hash); 192 | } 193 | Ok(accesses) 194 | } 195 | 196 | fn state_root_post_block( 197 | &mut self, 198 | _changes: HashMap, 199 | ) -> Result { 200 | unimplemented!( 201 | "Post execution root check is not implemented for RequiredBlockState data format." 202 | ) 203 | } 204 | 205 | fn print_account_proof>( 206 | &self, 207 | _account_address: T, 208 | ) -> Result { 209 | todo!() 210 | } 211 | 212 | fn print_storage_proof>( 213 | &self, 214 | _account_address: T, 215 | _storage_key: T, 216 | ) -> Result { 217 | todo!() 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /crates/types/src/utils.rs: -------------------------------------------------------------------------------- 1 | use ethers::types::transaction::eip2930::AccessList; 2 | use hex::FromHexError; 3 | use revm::primitives::{B160, B256, U256}; 4 | use thiserror::Error; 5 | 6 | use crate::alias::{SszH256, SszU256, SszU64}; 7 | 8 | /// An error with tracing a block 9 | #[derive(Debug, Error, PartialEq)] 10 | pub enum UtilsError { 11 | #[error("Unable to convert SSZ list to revm U256")] 12 | InvalidU256List, 13 | #[error("Unable to convert SSZ vector to revm U256")] 14 | InvalidH256Vector, 15 | #[error("Unable to convert SSZ bytes to u64")] 16 | InvalidU64List, 17 | #[error("Unable to convert Ethers H256 ({0}) to revm U256 ")] 18 | H256ValueTooLarge(String), 19 | #[error("Unable to convert Ethers U256 ({0}) to revm U256 ")] 20 | U256ValueTooLarge(String), 21 | #[error("Hex utils error {0}")] 22 | HexUtils(#[from] FromHexError), 23 | } 24 | 25 | /// Converts bytes to 0x-prefixed hex string. 26 | pub fn hex_encode>(bytes: T) -> String { 27 | format!("0x{}", hex::encode(bytes)) 28 | } 29 | 30 | /// Converts 0x-prefixed hex string to bytes. 31 | pub fn hex_decode>(string: T) -> Result, UtilsError> { 32 | let s = string.as_ref().trim_start_matches("0x"); 33 | Ok(hex::decode(s)?) 34 | } 35 | 36 | /// Ethers U256 to revm U256 37 | pub fn eu256_to_ru256(input: ethers::types::U256) -> Result { 38 | let mut bytes = [0u8; 32]; 39 | input.to_big_endian(&mut bytes); 40 | let value = U256::from_be_bytes(bytes); 41 | Ok(value) 42 | } 43 | 44 | /// Ethers U64 to revm U256 45 | pub fn eu64_to_ru256(input: ethers::types::U64) -> U256 { 46 | U256::from_limbs_slice(input.0.as_slice()) 47 | } 48 | 49 | /// Ethers U256 to u64 50 | pub fn eu256_to_u64(input: ethers::types::U256) -> u64 { 51 | input.as_u64() 52 | } 53 | 54 | /// revm U256 to u64 55 | pub fn ru256_to_u64(input: U256) -> u64 { 56 | let limbs = input.as_limbs(); 57 | limbs[0] 58 | } 59 | 60 | /// Ethers H256 to revm U256 61 | pub fn eh256_to_ru256(input: ethers::types::H256) -> U256 { 62 | let bytes: &[u8; 32] = input.as_fixed_bytes(); 63 | 64 | U256::from_be_bytes(*bytes) 65 | } 66 | 67 | /// revm U256 to ethers U256 68 | pub fn ru256_to_eu256(input: U256) -> ethers::types::U256 { 69 | let slice = input.as_le_slice(); 70 | ethers::types::U256::from_little_endian(slice) 71 | } 72 | 73 | /// revm U256 to ethers H256 74 | pub fn ru256_to_eh256(input: U256) -> ethers::types::H256 { 75 | let array: [u8; 32] = input.to_be_bytes(); 76 | array.into() 77 | } 78 | 79 | /// revm B256 to ethers H256 80 | pub fn rb256_to_eh256(input: revm::primitives::B256) -> ethers::types::H256 { 81 | let bytes: &[u8; 32] = input.as_fixed_bytes(); 82 | bytes.into() 83 | } 84 | 85 | /// revm B160 to ethers H160 86 | pub fn rb160_to_eh160(input: &B160) -> ethers::types::H160 { 87 | let bytes: &[u8; 20] = input.as_fixed_bytes(); 88 | bytes.into() 89 | } 90 | 91 | /// Helper for revm access list type conversion. 92 | type RevmAccessList = Vec; 93 | 94 | /// Helper for revm access list item type conversion. 95 | type RevmAccessesListItem = (B160, Vec); 96 | 97 | /// Ethers AccessList to revm access list 98 | pub fn access_list_e_to_r(input: AccessList) -> RevmAccessList { 99 | input 100 | .0 101 | .into_iter() 102 | .map(|list| { 103 | let out_address: B160 = list.address.0.into(); 104 | let out_values: Vec = list.storage_keys.into_iter().map(eh256_to_ru256).collect(); 105 | (out_address, out_values) 106 | }) 107 | .collect() 108 | } 109 | 110 | /// Convert SSZ U256 equivalent to revm U256. 111 | /// 112 | /// Input is big endian. 113 | pub fn ssz_u256_to_ru256(input: SszU256) -> Result { 114 | U256::try_from_be_slice(input.as_slice()).ok_or(UtilsError::InvalidU256List) 115 | } 116 | 117 | /// Convert SSZ H256 equivalent to revm U256. 118 | /// 119 | /// Input is big endian. 120 | pub fn ssz_h256_to_ru256(input: SszH256) -> Result { 121 | U256::try_from_be_slice(input.as_slice()).ok_or(UtilsError::InvalidH256Vector) 122 | } 123 | 124 | /// Convert SSZ U64 equivalent to u64. 125 | /// 126 | /// Input is big endian. 127 | pub fn ssz_u64_to_u64(input: SszU64) -> Result { 128 | let bytes = input.as_slice(); 129 | let num = u64::from_be_bytes(bytes.try_into().map_err(|_| UtilsError::InvalidU64List)?); 130 | Ok(num) 131 | } 132 | 133 | /// Convert SSZ H256 equivalent to revm B256. 134 | /// 135 | /// Input is big endian. 136 | pub fn ssz_h256_to_rb256(input: &SszH256) -> B256 { 137 | B256::from_slice(input) 138 | } 139 | 140 | #[cfg(test)] 141 | mod test { 142 | use ethers::types::{transaction::eip2930::AccessListItem, H160, H256}; 143 | use revm::primitives::B256; 144 | 145 | use super::*; 146 | 147 | use std::str::FromStr; 148 | 149 | #[test] 150 | fn test_eu256_to_ru256() { 151 | let input = ethers::types::U256::from_str("0x1234").unwrap(); 152 | let derived: U256 = eu256_to_ru256(input).unwrap(); 153 | let expected: U256 = U256::try_from_be_slice(&hex_decode("0x1234").unwrap()).unwrap(); 154 | assert_eq!(derived, expected); 155 | } 156 | 157 | #[test] 158 | fn test_eu64_to_ru256() { 159 | let input = ethers::types::U64::from_str("0x1234").unwrap(); 160 | let derived: U256 = eu64_to_ru256(input); 161 | let expected: U256 = U256::try_from_be_slice(&hex_decode("0x1234").unwrap()).unwrap(); 162 | assert_eq!(derived, expected); 163 | } 164 | 165 | #[test] 166 | fn test_eu256_to_u64() { 167 | let input = ethers::types::U256::from_str("0x1234").unwrap(); 168 | let derived: u64 = eu256_to_u64(input); 169 | let expected: u64 = 4660u64; // 0x1234 170 | assert_eq!(derived, expected); 171 | } 172 | 173 | #[test] 174 | fn test_ru256_to_u64() { 175 | let input = U256::from_str("0x1234").unwrap(); 176 | let derived: u64 = ru256_to_u64(input); 177 | let expected: u64 = 4660u64; // 0x1234 178 | assert_eq!(derived, expected); 179 | } 180 | 181 | #[test] 182 | fn test_eh256_to_ru256() { 183 | let input = ethers::types::H256::from_str( 184 | "0x0000000000000000000000000000000000000000000000000000000000001234", 185 | ) 186 | .unwrap(); 187 | let derived: U256 = eh256_to_ru256(input); 188 | let expected: U256 = U256::try_from_be_slice(&hex_decode("0x1234").unwrap()).unwrap(); 189 | assert_eq!(derived, expected); 190 | } 191 | 192 | #[test] 193 | fn test_ru256_to_eu256() { 194 | let input = U256::from_str("0x1234").unwrap(); 195 | let derived = ru256_to_eu256(input); 196 | let expected = ethers::types::U256::from_str("0x1234").unwrap(); 197 | assert_eq!(derived, expected); 198 | } 199 | 200 | #[test] 201 | fn test_ru256_to_eh256() { 202 | let input = U256::from_str("0x1234").unwrap(); 203 | let derived = ru256_to_eh256(input); 204 | let expected = ethers::types::H256::from_str( 205 | "0x0000000000000000000000000000000000000000000000000000000000001234", 206 | ) 207 | .unwrap(); 208 | assert_eq!(derived, expected); 209 | } 210 | 211 | #[test] 212 | fn test_rb256_to_eh256() { 213 | let hash_string = "0x0000000000000000000000000000000000000000000000000000000000001234"; 214 | let input = B256::from_str(hash_string).unwrap(); 215 | let derived = rb256_to_eh256(input); 216 | let expected = ethers::types::H256::from_str(hash_string).unwrap(); 217 | assert_eq!(derived, expected); 218 | } 219 | 220 | #[test] 221 | fn test_rb160_to_eh160() { 222 | let hash_string = "0x0000000000000000000000000000000000001234"; 223 | let input = B160::from_str(hash_string).unwrap(); 224 | let derived = rb160_to_eh160(&input); 225 | let expected = ethers::types::H160::from_str(hash_string).unwrap(); 226 | assert_eq!(derived, expected); 227 | } 228 | 229 | #[test] 230 | fn test_access_list_e_to_r() { 231 | let address = "0x0000000000000000000000000000000000009876"; 232 | let hash = "0x0000000000000000000000000000000000000000000000000000000000001234"; 233 | let input: AccessList = AccessList(vec![AccessListItem { 234 | address: H160::from_str(address).unwrap(), 235 | storage_keys: vec![H256::from_str(hash).unwrap()], 236 | }]); 237 | 238 | let derived = access_list_e_to_r(input); 239 | let address = B160::from_str(address).unwrap(); 240 | let storage = U256::try_from_be_slice(&hex_decode(hash).unwrap()).unwrap(); 241 | let expected: RevmAccessList = vec![(address, vec![storage])]; 242 | assert_eq!(derived, expected); 243 | } 244 | 245 | #[test] 246 | fn test_ssz_u64_to_u64() { 247 | let expected = 123456789_u64; 248 | let bytes = expected.to_be_bytes(); 249 | let mut ssz = SszU64::default(); 250 | for byte in bytes { 251 | ssz.push(byte) 252 | } 253 | let derived = ssz_u64_to_u64(ssz).unwrap(); 254 | assert_eq!(expected, derived); 255 | } 256 | 257 | #[test] 258 | fn test_ssz_u256_to_ru256() { 259 | let expected = 260 | U256::from(0x0000000000000000000000000000000000000000000000000000000000001234); 261 | let bytes = expected.to_be_bytes_vec(); 262 | let ssz = SszU256::try_from(bytes).unwrap(); 263 | let derived = ssz_u256_to_ru256(ssz).unwrap(); 264 | assert_eq!(derived, expected); 265 | } 266 | } 267 | -------------------------------------------------------------------------------- /crates/verify/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "archors_verify" 3 | version = "0.1.0" 4 | edition = "2021" 5 | description = "Tool for verifying EIP-1186 proofs" 6 | 7 | [dependencies] 8 | ethers = "2.0.4" 9 | hex = "0.4.3" 10 | rlp = "0.5.2" 11 | rlp-derive = "0.1.0" 12 | serde = { version = "1.0.152", features = ["derive"] } 13 | serde_json = "1.0.94" 14 | thiserror = "1.0.40" 15 | -------------------------------------------------------------------------------- /crates/verify/README.md: -------------------------------------------------------------------------------- 1 | # archors-verify 2 | 3 | For verification of EIP-1186 proofs. 4 | 5 | ## Why 6 | 7 | When a proof is received, one must verify it. This crate does that. 8 | 9 | When a proof fails verification, pointing to the cause is useful. It does that 10 | too. That way, if you are verifying a pile of proofs, it will point to the 11 | accoount, storage, proof element and reason for the failure. 12 | 13 | ## Architecture 14 | 15 | Designed to be readable in order to communicate how these proofs work. 16 | Special care is made for the error variants, which hopefully explain what 17 | behaviour is normal in the proving process. 18 | 19 | Seeks to explain: 20 | - What proofs does an EIP-1186 proof contain? 21 | - What does a single proof contain? 22 | - What is a path and how is it followed? 23 | - What are branches, extension and leaves? 24 | - What are exclusion and inclusion proofs? 25 | -------------------------------------------------------------------------------- /crates/verify/data/test_proof_1.json: -------------------------------------------------------------------------------- 1 | { 2 | "address": "0xaa00000000000000000000000000000000000000", 3 | "accountProof": [ 4 | "0xf8718080808080a0a2bd2175aed7ed88ed854c914fab94115c092ffb3c3c2ef647b70b7e73e3345880a0457ae8d978cd387f5332f978f5653226588b6cc76a355fc5977cd4325ffcff78a0c4bdbdbb240f8343b7f84bc83d4b7426e803a914138792d1e369907be8098b2d8080808080808080", 5 | "0xf869a0335649db80be637d281db0cc5896b0ff9869d08379a80fdc38dd073bba633949b846f8440101a08afc95b7d18a226944b9c2070b6bda1c3a36afcc3730429d47579c94b9fe5850a0ce92c756baff35fa740c3557c1a971fd24d2d35b7c8e067880d50cd86bb0bc99" 6 | ], 7 | "balance": "0x1", 8 | "codeHash": "0xce92c756baff35fa740c3557c1a971fd24d2d35b7c8e067880d50cd86bb0bc99", 9 | "nonce": "0x1", 10 | "storageHash": "0x8afc95b7d18a226944b9c2070b6bda1c3a36afcc3730429d47579c94b9fe5850", 11 | "storageProof": [ 12 | { 13 | "key": "0x0000000000000000000000000000000000000000000000000000000000000001", 14 | "value": "0x0", 15 | "proof": [ 16 | "0xf871808080a0ce028e108cf5c832b0a9afd3ed101183857b3b9ddaea5670c4f09b62e4d38d05a0de3ecad66628a5743ed089e3a35ebeedc25a922fb0ac346304613403911c18e0a0128c5f7abac505794cda09bde44d143e4736b50b1c42d6807a989c10af51e8d18080808080808080808080" 17 | ] 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /crates/verify/data/test_proof_2.json: -------------------------------------------------------------------------------- 1 | { 2 | "address": "0x7ae1d57b58fa6411f32948314badd83583ee0e8c", 3 | "accountProof": [ 4 | "0xf90211a0f5f0fc4435d7d28ef25fdc46d7f84504474f96263c36379476ac6d209d7f7dd6a04f060c649eb912f7ede96ca232c87c0acf02e2dc80c0806427e20655b2906e99a0327a57686166773927604ddae15bb31ed0286a2539bf56fb0eec69dffa726123a058bec0e078cd8ba10b281e405dd940bdcd7b36753a14ee0e8f991501182d3b74a06aa7d258010b69fe48d966af25ec26a57d4a8324ce42c87fe402cc2f6716e54ba0fd1fa0d1e1e78f5314b6b7b1e9c1e007cb3d023234d548baf00528149c530638a05e642d9084d1ea11282050395cf7d82a09c4324bbc1f00c555c4a9e6e634c4cba0d570f24e17e3cf5f4a5a27bfa39f5f471ae5ff3a5f03ee50896d390882b54e90a02ae426a9259726af2befabeba92b04506c9964c8428393879d0e12b8c8503c8aa0139ec83890ab95a2514715a691bd46520969649efa6b8b7ddb7c3873ac8273eea0e0e879d951586a126e8272d84ecd356b2269cf22ed3f8904e5806ec157b2cb79a0995cd6e482065130366c0020c64133564b00bf3844935268836d55c74596520ea0a7ad33b003ff333acaffdab9190103f6b17d6df8c73650dcba83e0655d65ea2aa0143fe270c96ba9de62c6ec4ad59bed02bc0fdec37d80188aab56244a00b288f3a00d825cd07b3ed210d7fbf143ca25c2d90618d37b67f8a536039fb4b88573dd02a0c941e6c81045fd12d7d43aa90472f78c422af3e8465924e84df0e4e0dcd3bf4780", 5 | "0xf90211a079a82b6696991b13a61ab127d4523ee51d6c88b7f67baa15b919888fd0743874a0e0c3ce98340b234c15d1d6a76ea265918fc282b8b9819dcbab4ee818db9bb015a0af0621f6341cd95597cfc52be4e0dfe3eb1c40ecfce5ac4ed981e874d2570a9da024c943c2d82fa83e9239209ae37abbb5b13aa5f8ef09f72eaea241a5d6424a90a0fad6914434628f110718ac7d7d6ce4112120e99b1aa4bb5f510e08502ac32af9a0b5951ac7f226a5436fa0b74f33c4ad242872f609dc73030b401080b0e4cc5a44a0b340c634bd307ddb4f99e34142b1fbecb08bd99f1154e707913a6ede40c44df2a05d1005b244d5bdeb657a27e37ee2ff2dc1bee9fc9dadad50a6a8f9501c83b496a01ab7e7ccb8c2993ce512e3f7a461fd48b4c62bcb0ce7c4fa40a248687defcd59a09937e967971e9cffa91a40eded9be942e0412b253e2f0fb5d7cacf25b63489d4a0be53036f7da95bab787e2f1c89abe4841ca6dc403157850da3f83f97ce9552b7a08d7e9e6503f429df4e1548d12298135d6ad07638265211df658d0d899553d1eca0f8105f035b8c3ffcfa057eb47df72c2072610ae4c3d525d0671b773d24602fa8a06f6b1c196163614e2fae2bc7333c2d11c34160575ba13a8f64bc2c4ebfe395a8a02a21453acdf51ca55d1c1dbf9c2568448498736852f89fbbc039c180ae27ff24a00cf5ea162fa3b0456349a7d6ca441a81951918b31d5f080412e6431e6918495880", 6 | "0xf90211a00f76fc33e956622fd1fc755eb873656ba95f726e66c1787e2267b31cc5bbd985a0fc8e5340344c10ca160906740cb0c4b4ea35f4c38130522f31dd66df79f0ad33a04ce755b44e7dfabb0fc7e23c884547075b2762ab3ec57d980f20754cc3dbc0b5a02a7e16917f7e51585b2cfc6a80dcc01036808dbaa14e5be3a3d5c134320e416ba0d648ef21330219ea856ecd9bd9a340bb6dbabd739a3c4f105e31b75183682bd9a0f92b3ad626495fb5278abba274677b5fba6e4f1d5cbf9c54521eb8b5ad5ffd30a04ddd49d6fe0a02bb83956a733437bb55c32c328c3fa778fd6d18e31853fd84bea0b89536a39637ff432e44184f756986495db413d66be496dd16dfc28c4a578735a0838826ea67312fc2bdc845ead924567aeb50a0f31919778300a1a2059ccc1c50a0e2c5c11f7b20bef6921ddde677ce58c3e679ce0a333d5b85622122c2fa9ce9efa0b5dfcca5631b1647e76437ab29ae262572fb291a186e47c056af5d8bd036add5a0e745abaa72b0d9475228000d89e74e529f3163b6cceb14150c3626977ce64729a070d94864f49bf3f5fb032d134340e6db39a2876587ca4b5e4241cb32df5df7f9a0a68c086d773a76f34b9bbdd08d80821f3a0074068041d0459394b54b523d680fa023bc5f7917a06e1a0f94596b82a564860617868f65f7e22ca566f33f26abcd5da0b7da3fd1cd32bfb2bb70de85ffed2963332e3aca068b84ca0fcd4964bbec8bff80", 7 | "0xf90211a0b3571d33c9849a8a017ed8fb486804706bbe8c795aff37df2a92a9dbd94d9c92a0622e60877c5b303eb50646dadc1dafadf9b523081fe30a50fcbaab7f5540e8d7a0f2e1376ac90b852e021c79aea8f3e235e0d0a5a02d80244b384deb460de3dd18a03e8e7eecd7ec987487305831a9476050539cc9eedb2cdf24ffcf674237faf77ca0d94cc8a9059c99d9f408800c218ae9d47680618ca2f47b396a13752704f3e554a0d76e79a852761a285d5da6a7b88a714706c73ca21760bf04db3e66cc292af90ea08cbadba557c74bdf46e47bbe8b8f5877484b6a83586f304ce6735d66fc238418a037b7adfb405a40a4a1a062fe486e0fe6f9c385b777191c24c53a2e1245a6a2e3a0e0db07f82a97ee038ae756e5c7003b7484f05b4ecf329dd011e0f23b9906e554a097c10736f0ab6a624b7e307912cedbc378c393a77fda46699a41aa37e996ea8aa03421ac703b162881e21ce111a2824c2b68f9e334334a1aa11094820da41ac2dfa08211fa3ef76e077bf4e8b7936983d3cc9bbd4533d29bc27516bc9c7123a965d6a0ba8c0be28246d36e563731039e57711b204f008daf0272479e55fb1dadf34202a004de9138b9911cbc95d1017bc253ab816963dd354aa8ae6a127e2d89f7f86161a08920b6f94ae6e9cbaa29ee5f8ca52cf6962f89f9fefd7386b6663711ff7b5d69a01f38cbc784d3b9d3eeaffa7d8e42c6cc94ce79c7bb12f827782b5f64a1a6a93d80", 8 | "0xf90211a034a2552054411dc664ef8e597cc2b7b1f0974cf62d40193d8e5f35013e612c1ba0b0fe062fb1ad401668f135654921ff6542dd00b18e152ee3fcf57d776fe2c179a00669f4d3374106b875b9800580995a18de66cda98e95fb07e1e79f35b52abb34a0b59dd059c974bd8ac3a98409c7f9c0d5a54827d1fe2e20b6d1cb0f8ce311bebca073c0e972ee0ca8a2985198158ec115008061076c6618c131ad8fa79eafeb7c32a0c68741d417a821daa549ad3b2a605cd78d43f62b8220c1d79da056f85dcb9bfda07f2a02d7bd6669fc512e05033cfbd56be68c517ae415d0f9ae3190797c0e81c5a096a00b2ddeda48df3ef0b88738c14caccc6eb4d072c11d98e0e7222811f8a4e7a06f7ae0647462143a3205a6e0b2167d15745f9febc28941b98e0e9b2120313eaaa07f4ebb1f1ceb49405904de266c8f521b91ad2982febe023a0ff6824355d4f9d8a0f8ab56eb1e5d8b1c4628d6749fe8f680043d074a62ce415528139b93c399f357a0d11f90835323d8f0339bb03692e1c69551ec37e15cb49ddb6c176c07d308b9c8a079d1ca600945c11077fb25bd68440345819ee1fb63ce60754ab23a1dab4ca23aa08a7c385645d96f62f8e60bf66521bd745c20d44c7b2da901388997fb2934d26da0787ca5c9f3fb27e5f82c0bb0c6a8ccba62202ab0cb5160fc087e5f8648835e80a01e12586d6c58962ef1b1f634e5ab8ea559442383a79f9170273d975e17d53bea80", 9 | "0xf90211a083eee2cc3aaa0de966ed9448a80d32f1c150d0be5f5665845927bf88c0097c52a0549e70926e435d33f2a16b5c13db33185187809d542bf9f6c48963410780b80aa0fad5f4c4e918284d54aedae7101e511e6957ff0ea57004507e3ccc2b7b8fe147a0a8373ad1441bb75727dc34f4eb43f8b4de2d17e5065874624d8b378a25745d2fa05295d90b2749aa759b7d573824fe86199ceaebc57fa98c57d9b3c12606226f1ea0727cedf499df4c1162534a12317279b2d8f6f48541549481bcf0ba7cc24e7d55a0030a8f35c8683b9d45416ec4996c700bdb1577d18f9990d1a4a6bc9e4f3bcee5a0575b5d3bc59e476fd3794856d9938344399b0ceb7526291b6cd44aaaa7d6a902a0ec2ce6eb12fbc3218d01cb20fa03a9cd30f10fd46379fd3271980501f62e06e5a045d1db58b141321600837901cc09f356713c71c0b9def24698e8a78b13889488a0d44a51694b70df547bcfbd0363068bc908fc3a32663e268011607d0631e0a32ba07c69374023e1ea2728c7130c0ee2dcc462e0ab53a7d122c286f5c3a480ae395fa0fee728d489e337c36af5bb40887c9747a096eb87cea1233007b28bbe2367622fa09e6f561888dfdb234a0268bf8dae457b1c1a6ec90ca06c314c72ed043a75bcc0a0aa3a0cc29b027a19e5eb8c0361387d30af96dd84d7a9c64064f077e925f6b389a0c31765f105fca312ff576214f30a5654cc7c4fc4522e3b37b35494fb00e1d95580", 10 | "0xf90151a0bf5e7a6355d2aae16870034397bcb78fb7f3677302857c4e3f0f11b2ad183ddaa0441a130e5b3344a0c6d4e01e69cdd8c3d54c9427c22df1c21e823bd5238bcedc80a0de4a8735f0afe745a73341f09b2641b136c4c6ceb33a4c04f868b8c0ae0c572da0616b1953ab56f21db0e3e0a8f04422bbdce75bd530e049560426deb7548c9324a0df7498a408a3cb6f416a60eb97bc61cdd31f9f9c1e3d9f2e131c476cca1a64aaa0b4b838d595815f1af27bc520f9054bbe7b8f1ae901d58ceba455a93a02b38fe3a088c2648a34b76ec09c67666bf1b2ff917c97a960dbebd2c8d56ec2b89c5f5d7ba080f002d80dc9f4e682660964f02c4f70fdfb5aeeee5f5651fca75c06f810c37980a0f6d68b8a203434af63aefd6acbce4e627b80e03c11d9c64334d48655f842ee24a02991191455c868799650d6cd4009a21443c9ac2aebedb76d55d9a01811d59a9c8080808080", 11 | "0xf8669d33269ec9b8f075a4723d27c611ac1c52a464f3516b25e0105a0d1c2210b846f8440180a03836d7e3afb674e5180b7564e096f6f3e30308878a443fe59012ced093544b7fa02cfdfbdd943ec0153ed07b97f03eb765dc11cc79c6f750effcc2d126f93c4b31" 12 | ], 13 | "balance": "0x0", 14 | "codeHash": "0x2cfdfbdd943ec0153ed07b97f03eb765dc11cc79c6f750effcc2d126f93c4b31", 15 | "nonce": "0x1", 16 | "storageHash": "0x3836d7e3afb674e5180b7564e096f6f3e30308878a443fe59012ced093544b7f", 17 | "storageProof": [ 18 | { 19 | "key": "0x0000000000000000000000000000000000000000000000000000000000000000", 20 | "value": "0x0", 21 | "proof": [ 22 | "0xf90211a0d24e9242c2ef8b8a5c74b22915b80db1d6febd83c1399af920e73a3a3e6f5359a0d8beb5d8687b39d32148247dfcfbcda4bf1507de6bd9025417aa97b90283bbfba025cbad12ebebe6d79041b8953dcb9088558deff7ebc5140a1180ead12a181151a0f4168b84a0e5e7aec2c26cbbf91aea09404ae63444455b0626a8ca3fea498c08a0f2eadf4864a004cedfd1452c00e65dc8aceeb60517ae9a9161e4ba3d9c2ae179a0e466381320d7f1943a0f92ae3149c54488771a3deddc1ef21f88673a96caa41da0f7807e5c7a5cd50ac11c9d63b326f728e7c7779332b4c288f1886c2c32fce2f4a02b6bffd177a66f7be5db11253a9cd990b8e7bcc6f615d1f2721ecae417194354a03b72c03fd3bc8dc71b7ea901ebb667679efe300989a3a7d8e480926814d1f8b3a00dc01b0aa64272858833a060a11c9cc385f845db10c9869cdb9ac399edc13604a084adcb82e3466c9070e93de7f1112f2b454235e46bba3757a827aeb141ac5ceea0e1ee371cb987eec41ffcc11a3d78cce4a3db934365ff9385cb6d41fc828fcbe7a04a9f0723b676f36ce1ca7c96440640e2521ddb1d408af9e0e40196246e86bdb4a0f8d5b3099b7800c8a8abd073675cc94fe913cf4b7af3d3736b40a99d16a5a26ba01dec8ffccb928fecb7654c9493a854f15d87a5d76d46f28dc98a176bf9b75eb2a09024c7e1e47678b91b8f1b88fa3195c903e852fd3771dc3a43d2a407f6a03e5680", 23 | "0xf90211a003ce494fb4c43f4bfbed16a2b55fe0db8f01e3bbfc39f479f035846749c89b62a099c49a7bd65ba7cdcaf7c1de712cda41b518b5418f690af1e191161e966d8a45a099e3683f6c1f344c3233804f479228c0eade51feac55f42dbd1b99774135ed0da0ab357eeee2e0ad78880a51db599c3f8428deb6ada8213a4b8245c27f99605451a07627f39a4627e0d9c3f5cc7f36752b11e5b1b818375fe470142f0c665a80e07ca0d6f082034fef118757fb2a4bec21f1b338119d827deb869369651a5484049feba0005c4014d4bdc60e62537fc57df020239db798e6319e9b659a47f11f68934052a0078e8847f104b0e911d24d955a539603c4293f43f929ee4e1ba528c2d0401384a0becfc0b36b3e583f698fb01151e753a23964c120f37982ee32fade0278bc70f5a056df0ee78f0773bdcc17cd40154f6d489e8015e956f50b64c8acddc61e7bb68ba0e66031bdc7fec2efae7165fd81adcc6738868d197d34174c629437554aad02e6a0495467963f9bec77aab577ba575c2fd8a12d2097549c13b22aa13ce3b710d900a0826dae7bcdc5517c1a99fec02fb0e01163e95c0504f1028551ab0c4367892871a0d8625ca51acff9b30970aebab9585e10794f470b05463b621d8520349f99693ea0de8cae4fe9fcd780ecd9c58946923357678ddcebe7dc8493f38dd28f18c4307ca09b6aaa66550685763e9ce4e8d8e3fd42a85e3a7fae094738c969ba0e5899fb9380", 24 | "0xf90211a02f735a1444035c376b883498ed8cb6904fa2dd0a030f134d5a0df3d8eaca9623a07b63f0c18a46e3e5fec248bdbc861b4651df4aa821c6735f778f28eb997ad851a026c6d7a14629f89cbe9532f31aabfe2fb12fb739dc8cdfb60b5855c312ddce96a0a25dcfa9f3e6736b35ea14ff51b63656a15e1785c53c28f0b82309839ca838a8a03de0fe33add7f57ac122d28470f48d6ebb61a351a37ee5fca40ca923335a603aa0ad7273bd535661496207181ff58e7f44adbfbc062fc03d85da0bd2bffacb03c4a0d4e09a5170239e48be3140d4a4fa33e7d55ea0361a4e3a135b2d9edf45075d06a0ccb26df003eb092dee9b77909f815407abdbd3f5c3c6a5b968addb729a2b29fba0aa6f915141fd795671ce8485027faccc81c0a9148f6806409ec1c636dd8b3302a0aaa6a639c30e53435d1fce25a3564bde89409cbcc12cffb090c167e88616a8f6a0ef6f1981e9786e96ec578a42646c04cc631ae848b6315c1271e7b4921a09b4a3a0705f0745083c9f87c3c9c23877e01efaf787e078f802a95b3dbe860d673174bfa0b5d83b6aab765759c1b39c85ff2ee0eb4779264d42b7c9fc0847995e8ec37ed3a0d3d833c4d5ab4d1d8832c88427f4940fbe6fddad6f0dc478a8df52212804f5ffa0f694df9afb92fe0c360c0d1d765743a249fec5858ce7253e526b0db9c4b4d20ca09755ac002364839992a491d6a24826dc4a2feb8eb5737763f0ed544f19dfa3ed80", 25 | "0xf871a0e4050339952e88a1d403d7078148abf3af96d8a2fdb175cf12244b721962fe4280808080808080a0cd71d6a12adb2cef5dba915f9cd9490173c5db30ea44a1aee026d8e0ea2fd27f80a059267a0b25d180d3cae2274c50da7b7da0ddddfd435671181e9dc2f7ba8cca7f808080808080" 26 | ] 27 | } 28 | ] 29 | } -------------------------------------------------------------------------------- /crates/verify/src/eip1186.rs: -------------------------------------------------------------------------------- 1 | //! Verifies an EIP-1186 style proof 2 | 3 | use ethers::{ 4 | types::{EIP1186ProofResponse, StorageProof, H256, U256, U64}, 5 | utils::keccak256, 6 | }; 7 | 8 | use rlp_derive::{RlpDecodable, RlpEncodable}; 9 | use serde::Deserialize; 10 | use thiserror::Error; 11 | 12 | use crate::{ 13 | proof::{ProofError, SingleProofPath, Verified}, 14 | utils::hex_encode, 15 | }; 16 | 17 | #[derive(Default, Debug, Clone, PartialEq, Eq, Deserialize, RlpEncodable, RlpDecodable)] 18 | pub struct Account { 19 | pub nonce: U64, 20 | pub balance: U256, 21 | pub storage_hash: H256, 22 | pub code_hash: H256, 23 | } 24 | 25 | impl Account { 26 | fn is_empty(&self) -> bool { 27 | let empty = Account::default(); 28 | self.eq(&empty) 29 | } 30 | } 31 | 32 | #[derive(Debug, Error)] 33 | pub enum VerifyProofError { 34 | #[error("Proof not valid for account {account}, AccountError {source} ")] 35 | AccountError { 36 | source: AccountError, 37 | account: String, 38 | }, 39 | #[error( 40 | "Proof not valid for account {account} storage key {storage_key}, StorageError {source}" 41 | )] 42 | StorageError { 43 | source: StorageError, 44 | account: String, 45 | storage_key: String, 46 | }, 47 | #[error("Proof is empty")] 48 | EmptyProof, 49 | } 50 | 51 | #[derive(Debug, Error)] 52 | pub enum AccountError { 53 | #[error("ProofError {0}")] 54 | ProofError(#[from] ProofError), 55 | #[error("Proof is empty")] 56 | EmptyProof, 57 | #[error("A valid exclusion proof exists, but the claimed account is not empty")] 58 | ExclusionProofForNonEmptyAccount, 59 | #[error("Unexpected inclusion proof for an empty account, expected exclusion proof")] 60 | InclusionProofForEmptyAccount, 61 | } 62 | 63 | #[derive(Debug, Error)] 64 | pub enum StorageError { 65 | #[error("ProofError {0}")] 66 | ProofError(#[from] ProofError), 67 | #[error("A valid exclusion proof exists, but the claimed storage is not empty")] 68 | ExclusionProofForNonZeroValue, 69 | #[error("Unexpected inclusion proof for a storage value of zero, expected exclusion proof")] 70 | InclusionProofForZeroValue, 71 | } 72 | 73 | /// Verifies a single account proof with respect to a state roof. The 74 | /// proof is of the form returned by eth_getProof. 75 | pub fn verify_proof( 76 | block_state_root: &[u8], 77 | proof: &EIP1186ProofResponse, 78 | ) -> Result<(), VerifyProofError> { 79 | // Account 80 | verify_account_component(block_state_root, proof).map_err(|source| { 81 | VerifyProofError::AccountError { 82 | source, 83 | account: hex_encode(proof.address), 84 | } 85 | })?; 86 | 87 | // Storage proofs for this account 88 | for storage_proof in &proof.storage_proof { 89 | verify_account_storage_component(&proof.storage_hash.0, storage_proof.clone()).map_err( 90 | |source| VerifyProofError::StorageError { 91 | source, 92 | account: hex_encode(proof.address), 93 | storage_key: hex_encode(storage_proof.key), 94 | }, 95 | )?; 96 | } 97 | Ok(()) 98 | } 99 | 100 | pub fn verify_account_component( 101 | block_state_root: &[u8], 102 | proof: &EIP1186ProofResponse, 103 | ) -> Result<(), AccountError> { 104 | let claimed_account = Account { 105 | nonce: proof.nonce, 106 | balance: proof.balance, 107 | storage_hash: proof.storage_hash, 108 | code_hash: proof.code_hash, 109 | }; 110 | 111 | let account_prover = SingleProofPath { 112 | proof: proof.account_proof.clone(), 113 | root: H256::from_slice(block_state_root).0, 114 | path: keccak256(proof.address.as_bytes()), 115 | claimed_value: rlp::encode(&claimed_account).to_vec(), 116 | }; 117 | 118 | match account_prover.verify()? { 119 | Verified::Inclusion => { 120 | if claimed_account == Account::default() { 121 | return Err(AccountError::InclusionProofForEmptyAccount); 122 | } 123 | } 124 | Verified::Exclusion => match claimed_account.is_empty() { 125 | true => {} 126 | false => return Err(AccountError::ExclusionProofForNonEmptyAccount), 127 | }, 128 | } 129 | Ok(()) 130 | } 131 | 132 | /// Verfies a single storage proof with respect to a known storage hash. 133 | fn verify_account_storage_component( 134 | storage_hash: &[u8; 32], 135 | storage_proof: StorageProof, 136 | ) -> Result<(), StorageError> { 137 | let rlp_value = rlp::encode(&storage_proof.value).to_vec(); 138 | 139 | // TODO: See yellow paper (205). Account for cases where entire node is <32 bytes. 140 | 141 | let storage_prover = SingleProofPath { 142 | proof: storage_proof.proof, 143 | root: *storage_hash, 144 | path: keccak256(storage_proof.key), 145 | claimed_value: rlp_value, 146 | }; 147 | 148 | match storage_prover.verify()? { 149 | Verified::Inclusion => { 150 | if storage_proof.value == U256::from(0) { 151 | return Err(StorageError::InclusionProofForZeroValue); 152 | } 153 | } 154 | Verified::Exclusion => match storage_proof.value.is_zero() { 155 | true => {} 156 | false => return Err(StorageError::ExclusionProofForNonZeroValue), 157 | }, 158 | } 159 | Ok(()) 160 | } 161 | 162 | #[cfg(test)] 163 | mod test { 164 | use crate::utils::hex_decode; 165 | 166 | use super::*; 167 | use std::{fs::File, io::BufReader}; 168 | 169 | fn load_proof(path: &str) -> EIP1186ProofResponse { 170 | let file = File::open(path).expect("no proof found"); 171 | let reader = BufReader::new(&file); 172 | serde_json::from_reader(reader).expect("could not parse proof") 173 | } 174 | 175 | /// data src: 176 | /// https://github.com/ethereum/execution-apis/blob/main/tests/eth_getProof/get-account-proof-with-storage.io 177 | /// ```json 178 | /// {"jsonrpc":"2.0","id":1,"method":"eth_getProof","params":["0xaa00000000000000000000000000000000000000",["0x01"],"0x3"]} 179 | /// ``` 180 | #[test] 181 | fn test_verify_inclusion_proof_of_storage_value_zero() { 182 | let account_proof = load_proof("data/test_proof_1.json"); 183 | let state_root = 184 | hex_decode("0x61effbbcca94f0d3e02e5bd22e986ad57142acabf0cb3d129a6ad8d0f8752e94") 185 | .unwrap(); 186 | verify_proof(&state_root, &account_proof).expect("could not verify proof"); 187 | } 188 | 189 | /// data src: https://github.com/gakonst/ethers-rs/blob/master/ethers-core/testdata/proof.json 190 | #[test] 191 | fn test_verify_exclusion_proof_for_storage_key_zero() { 192 | let account_proof = load_proof("data/test_proof_2.json"); 193 | let state_root = 194 | hex_decode("0x57e6e864257daf9d96aaca31edd0cfe4e3892f09061e727c57ab56197dd59287") 195 | .unwrap(); 196 | verify_proof(&state_root, &account_proof).expect("could not verify proof"); 197 | } 198 | 199 | /// data src: block 17190873 200 | #[test] 201 | fn test_verify_inclusion_proof_for_nonzero_storage_value() { 202 | let account_proof = load_proof("data/test_proof_3.json"); 203 | let state_root = 204 | hex_decode("0x38e5e1dd67f7873cd8cfff08685a30734c18d0075318e9fca9ed64cc28a597da") 205 | .unwrap(); 206 | verify_proof(&state_root, &account_proof).expect("could not verify proof"); 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /crates/verify/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod eip1186; 2 | pub mod node; 3 | pub mod path; 4 | pub mod proof; 5 | pub mod utils; 6 | -------------------------------------------------------------------------------- /crates/verify/src/utils.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | array::TryFromSliceError, 3 | io::{self}, 4 | }; 5 | 6 | use hex::FromHexError; 7 | use thiserror::Error; 8 | 9 | #[derive(Debug, Error)] 10 | pub enum UtilsError { 11 | #[error("IO error {0}")] 12 | IoError(#[from] io::Error), 13 | #[error("Hex utils error {0}")] 14 | HexUtils(#[from] FromHexError), 15 | #[error("TryFromSlice utils error {0}")] 16 | TryFromSlice(#[from] TryFromSliceError), 17 | } 18 | 19 | /// Converts bytes to 0x-prefixed hex string. 20 | pub fn hex_encode>(bytes: T) -> String { 21 | format!("0x{}", hex::encode(bytes)) 22 | } 23 | 24 | /// Converts 0x-prefixed hex string to bytes. 25 | pub fn hex_decode>(string: T) -> Result, UtilsError> { 26 | let s = string.as_ref().trim_start_matches("0x"); 27 | Ok(hex::decode(s)?) 28 | } 29 | -------------------------------------------------------------------------------- /data/blocks/17190873/block_accessed_state_deduplicated.snappy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perama-v/archors/14a2349d4d22f05d46761968e0c4f4331f79513f/data/blocks/17190873/block_accessed_state_deduplicated.snappy -------------------------------------------------------------------------------- /data/blocks/17190873/blockhash_opcode_use.json: -------------------------------------------------------------------------------- 1 | { 2 | "blockhash_accesses": [ 3 | { 4 | "block_number": "0x1064fd8", 5 | "block_hash": "0x573471736fea190c6fd49db2c40add79c9904ed92eea6b68f5f2490ff2ae7939" 6 | } 7 | ] 8 | } -------------------------------------------------------------------------------- /data/blocks/17190873/prior_block_state_proofs.snappy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perama-v/archors/14a2349d4d22f05d46761968e0c4f4331f79513f/data/blocks/17190873/prior_block_state_proofs.snappy -------------------------------------------------------------------------------- /data/blocks/17190873/prior_block_transferrable_state_proofs.ssz_snappy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perama-v/archors/14a2349d4d22f05d46761968e0c4f4331f79513f/data/blocks/17190873/prior_block_transferrable_state_proofs.ssz_snappy -------------------------------------------------------------------------------- /data/blocks/17193183/block_accessed_state_deduplicated.snappy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perama-v/archors/14a2349d4d22f05d46761968e0c4f4331f79513f/data/blocks/17193183/block_accessed_state_deduplicated.snappy -------------------------------------------------------------------------------- /data/blocks/17193183/block_state_proofs.snappy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perama-v/archors/14a2349d4d22f05d46761968e0c4f4331f79513f/data/blocks/17193183/block_state_proofs.snappy -------------------------------------------------------------------------------- /data/blocks/17193183/prior_block_state_proofs.snappy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perama-v/archors/14a2349d4d22f05d46761968e0c4f4331f79513f/data/blocks/17193183/prior_block_state_proofs.snappy -------------------------------------------------------------------------------- /data/blocks/17193183/prior_block_transferrable_state_proofs.ssz_snappy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perama-v/archors/14a2349d4d22f05d46761968e0c4f4331f79513f/data/blocks/17193183/prior_block_transferrable_state_proofs.ssz_snappy -------------------------------------------------------------------------------- /data/blocks/17193270/block_accessed_state_deduplicated.snappy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perama-v/archors/14a2349d4d22f05d46761968e0c4f4331f79513f/data/blocks/17193270/block_accessed_state_deduplicated.snappy -------------------------------------------------------------------------------- /data/blocks/17193270/block_state_proofs.snappy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perama-v/archors/14a2349d4d22f05d46761968e0c4f4331f79513f/data/blocks/17193270/block_state_proofs.snappy -------------------------------------------------------------------------------- /data/blocks/17193270/prior_block_state_proofs.snappy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perama-v/archors/14a2349d4d22f05d46761968e0c4f4331f79513f/data/blocks/17193270/prior_block_state_proofs.snappy -------------------------------------------------------------------------------- /data/blocks/17193270/prior_block_transferrable_state_proofs.ssz_snappy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perama-v/archors/14a2349d4d22f05d46761968e0c4f4331f79513f/data/blocks/17193270/prior_block_transferrable_state_proofs.ssz_snappy -------------------------------------------------------------------------------- /data/blocks/17640079/block_accessed_state_deduplicated.snappy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perama-v/archors/14a2349d4d22f05d46761968e0c4f4331f79513f/data/blocks/17640079/block_accessed_state_deduplicated.snappy -------------------------------------------------------------------------------- /data/blocks/17640079/blockhash_opcode_use.json: -------------------------------------------------------------------------------- 1 | { 2 | "blockhash_accesses": [] 3 | } 4 | -------------------------------------------------------------------------------- /data/blocks/17640079/prior_block_state_proofs.snappy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perama-v/archors/14a2349d4d22f05d46761968e0c4f4331f79513f/data/blocks/17640079/prior_block_state_proofs.snappy -------------------------------------------------------------------------------- /data/blocks/17640079/prior_block_transferrable_state_proofs.ssz_snappy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perama-v/archors/14a2349d4d22f05d46761968e0c4f4331f79513f/data/blocks/17640079/prior_block_transferrable_state_proofs.ssz_snappy -------------------------------------------------------------------------------- /data/blocks/17683184/prior_block_transferrable_state_proofs.ssz_snappy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perama-v/archors/14a2349d4d22f05d46761968e0c4f4331f79513f/data/blocks/17683184/prior_block_transferrable_state_proofs.ssz_snappy -------------------------------------------------------------------------------- /examples/00_cache_get_block_by_number.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use archors_inventory::cache::store_block_with_transactions; 3 | 4 | /// Request and store a block for later use. 5 | #[tokio::main] 6 | async fn main() -> Result<()> { 7 | store_block_with_transactions("http://127.0.0.1:8545", 17190873).await?; 8 | Ok(()) 9 | } 10 | -------------------------------------------------------------------------------- /examples/01_cache_trace_block.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use archors_inventory::cache::{store_block_prestate_tracer, store_blockhash_opcode_reads}; 3 | 4 | /// Calls an archive node eth_traceBlock twice and caches the tracing results. 5 | /// 6 | /// The tracing is performed twice because two types of information are needed: 7 | /// - state accesses (prestateTracer), stored entirely. 8 | /// - BLOCKHASH opcode accesses (default tracer), only store blockhashes accessed. 9 | #[tokio::main] 10 | async fn main() -> Result<()> { 11 | const NODE: &str = "http://127.0.0.1:8545"; 12 | 13 | const BLOCK_NUMBER: u64 = 17170873; 14 | 15 | store_block_prestate_tracer(NODE, BLOCK_NUMBER).await?; 16 | store_blockhash_opcode_reads(NODE, BLOCK_NUMBER).await?; 17 | 18 | Ok(()) 19 | } 20 | -------------------------------------------------------------------------------- /examples/02_deduplicate_state_accesses.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use archors_inventory::cache::store_deduplicated_state; 3 | 4 | /// Uses a cached block prestate and groups account state data when it is accessed 5 | /// in more than one transaction during a block. 6 | fn main() -> Result<()> { 7 | // For example, deduplication reduces state data for block 17190873 from 13MB to 4MB. 8 | store_deduplicated_state(17190873)?; 9 | // After deduplication there is still room for compression as data is represented 10 | // multiple times still. 11 | Ok(()) 12 | } 13 | -------------------------------------------------------------------------------- /examples/03_compress_state_accesses.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use archors_inventory::cache::compress_deduplicated_state; 3 | 4 | /// Uses a cached deduplicated block prestate, compress the data for reduced 5 | /// disk use. 6 | fn main() -> Result<()> { 7 | // After deduplication there is still room for compression. 8 | // In block 17190873, one contract is repeated 27 times. 9 | // Representing state as .snappy can improve the footprint. 10 | compress_deduplicated_state(17190873)?; 11 | 12 | Ok(()) 13 | } 14 | -------------------------------------------------------------------------------- /examples/04_get_proofs.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | 3 | use anyhow::Result; 4 | use archors_inventory::cache::store_state_proofs; 5 | 6 | /// Uses cached account and storage keys and gets a proof with respect 7 | /// to a block. 8 | #[tokio::main] 9 | async fn main() -> Result<()> { 10 | let url = env::var("GET_PROOF_NODE").expect("Environment variable GET_PROOF_NODE not found"); 11 | 12 | store_state_proofs(&url, 17190873).await?; 13 | 14 | Ok(()) 15 | } 16 | -------------------------------------------------------------------------------- /examples/05_compress_proofs.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use archors_inventory::cache::create_transferrable_proof; 3 | 4 | /// Uses a cached block accessed state proof and either: 5 | /// - compresses the file. 6 | /// - creates a file with a transferrable ssz format. 7 | fn main() -> Result<()> { 8 | // A block state proof has many merkle tree nodes common between individual 9 | // account proofs. These can be compressed. 10 | 11 | // In block 17190873, one account proof node is repeated 162 times. 12 | // Representing state as .snappy can improve the footprint. 13 | // compress_proofs(17190873)?; 14 | 15 | // Package block state proof into a ssz format with minimal duplication of 16 | // data, optimised for transfer to a peer. 17 | create_transferrable_proof(17190873)?; 18 | 19 | Ok(()) 20 | } 21 | -------------------------------------------------------------------------------- /examples/06_measure_inter_proof_overlap.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use archors_inventory::overlap::measure_proof_overlap; 3 | 4 | /// Compares cached transferrable block proofs and quantifies the degree 5 | /// of data overlap (contracts, nodes). This represents data that a node 6 | /// would not have to duplicate on disk. 7 | fn main() -> Result<()> { 8 | let data_saved = measure_proof_overlap(vec![17190873, 17193183 /*, 17193270*/])?; 9 | println!("{data_saved}"); 10 | 11 | Ok(()) 12 | } 13 | -------------------------------------------------------------------------------- /examples/07_stats.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use archors_inventory::{ 3 | cache::{get_block_from_cache, get_required_state_from_cache}, 4 | overlap::measure_proof_overlap, 5 | utils::compress, 6 | }; 7 | 8 | /// Request and store a block for later use. 9 | #[tokio::main] 10 | async fn main() -> Result<()> { 11 | let blocks = vec![ 12 | 17370975, 17370925, 17370875, 17370825, 17370775, 17370725, 17370675, 17370625, 17370575, 13 | 17370525, 17370475, 17370425, 17370375, 17370325, 17370275, 17370225, 17370175, 17370125, 14 | 17370075, 17370025, 15 | ]; 16 | let blocks: Vec = blocks.into_iter().rev().collect(); 17 | println!("|block number|block gas|block .ssz_snappy p2p wire|block wire per gas|block .ssz disk| block disk per gas|block count|cumulative sum duplicate discardable data|percentage disk saved"); 18 | println!("|-|-|-|-|-|-|-|-|-|"); 19 | let mut total_ssz_disk_kb = 0; 20 | let mut total_kgas = 0; 21 | let mut total_snappy_kb_per_mgas = 0; 22 | let mut total_data_saved_kb = 0; 23 | for i in 0..20 { 24 | let block_num = blocks[i]; 25 | // Get gas 26 | let block = get_block_from_cache(block_num)?; 27 | let kgas = (block.gas_used / 1000).as_usize(); 28 | total_kgas += kgas; 29 | 30 | let proof = get_required_state_from_cache(block_num)?; 31 | 32 | // Get disk size ssz 33 | let ssz_bytes = proof.to_ssz_bytes()?; 34 | let ssz_size_kb = ssz_bytes.len() / 1000; 35 | total_ssz_disk_kb += ssz_size_kb; 36 | let ssz_kb_per_mgas = 1000 * ssz_size_kb / kgas; 37 | 38 | // Get wire size ssz_snappy 39 | let snappy_size = compress(ssz_bytes)?.len(); 40 | let snappy_size_kb = snappy_size / 1000; 41 | let snappy_kb_per_mgas = 1000 * snappy_size_kb / kgas; 42 | total_snappy_kb_per_mgas += snappy_kb_per_mgas; 43 | 44 | total_data_saved_kb = 45 | measure_proof_overlap(blocks[..=i].to_owned())?.total_savings() / 1000; 46 | let percentage_saved = 100 * total_data_saved_kb / total_ssz_disk_kb; 47 | let count = i + 1; 48 | 49 | let mgas = kgas / 1000; 50 | println!("|{block_num}|{mgas} Mgas|{snappy_size_kb} kB|{snappy_kb_per_mgas} KB/Mgas|{ssz_size_kb} KB|{ssz_kb_per_mgas} KB/Mgas|{count}|{total_data_saved_kb} kB|{percentage_saved}%|"); 51 | } 52 | let final_disk = total_ssz_disk_kb - total_data_saved_kb; 53 | let average_disk_kb_per_mgas = final_disk / (total_kgas / 1000); 54 | println!("\nAverage disk (duplicate data excluded): {average_disk_kb_per_mgas} KB/Mgas"); 55 | 56 | let average_wire_kb_per_mgas = total_snappy_kb_per_mgas / blocks.len(); 57 | println!("Average wire: {average_wire_kb_per_mgas} KB/Mgas"); 58 | Ok(()) 59 | } 60 | -------------------------------------------------------------------------------- /examples/08_verify_proofs.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use archors_inventory::{cache::get_proofs_from_cache, types::BlockProofs, utils::hex_decode}; 3 | use archors_verify::eip1186::verify_proof; 4 | 5 | /// Uses cached accessed-state proofs and verifies them. 6 | fn main() -> Result<()> { 7 | // Load a proofs for a block from cache. 8 | let root_17190873 = "0x38e5e1dd67f7873cd8cfff08685a30734c18d0075318e9fca9ed64cc28a597da"; 9 | let root_17193270 = "0xd4a8ad280d35fb08d20cffc275e9295db83b77366c2f75050bf6e61d1ef303bd"; 10 | let root_17193183 = "0xeb7a68f112989f0584f91e09d7db1181cd35f6498abc41689d5ed68c96a3666e"; 11 | 12 | prove_block_state(root_17190873, &get_proofs_from_cache(17190873)?)?; 13 | prove_block_state(root_17193270, &get_proofs_from_cache(17193270)?)?; 14 | prove_block_state(root_17193183, &get_proofs_from_cache(17193183)?)?; 15 | 16 | Ok(()) 17 | } 18 | 19 | /// Verifies every EIP-1186 proof within a BlockProofs collection. 20 | /// 21 | /// A block has proofs for many accounts (each with many storage proofs). Each 22 | /// is verified separately. 23 | fn prove_block_state(state_root: &str, block_state_proofs: &BlockProofs) -> Result<()> { 24 | let root = hex_decode(state_root)?; 25 | for proof in block_state_proofs.proofs.values() { 26 | verify_proof(&root, proof)?; 27 | } 28 | Ok(()) 29 | } 30 | -------------------------------------------------------------------------------- /examples/09_cache_required_state.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use archors_inventory::cache::{ 3 | get_block_from_cache, get_required_state_from_cache, store_block_with_transactions, 4 | store_required_state, 5 | }; 6 | use archors_tracer::trace::{BlockExecutor, PostExecutionProof}; 7 | 8 | /// Create, cache and then use the RequiredBlockState data type. 9 | /// 10 | /// Calls an archive node and gets all information that is required to trace a block locally. 11 | /// Discards intermediate data. Resulting RequiredBlockState data type can be sent to a 12 | /// peer who can use it to trustlessly trace an historical Ethereum block. 13 | /// 14 | /// Involves: 15 | /// - debug_traceBlock for state accesses 16 | /// - debug_traceBlock for blockhash use 17 | /// - eth_getProof for proof of historical state 18 | #[tokio::main] 19 | async fn main() -> Result<()> { 20 | // Create and cache RequiredBlockState 21 | const NODE: &str = "http://127.0.0.1:8545"; 22 | let proof_node = 23 | std::env::var("GET_PROOF_NODE").expect("Environment variable GET_PROOF_NODE not found"); 24 | const BLOCK_NUMBER: u64 = 17190873; 25 | 26 | store_block_with_transactions(&NODE, BLOCK_NUMBER).await?; 27 | store_required_state(&NODE, &proof_node, BLOCK_NUMBER).await?; 28 | 29 | // Use the cached RequiredBlockState 30 | let block = get_block_from_cache(BLOCK_NUMBER)?; 31 | let state = get_required_state_from_cache(BLOCK_NUMBER)?; 32 | let executor = BlockExecutor::load(block, state, PostExecutionProof::Ignore)?; 33 | 34 | // Either trace the full block or a single transaction of interest. 35 | executor.trace_transaction(13)?; 36 | //executor.trace_block()?; 37 | Ok(()) 38 | } 39 | -------------------------------------------------------------------------------- /examples/10_use_proof_to_trace.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use archors_inventory::cache::{ 3 | get_block_from_cache, get_blockhashes_from_cache, get_contracts_from_cache, 4 | get_proofs_from_cache, get_required_state_from_cache, get_node_oracle_from_cache, 5 | }; 6 | use archors_multiproof::{EIP1186MultiProof, StateForEvm}; 7 | use archors_tracer::{ 8 | state::BlockProofsBasic, 9 | trace::{BlockExecutor, PostExecutionProof}, 10 | }; 11 | 12 | /// Consume one block state proof. 13 | /// 14 | /// Shows how a transferrable RequiredBlockState (spec compliant) data structure is 15 | /// used to trace a block. 16 | /// 17 | /// ## Example 18 | /// Either trace the full block or a single transaction of interest. 19 | /// Notable transactions for block: 17190873 20 | /// - 2, 21 | /// - 8: storage update via access list + CALLDATALOAD (account 0x0b09dea16768f0799065c475be02919503cb2a35, key 0x495035048c903d5331ae820b52f7c4dc5ce81ee403640178e77c00a916ba54ab) 22 | /// - 14: Failed swap 23 | /// - 28: Failed contract execution 24 | /// - 37: Failed contract execution 25 | /// - 95: Coinbase using multiple CALL to send ether to EOAs. 26 | /// - 185: CREATEs 5 contracts 27 | /// - 196, 204, 28 | /// - 205 simple transfer (final tx) 29 | fn main() -> Result<()> { 30 | env_logger::init(); 31 | let block_number = 17190873; 32 | // Get block to execute (eth_getBlockByNumber). 33 | let block = get_block_from_cache(block_number)?; 34 | let form = StateDataForm::Basic; 35 | 36 | match form { 37 | StateDataForm::Basic => { 38 | let state = BlockProofsBasic { 39 | proofs: get_proofs_from_cache(block_number)?.proofs, 40 | code: get_contracts_from_cache(block_number)?, 41 | block_hashes: get_blockhashes_from_cache(block_number)?.to_hashmap(), 42 | }; 43 | let executor = BlockExecutor::load(block, state, PostExecutionProof::Ignore)?; 44 | re_execute_block(executor)?; 45 | } 46 | StateDataForm::MultiProof => { 47 | let proofs = get_proofs_from_cache(block_number)? 48 | .proofs 49 | .into_values() 50 | .collect(); 51 | let code = get_contracts_from_cache(block_number)?; 52 | let block_hashes = get_blockhashes_from_cache(block_number)?.to_hashmap(); 53 | let node_oracle = get_node_oracle_from_cache(block_number)?; 54 | 55 | let state = EIP1186MultiProof::from_separate(proofs, code, block_hashes, node_oracle)?; 56 | let executor = BlockExecutor::load(block, state, PostExecutionProof::UpdateAndIgnore)?; 57 | re_execute_block(executor)?; 58 | } 59 | StateDataForm::SpecCompliant => { 60 | // Get state proofs (from peer / disk). 61 | let state = get_required_state_from_cache(block_number)?; 62 | let executor = BlockExecutor::load(block, state, PostExecutionProof::Ignore)?; 63 | re_execute_block(executor)?; 64 | } 65 | } 66 | Ok(()) 67 | } 68 | 69 | fn re_execute_block(executor: BlockExecutor) -> Result<()> { 70 | //let post_state = executor.trace_block()?; 71 | let post_state = executor.trace_transaction(8)?; 72 | Ok(()) 73 | } 74 | 75 | /// The format of the state data (proofs, blockhashes) that will be fed to the EVM. 76 | /// 77 | /// The library has different forms for prototyping. 78 | #[allow(dead_code)] 79 | enum StateDataForm { 80 | /// Simplest prototype, data is aggregated naively which involves some duplication of 81 | /// internal trie nodes. 82 | Basic, 83 | /// Data is aggregated as a multiproof for deduplication of internal trie nodes and easier 84 | /// computation of post-block root. 85 | MultiProof, 86 | /// Spec-compliant `RequiredBlockState` data structure optimised for peer to peer transfer. 87 | SpecCompliant, 88 | } 89 | -------------------------------------------------------------------------------- /spec/interpreted_trace.md: -------------------------------------------------------------------------------- 1 | ## InterpretedTrace specification 2 | 3 | Details for a relatable data format that can be produced from an EIP-3155 trace. 4 | 5 | > Note: This is a draft to test how useful a transaction trace can be made without ancillary data. 6 | 7 | This data structure may be used to quickly evaluate "what a transaction is doing" 8 | 9 | ## Abstract 10 | 11 | A specification for a data format that summarises what a transaction is doing. The 12 | data is produced by a pure function with an EIP-3155 trace as an input. 13 | 14 | ## Motivation 15 | 16 | Transaction logs show select important data, but exclude many details. Transaction 17 | traces contain all data, but include many meaningless details. 18 | 19 | This format seeks to filter important information from a transaction trace. That is, 20 | include things like CALL, and the context surrounding the call and exclude things like ADD. 21 | 22 | ## Table of Contents 23 | 24 | 25 | ## Overview 26 | 27 | The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", 28 | "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted 29 | as described in RFC 2119 and RFC 8174. 30 | 31 | ### General Structure 32 | 33 | TODO -------------------------------------------------------------------------------- /spec/required_block_state_format.md: -------------------------------------------------------------------------------- 1 | ## RequiredBlockState specification 2 | 3 | Details for a data format that contains state required to 4 | trace a single Ethereum block. 5 | 6 | > Note: This is a draft to test a format for distributed archival nodes. 7 | 8 | This data structure may be suitable as a sub-protocol in the Portal Network, 9 | see the accompanying sub-protocol spec [./spec/required_block_state_subprotocol.md](./required_block_state_subprotocol.md) 10 | ## Abstract 11 | 12 | A specification for peer-to-peer distributable data that can enables trustless 13 | tracing of an Ethereum block. 14 | 15 | ## Motivation 16 | 17 | State is rooted in the header. A multiproof for all state required for all 18 | transactions in one block enables is sufficient to trace any historical block. 19 | 20 | In addition to the proof, BLOCKHASH opcode reads are also included. 21 | 22 | Together, anyone with an ability to verify that a historical block header is canonical 23 | can trustlessly trace a block without posession of an archive node. 24 | 25 | The format of the data is deterministic, so that two peers creating the same 26 | data will produce identical structures. 27 | 28 | ## Table of Contents 29 | 30 | 31 | ## Overview 32 | 33 | The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", 34 | "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted 35 | as described in RFC 2119 and RFC 8174. 36 | 37 | ### General Structure 38 | 39 | 40 | ### Notation 41 | Code snippets appearing in `this style` are to be interpreted as Python 3 psuedocode. The 42 | style of the document is intended to be readable by those familiar with the 43 | Ethereum [consensus](#ethereum-consensus-specification) and 44 | [ssz](#ssz-spec) specifications. Part of this 45 | rationale is that SSZ serialization is used in this index to benefit from ecosystem tooling. 46 | No doctesting is applied and functions are not likely to be executable. 47 | 48 | Where a list/vector is said to be sorted, it indicates that the elements are ordered 49 | lexicographically when in hexadecimal representation (e.g., `[0x12, 0x3e, 0xe3]`) prior 50 | to conversion to ssz format. For elements that are containers, the ordering is determined by 51 | the first element in the container. 52 | 53 | ### Endianness 54 | 55 | Big endian form is used as most data relates to the Execution context. 56 | 57 | ## Constants 58 | 59 | ### Design parameters 60 | 61 | | Name | Value | Description | 62 | | - | - | - | 63 | |-|-|-| 64 | 65 | ### Fixed-size type parameters 66 | 67 | 68 | | Name | Value | Description | 69 | | - | - | - | 70 | |-|-|-| 71 | 72 | ### Variable-size type parameters 73 | 74 | Helper values for SSZ operations. SSZ variable-size elements require a maximum length field. 75 | 76 | Most values are chosen to be the approximately the smallest possible value. 77 | 78 | | Name | Value | Description | 79 | | - | - | - | 80 | | MAX_ACCOUNT_NODES_PER_BLOCK | uint16(32768) | - | 81 | | MAX_BLOCKHASH_READS_PER_BLOCK | uint16(256) | A BLOCKHASH opcode may read up to 256 recent blocks | 82 | | MAX_BYTES_PER_NODE | uint16(32768) | - | 83 | | MAX_BYTES_PER_CONTRACT | uint16(32768) | - | 84 | | MAX_CONTRACTS_PER_BLOCK | uint16(2048) | - | 85 | | MAX_NODES_PER_PROOF | uint16(64) | - | 86 | | MAX_STORAGE_NODES_PER_BLOCK | uint16(32768) | - | 87 | | MAX_ACCOUNT_PROOFS_PER_BLOCK | uint16(8192) | - | 88 | | MAX_STORAGE_PROOFS_PER_ACCOUNT | uint16(8192) | - | 89 | 90 | ### Derived 91 | 92 | Constants derived from [design parameters](#design-parameters). 93 | 94 | | Name | Value | Description | 95 | | - | - | - | 96 | |-|-|-| 97 | 98 | ## Definitions 99 | 100 | ### RequiredBlockState 101 | 102 | The entire `RequiredBlockState` data format is represented by the following. 103 | 104 | As proofs sometimes have common internal nodes, the nodes are kept separate. 105 | A proof consists of a list of indices, indicating which node is used. 106 | 107 | ```python 108 | class RequiredBlockState(Container): 109 | # sorted 110 | compact_eip1186_proofs: List[CompactEip1186Proof, MAX_ACCOUNT_PROOFS_PER_BLOCK] 111 | # sorted 112 | contracts: List[Contract, MAX_CONTRACTS_PER_BLOCK] 113 | # sorted 114 | account_nodes: List[TrieNode, MAX_ACCOUNT_NODES_PER_BLOCK] 115 | # sorted 116 | storage_nodes: List[TrieNode, MAX_STORAGE_NODES_PER_BLOCK] 117 | # sorted 118 | block_hashes: List[RecentBlockHash, MAX_BLOCKHASH_READS_PER_BLOCK] 119 | ``` 120 | 121 | > Note that merkle patricia proofs may be replaced by verkle proofs after some hard fork 122 | 123 | ### CompactEip1186Proof 124 | 125 | ```python 126 | class CompactEip1186Proof(Container): 127 | address: Vector[uint8, 20] 128 | balance: List[uint8, 32] 129 | code_hash: Vector[uint8, 32] 130 | nonce: List[uint8, 8] 131 | storage_hash: Vector[uint8, 32] 132 | # sorted: node nearest to root first 133 | account_proof: List[uint16, MAX_NODES_PER_PROOF] 134 | # sorted 135 | storage_proofs: List[CompactStorageProof, MAX_STORAGE_PROOFS_PER_ACCOUNT] 136 | ``` 137 | 138 | ### Contract 139 | 140 | An alias for contract bytecode. 141 | ```python 142 | Contract = List[uint8, MAX_BYTES_PER_CONTRACT] 143 | ``` 144 | 145 | ### TrieNode 146 | 147 | An alias for a node in a merkle patricia proof. 148 | 149 | Merkle Patricia Trie (MPT) proofs consist of a list of witness nodes that correspond to each trie node that consists of various data elements depending on the type of node (e.g. blank, branch, extension, leaf). When serialized, each witness node is represented as an RLP serialized list of the component elements. 150 | 151 | ```python 152 | TrieNode = List[uint8, MAX_BYTES_PER_NODE] 153 | ``` 154 | 155 | ### RecentBlockHash 156 | 157 | ```python 158 | class RecentBlockHash(Container): 159 | block_number: List[uint8, 8] 160 | block_hash: Vector[uint8, 32] 161 | ``` 162 | 163 | ### CompactStorageProof 164 | 165 | The proof consists of a list of indices, one per node. The indices refer 166 | to the nodes in `TrieNode`. 167 | ```python 168 | class CompactStorageProof(Container): 169 | key: Vector[uint8, 32] 170 | value: List[uint8, 8] 171 | # sorted: node nearest to root first 172 | proof: List[uint16, MAX_NODES_PER_PROOF] 173 | ``` 174 | 175 | ## Helper functions 176 | 177 | High level algorithms relevant to the production/use of RequiredBlockState 178 | 179 | ### Get state accesses 180 | 181 | Trace the block with the prestate tracer, record key/value pairs where 182 | they are first encountered in the block. 183 | 184 | ### Get proofs 185 | 186 | Call eth_getProof for each state key required. Do this for the block prior 187 | to the block of interest (state is stored as post-block state). 188 | 189 | ### Verify data 190 | 191 | Check block hashes are canonical against an accumulator of canonical 192 | block hashes. Check merkle proofs in the requied block state. 193 | 194 | ### Trace block locally 195 | 196 | Obtain a block (eth_getBlockByNumber) with transactions. Use an EVM 197 | and load it with the `RequiredBlockState` and the block. Execute 198 | the block and observe the trace. -------------------------------------------------------------------------------- /spec/required_block_state_subprotocol.md: -------------------------------------------------------------------------------- 1 | # Required Block State Network 2 | 3 | This document is the specification for the sub-protocol that supports on-demand availability of state data from the execution chain to enable archive functionality. 4 | 5 | > 🚧 The spec is for design space exploration and is independent from the Portal Network 6 | 7 | This is the sub-protocol description for the `RequiredBlockState` data format described in [./spec/required_block_state_format.md](./required_block_state_format.md). This format implies a 8 | distributed network of approximately 30TB total size. 9 | 10 | These data, when combined with data in the History sub-protocol allow EVM re-execution (tracing) for arbitrary historical blocks. 11 | 12 | ## Overview 13 | 14 | The execution state network is a [Kademlia](https://pdos.csail.mit.edu/~petar/papers/maymounkov-kademlia-lncs.pdf) DHT that uses the [Portal Wire Protocol](./portal-wire-protocol.md) to establish an overlay network on top of the [Discovery v5](https://github.com/ethereum/devp2p/blob/master/discv5/discv5-wire.md) protocol. 15 | 16 | State data from the execution chain consists of all account data from the main storage trie, all contract storage data from all of the individual contract storage tries, and the individul bytecodes for all contracts. 17 | 18 | The EVM accesses the state when executing transactions through opcodes that read (e.g., SLOAD) or write (SSTORE) values that persist across blocks. After a block is finalized, the 19 | state that was accessed is known. To re-execute an old block (also called 'tracing', as in `eth_debugTraceBlockByNumber`), only that state is required. This data can be called `RequiredBlockState`. As state data is stored in merkle tree, a proof can accompany every value 20 | so that no trust is required. 21 | 22 | The EVM also has access to recent blockhash values via the BLOCKHASH opcode. This data can also 23 | be included in `RequiredBlockState` such that it is sufficient re-execute a block using two data structures: the block, and the `RequiredBlockState`. 24 | 25 | ### Data 26 | 27 | #### Types 28 | 29 | The network stores the full record of accessed state for the execution layer. This 30 | includes merkle proofs for the values that were accessed: 31 | 32 | - `RequiredBlockState` (state required to re-execute a block) 33 | - Account proofs 34 | - Contract bytecode 35 | - Contract storage proofs 36 | 37 | #### Retrieval 38 | 39 | - `RequiredBlockState` by block hash 40 | 41 | Note that the history sub-protocol can be used to obtain the block (header and body/transactions). 42 | Together, this is sufficient to re-execute that block. 43 | 44 | ## Specification 45 | 46 | 47 | 48 | ### Distance Function 49 | 50 | The state network uses the following "ring geometry" distance function. 51 | 52 | ```python 53 | MODULO = 2**256 54 | MID = 2**255 55 | 56 | def distance(node_id: uint256, content_id: uint256) -> uint256: 57 | """ 58 | A distance function for determining proximity between a node and content. 59 | 60 | Treats the keyspace as if it wraps around on both ends and 61 | returns the minimum distance needed to traverse between two 62 | different keys. 63 | 64 | Examples: 65 | 66 | >>> assert distance(10, 10) == 0 67 | >>> assert distance(5, 2**256 - 1) == 6 68 | >>> assert distance(2**256 - 1, 6) == 7 69 | >>> assert distance(5, 1) == 4 70 | >>> assert distance(1, 5) == 4 71 | >>> assert distance(0, 2**255) == 2**255 72 | >>> assert distance(0, 2**255 + 1) == 2**255 - 1 73 | """ 74 | if node_id > content_id: 75 | diff = node_id - content_id 76 | else: 77 | diff = content_id - node_id 78 | 79 | if diff > MID: 80 | return MODULO - diff 81 | else: 82 | return diff 83 | 84 | ``` 85 | 86 | This distance function distributes data evenly and deterministically across nodes. Nodes 87 | with similar IDs will store similar data. 88 | 89 | ### Content ID Derivation Function 90 | 91 | The derivation function for Content ID values is defined separately for each data type. 92 | 93 | ### Wire Protocol 94 | 95 | #### Protocol Identifier 96 | 97 | As specified in the protocol identifiers section of the Portal wire protocol, the `protocol` field in the `TALKREQ` message **MUST** contain the value of: 98 | 99 | `0x5050` (placeholder only) 100 | 101 | #### Supported Message Types 102 | 103 | The execution state network supports the following protocol messages: 104 | 105 | - `Ping` - `Pong` 106 | - `Find Nodes` - `Nodes` 107 | - `Find Content` - `Found Content` 108 | - `Offer` - `Accept` 109 | 110 | #### `Ping.custom_data` & `Pong.custom_data` 111 | 112 | In the execution state network the `custom_payload` field of the `Ping` and `Pong` messages is the serialization of an SSZ Container specified as `custom_data`: 113 | 114 | ``` 115 | custom_data = Container(data_radius: uint256) 116 | custom_payload = SSZ.serialize(custom_data) 117 | ``` 118 | 119 | ### Routing Table 120 | 121 | The execution state network uses the standard routing table structure from the Portal Wire Protocol. 122 | 123 | ### Node State 124 | 125 | #### Data Radius 126 | 127 | The execution state network includes one additional piece of node state that should be tracked. Nodes must track the `data_radius` from the Ping and Pong messages for other nodes in the network. This value is a 256 bit integer and represents the data that a node is "interested" in. We define the following function to determine whether node in the network should be interested in a piece of content. 128 | 129 | ``` 130 | interested(node, content) = distance(node.id, content.id) <= node.radius 131 | ``` 132 | 133 | A node is expected to maintain `radius` information for each node in its local node table. A node's `radius` value may fluctuate as the contents of its local key-value store change. 134 | 135 | A node should track their own radius value and provide this value in all Ping or Pong messages it sends to other nodes. 136 | 137 | ### Data Types 138 | 139 | #### Required block state 140 | 141 | See [./spec/required_block_state_format.md](./required_block_state_format.md) for 142 | the specification of the `RequiredBlockState` data type. 143 | 144 | The content is addressed in the portal network using the blockhash, similar to the block header 145 | in the history sub-protocol. 146 | 147 | ``` 148 | content := # See RequiredBlockState in the spec ./spec/required_block_state.md 149 | required_block_state_key := Container(block_hash: Bytes32) 150 | selector := 0x00 151 | content_key := selector + SSZ.serialize(required_block_state_key) 152 | content_id := keccak(content_key) 153 | ``` 154 | 155 | ## Gossip 156 | 157 | ### Overview 158 | 159 | A bridge node composes proofs for blocks when available. The network aims to have `RequiredBlockState` for every block older than 256 blocks. 160 | 161 | There are two modes in which a bridge mode may operate: 162 | - Contemporary: to populate data for recent (<=256) blocks. A bridge can be a full node. 163 | - Historic: to populate data for old (>256) blocks. An bridge must be an archive node. 164 | 165 | The bridge node must have access to a node that has the following methods: 166 | - eth_debugTraceBlockByNumber 167 | - prestateTracer. 168 | - default, with memory disabled. 169 | - eth_getProof 170 | - eth_getBlockByNumber 171 | 172 | The bridge node gossips each proof to some (bounded-size) subset of its peers who are closest to the data based on the distance metric. 173 | 174 | ### Terminology 175 | 176 | Historical bridge mode requires archive node, contemporary mode only requires 177 | full node. 178 | ``` 179 | Entire blockchain 180 | <0>-------------...----------<256>-------------------head 181 | 182 | Bridge modes 183 | |-------------Historic-------|-----Contemporary------| 184 | ``` 185 | ### eth_getProof complexity 186 | 187 | Some node architectures (flat architecture) make `eth_getProof` for old blocks difficult. See https://github.com/ledgerwatch/erigon/issues/7748. Recent blocks are not problematic. 188 | 189 | The network promises to have data for blocks older than 256 blocks. So, this lead-time allows 190 | bridge nodes in contemporary mode to populate the network before they fall outside this 'easy' 191 | window. 192 | 193 | The network is most optimally populated as follows: 194 | - Create historic data once using an archive node with architecture that supports 195 | `eth_getProof` to arbitrary depths. 196 | - Keep up with the chain tip with non-archive nodes. Full nodes have 197 | have eth_getProof and eth_debugTraceBlock capacity to a depth of about 256 198 | 199 | ### Denial of service - considerations 200 | 201 | The `RequiredBlockState` data is approximately 2.5MB on average (~167 kb/Mgas) by estimation. A bridge nodes could gossip data that has many valid parts, but some invalid data. While 202 | a recipient portal node can independently identify and reject this, it constitutes a denial of service vector. One specific vector is including valid state proof for state that is not required 203 | by the block. Hence, the node could trace the block locally to determine this, expending work without gain. 204 | 205 | One mitigation for this is to compare offers from peers and use a heuristic to reject spam. As `RequiredBlockState` is deterministic, if two separate bridges produce the same data, that is evidence for correctness. 206 | 207 | Another mitigation might be to have nodes signal if they have re-executed a given block using the `RequiredBlockState`. As node ids determine which data to hold and re-execute, a single node would not be required to re-execute long sequential collections of new data. A third mitigation is to create an accumulator for the hashes of all `RequiredBlockState` data that. 208 | 209 | Critically, the recipient is not vulnerable to incorrect state data as proofs are included and are quick to verify. `RequiredBlockState` is a self contained package that does not need 210 | additional network requests to verify. 211 | 212 | ### Denial of service - alternative designs 213 | 214 | Note that there are complexities with other approaches. Consider an alternative historical state network design where nodes store proofs for all history of individual accounts (flat distributed storage with reverse diffs, each with a state proof). To re-execute a block, 215 | one requests the required state from multiple nodes, which can happen by one of two means: 216 | - One has a list of states required. This list is a denial of service vector, one can create incorrect lists that cannot be rejected without tracing the block. Except this time the state must be obtained from the network. So the denial of service also can create network amplification. 217 | - Async EVM. Start executing the block and when a new state is required, pause and request it from the network. The time it would take to run one `debug_traceBlockByNumber` could be multiple minutes(network response time x number of states accessed). 218 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! A library for actions that combine different archors crates. 2 | -------------------------------------------------------------------------------- /tests/post_block_state_root.rs: -------------------------------------------------------------------------------- 1 | use std::str::FromStr; 2 | 3 | use archors_inventory::{ 4 | cache::{ 5 | get_block_from_cache, get_blockhashes_from_cache, get_contracts_from_cache, 6 | get_node_oracle_from_cache, get_post_state_proofs_from_cache, get_proofs_from_cache, 7 | }, 8 | utils::hex_encode, 9 | }; 10 | use archors_multiproof::{EIP1186MultiProof, StateForEvm}; 11 | use archors_tracer::trace::{BlockExecutor, PostExecutionProof}; 12 | use archors_types::proof::DisplayProof; 13 | use ethers::{ 14 | types::{EIP1186ProofResponse, H160, H256}, 15 | utils::keccak256, 16 | }; 17 | use log::info; 18 | 19 | /** 20 | Loads the state multiproof, executes the block, updates the multiproof for all changes 21 | the block caused and then checks that the storage in a particular account is correct. 22 | 23 | Canonical account storage roots were obtained vai eth_getProof. 24 | 25 | Account 0x00000000000000adc04c56bf30ac9d3c0aaf14dc has 3 keys modified by the block 17190873: 26 | - 0xfe073a4a8a654ccc9ca8e39369abc3b9919fde0aa58577acb685c63e0603a5a1 from 0 -> 0x0000000000000000000000000000010000000000000000000000000000010001. 27 | - 0xde1877e04330e34e13ed4a88ad37f2de41cb2cc9e5f0539ca4718f353374cbe2 from 0 -> 0x10000000000000000000000000000010001 28 | - 0x8b047007c345eab12063c0f43cc4c85dd576613e88dd595ed36f7ed99d774a9b from 0x20000000000000000000000000000010001 -> 0x20000000000000000000000000000020001 29 | */ 30 | #[test] 31 | #[ignore] 32 | fn test_single_account_update_from_block_17190873() { 33 | let block_number = 17190873; 34 | let block = get_block_from_cache(block_number).unwrap(); 35 | let proofs = get_proofs_from_cache(block_number) 36 | .unwrap() 37 | .proofs 38 | .into_values() 39 | .collect(); 40 | let code = get_contracts_from_cache(block_number).unwrap(); 41 | let block_hashes = get_blockhashes_from_cache(block_number) 42 | .unwrap() 43 | .to_hashmap(); 44 | let node_oracle = get_node_oracle_from_cache(block_number).unwrap(); 45 | 46 | let state: EIP1186MultiProof = 47 | EIP1186MultiProof::from_separate(proofs, code, block_hashes, node_oracle).unwrap(); 48 | 49 | let address = H160::from_str("0x00000000000000adc04c56bf30ac9d3c0aaf14dc").unwrap(); 50 | 51 | // Check storage root for account prior to block execution (rooted in block 17190872). 52 | let known_storage_root_17190872 = 53 | H256::from_str("0x8a150b46c0f63a2330dcb3a20c526798768e000f1d911ac70853c199d2accb94") 54 | .unwrap(); 55 | let computed_storage_root_17190872 = state.storage_proofs.get(&address).unwrap().root; 56 | assert_eq!(known_storage_root_17190872, computed_storage_root_17190872); 57 | 58 | // Check storage root for account after block execution (rooted in block 17190873). 59 | let executor = BlockExecutor::load(block, state, PostExecutionProof::UpdateAndIgnore).unwrap(); 60 | let post_state = executor.trace_block_silent().unwrap(); 61 | 62 | let computed_storage_root_17190873 = post_state.storage_proofs.get(&address).unwrap().root; 63 | let known_storage_root_17190873 = 64 | H256::from_str("0x4b8ea4eb3e8cafce4e8b35a4a3560c3f4a86ef33b804b25c406707139387a2c1") 65 | .unwrap(); 66 | assert_eq!(known_storage_root_17190873, computed_storage_root_17190873); 67 | post_state 68 | .print_storage_proof( 69 | "0x00000000000000adc04c56bf30ac9d3c0aaf14dc", 70 | "0xfe073a4a8a654ccc9ca8e39369abc3b9919fde0aa58577acb685c63e0603a5a1", 71 | ) 72 | .unwrap(); 73 | } 74 | 75 | /// Tests post-execution storage roots for each account. Multiproof updates are all applied 76 | /// after the block is executed. Post-execution roots can therefore be individually checked 77 | /// against those obtainable by calling eth_getProof on the block (they are cached for this block). 78 | #[test] 79 | #[ignore] 80 | fn test_individual_account_updates_from_block_17190873() { 81 | std::env::set_var("RUST_LOG", "debug"); 82 | env_logger::init(); 83 | let block_number = 17190873; 84 | let block = get_block_from_cache(block_number).unwrap(); 85 | let proofs = get_proofs_from_cache(block_number) 86 | .unwrap() 87 | .proofs 88 | .into_values() 89 | .collect(); 90 | let code = get_contracts_from_cache(block_number).unwrap(); 91 | let block_hashes = get_blockhashes_from_cache(block_number) 92 | .unwrap() 93 | .to_hashmap(); 94 | let node_oracle = get_node_oracle_from_cache(block_number).unwrap(); 95 | 96 | let state: EIP1186MultiProof = 97 | EIP1186MultiProof::from_separate(proofs, code, block_hashes, node_oracle).unwrap(); 98 | 99 | // Check storage root for accounts after block execution (rooted in block 17190873). 100 | // If the root is incorrect, run UpdateAndIgnore, then identify the error in next section. 101 | let executor = BlockExecutor::load(block, state, PostExecutionProof::UpdateAndIgnore).unwrap(); 102 | let computed_proofs = executor.trace_block_silent().unwrap(); 103 | 104 | // Interrogate each account vs known value in cached post-block RPC-based proofs. 105 | let mut expected_proofs: Vec = 106 | get_post_state_proofs_from_cache(block_number) 107 | .unwrap() 108 | .proofs 109 | .into_values() 110 | .collect(); 111 | expected_proofs.sort_by_key(|p| p.address); 112 | 113 | let account_sum = expected_proofs.len(); 114 | for (index, expected) in expected_proofs.into_iter().enumerate() { 115 | let address = expected.address; 116 | let computed_storage = computed_proofs.storage_proofs.get(&address).unwrap(); 117 | 118 | if expected.storage_hash != computed_storage.root { 119 | for storage in expected.storage_proof { 120 | let expected_proof = 121 | DisplayProof::init(storage.proof.into_iter().map(|p| p.to_vec()).collect()); 122 | let key_string: String = hex_encode(storage.key.as_bytes()); 123 | let address_string: String = hex_encode(address.as_bytes()); 124 | let computed_proof = computed_proofs 125 | .print_storage_proof(&address_string, &key_string) 126 | .unwrap(); 127 | match computed_proof.storage.different_final_node(&expected_proof) { 128 | true => { 129 | println!( 130 | "Proof for key {} has incorrect value. Expected proof: {}\nGot: {}", 131 | hex_encode(storage.key), 132 | expected_proof, 133 | computed_proof.storage 134 | ); 135 | } 136 | false => { 137 | if let Some(divergence_index) = 138 | computed_proof.storage.divergence_point(&expected_proof) 139 | { 140 | if divergence_index != 0 { 141 | println!( 142 | "key {} has bad proof (divergence index {}) but value is ok. Expected proof: {}\nGot: {}", 143 | hex_encode(storage.key), 144 | divergence_index, 145 | expected_proof, 146 | computed_proof.storage 147 | ); 148 | } 149 | } 150 | } 151 | } 152 | } 153 | 154 | assert_eq!( 155 | expected.storage_hash, 156 | computed_storage.root, 157 | "Storage hash for account {} incorrect (check number {})", 158 | hex_encode(address), 159 | index + 1 160 | ); 161 | } 162 | 163 | let expected_account_proof_post = DisplayProof::init( 164 | expected 165 | .account_proof 166 | .into_iter() 167 | .map(|node| node.to_vec()) 168 | .collect(), 169 | ); 170 | let computed_account_proof_post = computed_proofs 171 | .print_account_proof(&hex_encode(address)) 172 | .unwrap(); 173 | match expected_account_proof_post.different_final_node(&computed_account_proof_post) { 174 | true => todo!("Account {} final node different.", hex_encode(address)), 175 | false => { 176 | match computed_account_proof_post.divergence_point(&expected_account_proof_post) { 177 | None => {} 178 | Some(index) => println!( 179 | "Account {} differs at account proof index {}. ",//Expected {}\n Got {}", 180 | hex_encode(address), 181 | index, 182 | //expected_account_proof_post, 183 | //computed_account_proof_post 184 | ), 185 | } 186 | } 187 | } 188 | 189 | // println!("Finished account {} of {}", index + 1, account_sum); 190 | } 191 | } 192 | 193 | #[test] 194 | #[ignore] 195 | fn test_state_root_update_from_block_17190873() { 196 | todo!("similar to the account test, but check the state root") 197 | } 198 | --------------------------------------------------------------------------------