├── rust-toolchain ├── common ├── src │ ├── lib.rs │ ├── errors.rs │ ├── utils.rs │ └── types.rs └── Cargo.toml ├── execution ├── src │ ├── constants.rs │ ├── lib.rs │ ├── rpc │ │ ├── mod.rs │ │ ├── mock_rpc.rs │ │ └── http_rpc.rs │ ├── errors.rs │ ├── types.rs │ ├── proof.rs │ └── evm.rs ├── testdata │ ├── logs.json │ ├── transaction.json │ ├── receipt.json │ └── proof.json ├── Cargo.toml └── tests │ └── execution.rs ├── .gitignore ├── consensus ├── src │ ├── lib.rs │ ├── constants.rs │ ├── rpc │ │ ├── mod.rs │ │ ├── mock_rpc.rs │ │ └── nimbus_rpc.rs │ ├── errors.rs │ └── utils.rs ├── testdata │ ├── optimistic.json │ └── finality.json ├── Cargo.toml └── tests │ └── sync.rs ├── client ├── src │ ├── lib.rs │ ├── errors.rs │ ├── database.rs │ └── rpc.rs └── Cargo.toml ├── helios-ts ├── run.sh ├── tsconfig.json ├── package.json ├── Cargo.toml ├── index.html ├── webpack.config.js ├── lib.ts └── src │ └── lib.rs ├── examples ├── config.rs ├── client.rs ├── basic.rs ├── mixnet.rs └── checkpoints.rs ├── config ├── src │ ├── lib.rs │ ├── base.rs │ ├── types.rs │ ├── utils.rs │ ├── cli.rs │ ├── networks.rs │ ├── config.rs │ └── checkpoints.rs ├── Cargo.toml └── tests │ └── checkpoints.rs ├── cli ├── Cargo.toml └── src │ └── main.rs ├── .github └── workflows │ ├── benchmarks.yml │ ├── test.yml │ └── release.yml ├── LICENSE ├── nym-socks5 ├── Cargo.toml └── src │ ├── lib.rs │ ├── error.rs │ ├── config.rs │ └── tasks.rs ├── heliosup ├── heliosup └── install ├── benches ├── file_db.rs ├── get_code.rs ├── get_balance.rs ├── sync.rs └── harness.rs ├── Cargo.toml ├── src └── lib.rs ├── rpc.md ├── config.md └── README.md /rust-toolchain: -------------------------------------------------------------------------------- 1 | nightly-2023-01-23 2 | -------------------------------------------------------------------------------- /common/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod errors; 2 | pub mod types; 3 | pub mod utils; 4 | -------------------------------------------------------------------------------- /execution/src/constants.rs: -------------------------------------------------------------------------------- 1 | pub const PARALLEL_QUERY_BATCH_SIZE: usize = 20; 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | target 3 | *.env 4 | 5 | helios-ts/node_modules 6 | helios-ts/dist 7 | helios-ts/helios-*.tgz 8 | -------------------------------------------------------------------------------- /consensus/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod errors; 2 | pub mod rpc; 3 | pub mod types; 4 | 5 | mod consensus; 6 | pub use crate::consensus::*; 7 | 8 | mod constants; 9 | mod utils; 10 | -------------------------------------------------------------------------------- /client/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod client; 2 | pub use crate::client::*; 3 | 4 | pub mod database; 5 | pub mod errors; 6 | 7 | #[cfg(not(target_arch = "wasm32"))] 8 | pub mod rpc; 9 | 10 | pub mod node; 11 | -------------------------------------------------------------------------------- /execution/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod constants; 2 | pub mod errors; 3 | pub mod evm; 4 | pub mod rpc; 5 | pub mod types; 6 | 7 | mod execution; 8 | pub use crate::execution::*; 9 | 10 | mod proof; 11 | -------------------------------------------------------------------------------- /consensus/src/constants.rs: -------------------------------------------------------------------------------- 1 | // Consensus constants 2 | 3 | // https://github.com/ethereum/consensus-specs/blob/dev/specs/altair/light-client/p2p-interface.md#configuration 4 | pub const MAX_REQUEST_LIGHT_CLIENT_UPDATES: u8 = 128; 5 | -------------------------------------------------------------------------------- /helios-ts/run.sh: -------------------------------------------------------------------------------- 1 | set -e 2 | 3 | (&>/dev/null lcp --proxyUrl https://eth-mainnet.g.alchemy.com/v2/23IavJytUwkTtBMpzt_TZKwgwAarocdT --port 9001 &) 4 | (&>/dev/null lcp --proxyUrl https://www.lightclientdata.org --port 9002 &) 5 | 6 | npm run build 7 | simple-http-server 8 | -------------------------------------------------------------------------------- /helios-ts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist/", 4 | "noImplicitAny": true, 5 | "module": "es6", 6 | "target": "es6", 7 | "jsx": "react", 8 | "allowJs": true, 9 | "moduleResolution": "node", 10 | "sourceMap": true, 11 | "declaration": true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /common/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "common" 3 | version = "0.2.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | eyre = "0.6.8" 8 | serde = { version = "1.0.143", features = ["derive"] } 9 | hex = "0.4.3" 10 | ssz-rs = { git = "https://github.com/ralexstokes/ssz-rs", rev = "d09f55b4f8554491e3431e01af1c32347a8781cd" } 11 | ethers = "1.0.0" 12 | thiserror = "1.0.37" 13 | -------------------------------------------------------------------------------- /examples/config.rs: -------------------------------------------------------------------------------- 1 | use config::CliConfig; 2 | use eyre::Result; 3 | 4 | use helios::prelude::*; 5 | 6 | #[tokio::main] 7 | async fn main() -> Result<()> { 8 | // Load the config from the global config file 9 | let config_path = home::home_dir().unwrap().join(".helios/helios.toml"); 10 | let config = Config::from_file(&config_path, "mainnet", &CliConfig::default()); 11 | println!("Constructed config: {config:#?}"); 12 | 13 | Ok(()) 14 | } 15 | -------------------------------------------------------------------------------- /config/src/lib.rs: -------------------------------------------------------------------------------- 1 | /// Base Config 2 | pub mod base; 3 | pub use base::*; 4 | 5 | /// Core Config 6 | pub mod config; 7 | pub use crate::config::*; 8 | 9 | /// Checkpoint Config 10 | pub mod checkpoints; 11 | pub use checkpoints::*; 12 | 13 | /// Cli Config 14 | pub mod cli; 15 | pub use cli::*; 16 | 17 | /// Network Configuration 18 | pub mod networks; 19 | pub use networks::*; 20 | 21 | /// Generic Config Types 22 | pub mod types; 23 | pub use types::*; 24 | 25 | /// Generic Utilities 26 | pub mod utils; 27 | -------------------------------------------------------------------------------- /helios-ts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "helios", 3 | "version": "0.1.0", 4 | "main": "./dist/lib.js", 5 | "types": "./dist/lib.d.ts", 6 | "scripts": { 7 | "build": "webpack" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "devDependencies": { 13 | "@wasm-tool/wasm-pack-plugin": "^1.6.0", 14 | "ts-loader": "^9.4.1", 15 | "typescript": "^4.9.3", 16 | "webpack": "^5.75.0", 17 | "webpack-cli": "^5.0.0" 18 | }, 19 | "dependencies": { 20 | "ethers": "^5.7.2" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /config/src/base.rs: -------------------------------------------------------------------------------- 1 | use serde::Serialize; 2 | 3 | use crate::types::{ChainConfig, Forks}; 4 | use crate::utils::bytes_serialize; 5 | 6 | /// The base configuration for a network. 7 | #[derive(Serialize, Default)] 8 | pub struct BaseConfig { 9 | pub rpc_port: u16, 10 | pub consensus_rpc: Option, 11 | #[serde( 12 | deserialize_with = "bytes_deserialize", 13 | serialize_with = "bytes_serialize" 14 | )] 15 | pub default_checkpoint: Vec, 16 | pub chain: ChainConfig, 17 | pub forks: Forks, 18 | pub max_checkpoint_age: u64, 19 | } 20 | -------------------------------------------------------------------------------- /cli/Cargo.toml: -------------------------------------------------------------------------------- 1 | cargo-features = ["different-binary-name"] 2 | 3 | [package] 4 | name = "cli" 5 | version = "0.2.0" 6 | edition = "2021" 7 | 8 | [[bin]] 9 | name = "cli" 10 | filename = "helios" 11 | path = "src/main.rs" 12 | 13 | [dependencies] 14 | tokio = { version = "1", features = ["full"] } 15 | clap = { version = "3.2.18", features = ["derive", "env"] } 16 | eyre = "0.6.8" 17 | dirs = "4.0.0" 18 | env_logger = "0.9.0" 19 | log = "0.4.17" 20 | ctrlc = "3.2.3" 21 | futures = "0.3.23" 22 | 23 | client = { path = "../client" } 24 | config = { path = "../config" } 25 | common = { path = "../common" } 26 | -------------------------------------------------------------------------------- /.github/workflows/benchmarks.yml: -------------------------------------------------------------------------------- 1 | name: benchmarks 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | 7 | env: 8 | MAINNET_RPC_URL: ${{ secrets.MAINNET_RPC_URL }} 9 | GOERLI_RPC_URL: ${{ secrets.GOERLI_RPC_URL }} 10 | 11 | jobs: 12 | benches: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v3 16 | - uses: actions-rs/toolchain@v1 17 | with: 18 | profile: minimal 19 | toolchain: nightly 20 | override: true 21 | components: rustfmt 22 | - uses: Swatinem/rust-cache@v2 23 | - uses: actions-rs/cargo@v1 24 | with: 25 | command: bench 26 | -------------------------------------------------------------------------------- /config/Cargo.toml: -------------------------------------------------------------------------------- 1 | 2 | [package] 3 | name = "config" 4 | version = "0.2.0" 5 | edition = "2021" 6 | 7 | [dependencies] 8 | eyre = "0.6.8" 9 | serde = { version = "1.0.143", features = ["derive"] } 10 | hex = "0.4.3" 11 | ssz-rs = { git = "https://github.com/ralexstokes/ssz-rs", rev = "d09f55b4f8554491e3431e01af1c32347a8781cd" } 12 | ethers = "1.0.0" 13 | figment = { version = "0.10.7", features = ["toml", "env"] } 14 | thiserror = "1.0.37" 15 | log = "0.4.17" 16 | reqwest = "0.11.13" 17 | serde_yaml = "0.9.14" 18 | strum = "0.24.1" 19 | futures = "0.3.25" 20 | 21 | common = { path = "../common" } 22 | 23 | [target.'cfg(not(target_arch = "wasm32"))'.dependencies] 24 | tokio = { version = "1", features = ["full"] } 25 | -------------------------------------------------------------------------------- /common/src/errors.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | use crate::types::BlockTag; 4 | 5 | #[derive(Debug, Error)] 6 | #[error("block not available: {block}")] 7 | pub struct BlockNotFoundError { 8 | block: BlockTag, 9 | } 10 | 11 | impl BlockNotFoundError { 12 | pub fn new(block: BlockTag) -> Self { 13 | Self { block } 14 | } 15 | } 16 | 17 | #[derive(Debug, Error)] 18 | #[error("rpc error on method: {method}, message: {error}")] 19 | pub struct RpcError { 20 | method: String, 21 | error: E, 22 | } 23 | 24 | impl RpcError { 25 | pub fn new(method: &str, err: E) -> Self { 26 | Self { 27 | method: method.to_string(), 28 | error: err, 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /common/src/utils.rs: -------------------------------------------------------------------------------- 1 | use ethers::prelude::Address; 2 | use eyre::Result; 3 | use ssz_rs::{Node, Vector}; 4 | 5 | use super::types::Bytes32; 6 | 7 | pub fn hex_str_to_bytes(s: &str) -> Result> { 8 | let stripped = s.strip_prefix("0x").unwrap_or(s); 9 | Ok(hex::decode(stripped)?) 10 | } 11 | 12 | pub fn bytes32_to_node(bytes: &Bytes32) -> Result { 13 | Ok(Node::from_bytes(bytes.as_slice().try_into()?)) 14 | } 15 | 16 | pub fn bytes_to_bytes32(bytes: &[u8]) -> Bytes32 { 17 | Vector::from_iter(bytes.to_vec()) 18 | } 19 | 20 | pub fn address_to_hex_string(address: &Address) -> String { 21 | format!("0x{}", hex::encode(address.as_bytes())) 22 | } 23 | 24 | pub fn u64_to_hex_string(val: u64) -> String { 25 | format!("0x{val:x}") 26 | } 27 | -------------------------------------------------------------------------------- /execution/testdata/logs.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "transactionHash": "0x2dac1b27ab58b493f902dda8b63979a112398d747f1761c0891777c0983e591f", 4 | "address": "0x326c977e6efc84e512bb9c30f76e30c160ed06fb", 5 | "blockHash": "0x6663f197e991f5a0bb235f33ec554b9bd48c37b4f5002d7ac2abdfa99f86ac14", 6 | "blockNumber": "0x72e9b5", 7 | "data": "0x000000000000000000000000000000000000000000000001158e460913d00000", 8 | "logIndex": "0x0", 9 | "removed": false, 10 | "topics": [ 11 | "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", 12 | "0x0000000000000000000000004281ecf07378ee595c564a59048801330f3084ee", 13 | "0x0000000000000000000000007daccf9b3c1ae2fa5c55f1c978aeef700bc83be0" 14 | ], 15 | "transactionIndex": "0x0" 16 | } 17 | ] -------------------------------------------------------------------------------- /helios-ts/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "helios-ts" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [lib] 9 | crate-type = ["cdylib"] 10 | 11 | [dependencies] 12 | wasm-bindgen = "0.2.83" 13 | wasm-bindgen-futures = "0.4.33" 14 | serde-wasm-bindgen = "0.4.5" 15 | console_error_panic_hook = "0.1.7" 16 | 17 | ethers = "1.0.0" 18 | hex = "0.4.3" 19 | serde = { version = "1.0.143", features = ["derive"] } 20 | serde_json = "1.0.85" 21 | 22 | client = { path = "../client" } 23 | common = { path = "../common" } 24 | consensus = { path = "../consensus" } 25 | execution = { path = "../execution" } 26 | config = { path = "../config" } 27 | 28 | [dependencies.web-sys] 29 | version = "0.3" 30 | features = [ 31 | "console", 32 | ] 33 | -------------------------------------------------------------------------------- /helios-ts/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | hello-wasm example 6 | 7 | 8 | 9 | 10 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /config/src/types.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | use crate::utils::{bytes_deserialize, bytes_serialize}; 4 | 5 | #[derive(Serialize, Deserialize, Debug, Default, Clone)] 6 | pub struct ChainConfig { 7 | pub chain_id: u64, 8 | pub genesis_time: u64, 9 | #[serde( 10 | deserialize_with = "bytes_deserialize", 11 | serialize_with = "bytes_serialize" 12 | )] 13 | pub genesis_root: Vec, 14 | } 15 | 16 | #[derive(Serialize, Deserialize, Debug, Default, Clone)] 17 | pub struct Forks { 18 | pub genesis: Fork, 19 | pub altair: Fork, 20 | pub bellatrix: Fork, 21 | } 22 | 23 | #[derive(Serialize, Deserialize, Debug, Default, Clone)] 24 | pub struct Fork { 25 | pub epoch: u64, 26 | #[serde( 27 | deserialize_with = "bytes_deserialize", 28 | serialize_with = "bytes_serialize" 29 | )] 30 | pub fork_version: Vec, 31 | } 32 | -------------------------------------------------------------------------------- /consensus/testdata/optimistic.json: -------------------------------------------------------------------------------- 1 | { 2 | "attested_header": { 3 | "slot": "3818196", 4 | "proposer_index": "162891", 5 | "parent_root": "0x3a0ed66198e518928422cf2164d3a04f00ba2746f718f05c196c0e073ee80051", 6 | "state_root": "0x859fe66e13da75d7f686581415f6b370c8d09c0bf83a6b019c74493c535387c0", 7 | "body_root": "0x4cd79571a142221743ffe43439f2d7a2caf081a185113032577a84189ec7b25e" 8 | }, 9 | "sync_aggregate": { 10 | "sync_committee_bits": "0x7df8ffdeff5b7fdbfffb77efffffefdfefddf8f7e7f7ffbffdfbfd5fbfefbfff8f3fff7dffffde7cfd7f55d7fffbcfdee7efa7bd5fdbbfbded9fff637fffed7f", 11 | "sync_committee_signature": "0x87c6142478c55127b1f570d115935383c5d4be650df710e935b3048322b0207372a5016a0b2e3ad8ef195841fcdb12c108e6960456c14e5be1f15f0097ba6c427e93796ae1da39c277b7b6ce16ad4d02196a358a59a3e476e134dae56d389e6e" 12 | }, 13 | "signature_slot": "3818197" 14 | } 15 | -------------------------------------------------------------------------------- /helios-ts/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const WasmPackPlugin = require("@wasm-tool/wasm-pack-plugin"); 3 | 4 | module.exports = { 5 | entry: "./lib.ts", 6 | module: { 7 | rules: [ 8 | { 9 | test: /\.ts?$/, 10 | use: 'ts-loader', 11 | exclude: /node_modules/, 12 | }, 13 | { 14 | test: /\.wasm$/, 15 | type: "asset/inline", 16 | }, 17 | ], 18 | }, 19 | resolve: { 20 | extensions: ['.ts', '.js'], 21 | }, 22 | output: { 23 | filename: "lib.js", 24 | globalObject: 'this', 25 | path: path.resolve(__dirname, "dist"), 26 | library: { 27 | name: "helios", 28 | type: "umd", 29 | } 30 | }, 31 | experiments: { 32 | asyncWebAssembly: true, 33 | }, 34 | plugins: [ 35 | new WasmPackPlugin({ 36 | extraArgs: "--target web", 37 | crateDirectory: path.resolve(__dirname), 38 | }), 39 | ], 40 | }; 41 | -------------------------------------------------------------------------------- /config/src/utils.rs: -------------------------------------------------------------------------------- 1 | use common::utils::hex_str_to_bytes; 2 | 3 | pub fn bytes_deserialize<'de, D>(deserializer: D) -> Result, D::Error> 4 | where 5 | D: serde::Deserializer<'de>, 6 | { 7 | let bytes: String = serde::Deserialize::deserialize(deserializer)?; 8 | Ok(hex_str_to_bytes(&bytes).unwrap()) 9 | } 10 | 11 | pub fn bytes_serialize(bytes: &Vec, serializer: S) -> Result 12 | where 13 | S: serde::Serializer, 14 | { 15 | let bytes_string = hex::encode(bytes); 16 | serializer.serialize_str(&bytes_string) 17 | } 18 | 19 | pub fn bytes_opt_deserialize<'de, D>(deserializer: D) -> Result>, D::Error> 20 | where 21 | D: serde::Deserializer<'de>, 22 | { 23 | let bytes_opt: Option = serde::Deserialize::deserialize(deserializer)?; 24 | if let Some(bytes) = bytes_opt { 25 | Ok(Some(hex_str_to_bytes(&bytes).unwrap())) 26 | } else { 27 | Ok(None) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /client/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "client" 3 | version = "0.2.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | eyre = "0.6.8" 8 | serde = { version = "1.0.143", features = ["derive"] } 9 | hex = "0.4.3" 10 | ssz-rs = { git = "https://github.com/ralexstokes/ssz-rs", rev = "d09f55b4f8554491e3431e01af1c32347a8781cd" } 11 | ethers = "1.0.0" 12 | futures = "0.3.23" 13 | log = "0.4.17" 14 | thiserror = "1.0.37" 15 | 16 | common = { path = "../common" } 17 | consensus = { path = "../consensus" } 18 | execution = { path = "../execution" } 19 | config = { path = "../config" } 20 | nym-socks5 = { path = "../nym-socks5" } 21 | 22 | [target.'cfg(not(target_arch = "wasm32"))'.dependencies] 23 | jsonrpsee = { version = "0.15.1", features = ["full"] } 24 | tokio = { version = "1", features = ["full"] } 25 | 26 | [target.'cfg(target_arch = "wasm32")'.dependencies] 27 | gloo-timers = "0.2.6" 28 | wasm-bindgen-futures = "0.4.33" 29 | tokio = { version = "1", features = ["sync"] } 30 | 31 | -------------------------------------------------------------------------------- /consensus/src/rpc/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod mock_rpc; 2 | pub mod nimbus_rpc; 3 | 4 | use async_trait::async_trait; 5 | use eyre::Result; 6 | 7 | use crate::types::{BeaconBlock, Bootstrap, FinalityUpdate, OptimisticUpdate, Update}; 8 | 9 | // implements https://github.com/ethereum/beacon-APIs/tree/master/apis/beacon/light_client 10 | #[cfg_attr(not(target_arch = "wasm32"), async_trait)] 11 | #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] 12 | pub trait ConsensusRpc { 13 | fn new(path: &str) -> Self; 14 | fn new_mixnet(path: &str) -> Self; 15 | async fn get_bootstrap(&self, block_root: &'_ [u8]) -> Result; 16 | async fn get_updates(&self, period: u64, count: u8) -> Result>; 17 | async fn get_finality_update(&self) -> Result; 18 | async fn get_optimistic_update(&self) -> Result; 19 | async fn get_block(&self, slot: u64) -> Result; 20 | async fn chain_id(&self) -> Result; 21 | } 22 | -------------------------------------------------------------------------------- /consensus/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "consensus" 3 | version = "0.2.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | eyre = "0.6.8" 8 | futures = "0.3.23" 9 | serde = { version = "1.0.143", features = ["derive"] } 10 | serde_json = "1.0.85" 11 | hex = "0.4.3" 12 | ssz-rs = { git = "https://github.com/ralexstokes/ssz-rs", rev = "d09f55b4f8554491e3431e01af1c32347a8781cd" } 13 | milagro_bls = { git = "https://github.com/Snowfork/milagro_bls" } 14 | ethers = "1.0.0" 15 | bytes = "1.2.1" 16 | toml = "0.5.9" 17 | async-trait = "0.1.57" 18 | log = "0.4.17" 19 | chrono = "0.4.22" 20 | thiserror = "1.0.37" 21 | reqwest = { version = "0.11.13", features = ["json", "socks"] } 22 | 23 | common = { path = "../common" } 24 | config = { path = "../config" } 25 | 26 | [target.'cfg(not(target_arch = "wasm32"))'.dependencies] 27 | openssl = { version = "0.10", features = ["vendored"] } 28 | tokio = { version = "1", features = ["full"] } 29 | 30 | [target.'cfg(target_arch = "wasm32")'.dependencies] 31 | wasm-timer = "0.2.5" 32 | 33 | -------------------------------------------------------------------------------- /execution/testdata/transaction.json: -------------------------------------------------------------------------------- 1 | { 2 | "blockHash": "0x6663f197e991f5a0bb235f33ec554b9bd48c37b4f5002d7ac2abdfa99f86ac14", 3 | "blockNumber": "0x72e9b5", 4 | "hash": "0x2dac1b27ab58b493f902dda8b63979a112398d747f1761c0891777c0983e591f", 5 | "accessList": [], 6 | "chainId": "0x5", 7 | "from": "0x4281ecf07378ee595c564a59048801330f3084ee", 8 | "gas": "0xea60", 9 | "gasPrice": "0x9502f908", 10 | "input": "0xa9059cbb0000000000000000000000007daccf9b3c1ae2fa5c55f1c978aeef700bc83be0000000000000000000000000000000000000000000000001158e460913d00000", 11 | "maxFeePerGas": "0x9502f910", 12 | "maxPriorityFeePerGas": "0x9502f900", 13 | "nonce": "0x623355", 14 | "r": "0xe1445466b058b6f883c0222f1b1f3e2ad9bee7b5f688813d86e3fa8f93aa868c", 15 | "s": "0x786d6e7f3aefa8fe73857c65c32e4884d8ba38d0ecfb947fbffb82e8ee80c167", 16 | "to": "0x326c977e6efc84e512bb9c30f76e30c160ed06fb", 17 | "transactionIndex": "0x0", 18 | "type": "0x2", 19 | "v": "0x0", 20 | "value": "0x0" 21 | } 22 | -------------------------------------------------------------------------------- /execution/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "execution" 3 | version = "0.2.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | reqwest = { version = "0.11", features = ["json"] } 8 | eyre = "0.6.8" 9 | serde = { version = "1.0.143", features = ["derive"] } 10 | serde_json = "1.0.85" 11 | hex = "0.4.3" 12 | ssz-rs = { git = "https://github.com/ralexstokes/ssz-rs", rev = "d09f55b4f8554491e3431e01af1c32347a8781cd" } 13 | revm = { version = "2.3", default-features = false, features = ["std", "k256", "with-serde"] } 14 | ethers = "1.0.0" 15 | bytes = "1.2.1" 16 | futures = "0.3.23" 17 | toml = "0.5.9" 18 | triehash-ethereum = { git = "https://github.com/openethereum/parity-ethereum", rev = "55c90d4016505317034e3e98f699af07f5404b63" } 19 | async-trait = "0.1.57" 20 | log = "0.4.17" 21 | thiserror = "1.0.37" 22 | 23 | common = { path = "../common" } 24 | consensus = { path = "../consensus" } 25 | 26 | [target.'cfg(not(target_arch = "wasm32"))'.dependencies] 27 | openssl = { version = "0.10", features = ["vendored"] } 28 | tokio = { version = "1", features = ["full"] } 29 | -------------------------------------------------------------------------------- /consensus/src/errors.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | #[derive(Debug, Error)] 4 | pub enum ConsensusError { 5 | #[error("insufficient participation")] 6 | InsufficientParticipation, 7 | #[error("invalid timestamp")] 8 | InvalidTimestamp, 9 | #[error("invalid sync committee period")] 10 | InvalidPeriod, 11 | #[error("update not relevant")] 12 | NotRelevant, 13 | #[error("invalid finality proof")] 14 | InvalidFinalityProof, 15 | #[error("invalid next sync committee proof")] 16 | InvalidNextSyncCommitteeProof, 17 | #[error("invalid current sync committee proof")] 18 | InvalidCurrentSyncCommitteeProof, 19 | #[error("invalid sync committee signature")] 20 | InvalidSignature, 21 | #[error("invalid header hash found: {0}, expected: {1}")] 22 | InvalidHeaderHash(String, String), 23 | #[error("payload not found for slot: {0}")] 24 | PayloadNotFound(u64), 25 | #[error("checkpoint is too old")] 26 | CheckpointTooOld, 27 | #[error("consensus rpc is for the incorrect network")] 28 | IncorrectRpcNetwork, 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 a16z 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /nym-socks5/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "nym-socks5" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | futures = "0.3" 10 | log = { version = "0.4", features = ["serde"] } 11 | reqwest = { version = "0.11", features = ["json", "socks"] } 12 | serde = { version = "1.0", features = ["derive"] } 13 | serde_json = "1.0" 14 | tap = "1" 15 | thiserror = "1" 16 | tokio = { version = "1.21.2", features = ["time", "rt-multi-thread", "process", "macros", "sync", "net", "signal"] } 17 | 18 | client-core = { git = "https://github.com/nymtech/nym", branch = "helios_dep", package = "client-core" } 19 | config-common = { git = "https://github.com/nymtech/nym", branch = "helios_dep", package = "nym-config" } 20 | network_defaults = { git = "https://github.com/nymtech/nym", branch = "helios_dep", package = "nym-network-defaults" } 21 | nym_socks5 = { git = "https://github.com/nymtech/nym", branch = "helios_dep", package = "nym-socks5-client" } 22 | task = { git = "https://github.com/nymtech/nym", branch = "helios_dep", package = "nym-task" } -------------------------------------------------------------------------------- /examples/client.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use eyre::Result; 4 | 5 | use helios::prelude::*; 6 | 7 | #[tokio::main] 8 | async fn main() -> Result<()> { 9 | // Create a new Helios Client Builder 10 | let mut builder = ClientBuilder::new(); 11 | 12 | // Set the network to mainnet 13 | builder = builder.network(networks::Network::MAINNET); 14 | 15 | // Set the consensus rpc url 16 | builder = builder.consensus_rpc("https://www.lightclientdata.org"); 17 | 18 | // Set the execution rpc url 19 | builder = builder.execution_rpc("https://eth-mainnet.g.alchemy.com/v2/XXXXX"); 20 | 21 | // Set the checkpoint to the last known checkpoint 22 | builder = 23 | builder.checkpoint("85e6151a246e8fdba36db27a0c7678a575346272fe978c9281e13a8b26cdfa68"); 24 | 25 | // Set the rpc port 26 | builder = builder.rpc_port(8545); 27 | 28 | // Set the data dir 29 | builder = builder.data_dir(PathBuf::from("/tmp/helios")); 30 | 31 | // Set the fallback service 32 | builder = builder.fallback("https://sync-mainnet.beaconcha.in"); 33 | 34 | // Enable lazy checkpoints 35 | builder = builder.load_external_fallback(); 36 | 37 | // Build the client 38 | let _client: Client = builder.build().unwrap(); 39 | println!("Constructed client!"); 40 | 41 | Ok(()) 42 | } 43 | -------------------------------------------------------------------------------- /heliosup/heliosup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | # modified from https://github.com/foundry-rs/foundry/blob/master/foundryup/foundryup 5 | 6 | REPO="a16z/helios" 7 | NAME=helios 8 | 9 | DIR=$HOME/.$NAME 10 | BIN_DIR=$DIR/bin 11 | 12 | # delete existing binaries 13 | rm -f $BIN_DIR/$NAME 14 | 15 | TAG=$(curl https://api.github.com/repos/$REPO/releases/latest | grep -o '"tag_name": "[^"]*' | grep -o '[^"]*$') 16 | 17 | PLATFORM="$(uname -s)" 18 | case $PLATFORM in 19 | Linux) 20 | PLATFORM="linux" 21 | ;; 22 | Darwin) 23 | PLATFORM="darwin" 24 | ;; 25 | *) 26 | err "unsupported platform: $PLATFORM" 27 | ;; 28 | esac 29 | 30 | ARCHITECTURE="$(uname -m)" 31 | if [ "${ARCHITECTURE}" = "x86_64" ]; then 32 | # Redirect stderr to /dev/null to avoid printing errors if non Rosetta. 33 | if [ "$(sysctl -n sysctl.proc_translated 2>/dev/null)" = "1" ]; then 34 | ARCHITECTURE="arm64" # Rosetta. 35 | else 36 | ARCHITECTURE="amd64" # Intel. 37 | fi 38 | elif [ "${ARCHITECTURE}" = "arm64" ] ||[ "${ARCHITECTURE}" = "aarch64" ] ; then 39 | ARCHITECTURE="arm64" # Arm. 40 | else 41 | ARCHITECTURE="amd64" # Amd. 42 | fi 43 | 44 | TARBALL_URL="https://github.com/$REPO/releases/download/${TAG}/${NAME}_${PLATFORM}_${ARCHITECTURE}.tar.gz" 45 | curl -L $TARBALL_URL | tar -xzC $BIN_DIR 46 | 47 | echo "Installed $NAME" 48 | -------------------------------------------------------------------------------- /consensus/tests/sync.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use config::{networks, Config}; 4 | use consensus::{rpc::mock_rpc::MockRpc, ConsensusClient}; 5 | 6 | async fn setup() -> ConsensusClient { 7 | let base_config = networks::goerli(); 8 | let config = Config { 9 | consensus_rpc: String::new(), 10 | execution_rpc: String::new(), 11 | chain: base_config.chain, 12 | forks: base_config.forks, 13 | max_checkpoint_age: 123123123, 14 | ..Default::default() 15 | }; 16 | 17 | let checkpoint = 18 | hex::decode("1e591af1e90f2db918b2a132991c7c2ee9a4ab26da496bd6e71e4f0bd65ea870").unwrap(); 19 | 20 | ConsensusClient::new("testdata/", &checkpoint, Arc::new(config)).unwrap() 21 | } 22 | 23 | #[tokio::test] 24 | async fn test_sync() { 25 | let mut client = setup().await; 26 | client.sync().await.unwrap(); 27 | 28 | let head = client.get_header(); 29 | assert_eq!(head.slot, 3818196); 30 | 31 | let finalized_head = client.get_finalized_header(); 32 | assert_eq!(finalized_head.slot, 3818112); 33 | } 34 | 35 | #[tokio::test] 36 | async fn test_get_payload() { 37 | let mut client = setup().await; 38 | client.sync().await.unwrap(); 39 | 40 | let payload = client.get_execution_payload(&None).await.unwrap(); 41 | assert_eq!(payload.block_number, 7530932); 42 | } 43 | -------------------------------------------------------------------------------- /execution/src/rpc/mod.rs: -------------------------------------------------------------------------------- 1 | use async_trait::async_trait; 2 | use ethers::types::{ 3 | transaction::eip2930::AccessList, Address, EIP1186ProofResponse, Filter, Log, Transaction, 4 | TransactionReceipt, H256, 5 | }; 6 | use eyre::Result; 7 | 8 | use crate::types::CallOpts; 9 | 10 | pub mod http_rpc; 11 | pub mod mock_rpc; 12 | 13 | #[cfg_attr(not(target_arch = "wasm32"), async_trait)] 14 | #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] 15 | pub trait ExecutionRpc: Send + Clone + Sync + 'static { 16 | fn new(rpc: &str) -> Result 17 | where 18 | Self: Sized; 19 | fn new_mixnet(rpc: &str) -> Result 20 | where 21 | Self: Sized; 22 | 23 | async fn get_proof( 24 | &self, 25 | address: &Address, 26 | slots: &[H256], 27 | block: u64, 28 | ) -> Result; 29 | 30 | async fn create_access_list(&self, opts: &CallOpts, block: u64) -> Result; 31 | async fn get_code(&self, address: &Address, block: u64) -> Result>; 32 | async fn send_raw_transaction(&self, bytes: &[u8]) -> Result; 33 | async fn get_transaction_receipt(&self, tx_hash: &H256) -> Result>; 34 | async fn get_transaction(&self, tx_hash: &H256) -> Result>; 35 | async fn get_logs(&self, filter: &Filter) -> Result>; 36 | async fn chain_id(&self) -> Result; 37 | } 38 | -------------------------------------------------------------------------------- /nym-socks5/src/lib.rs: -------------------------------------------------------------------------------- 1 | use crate::error::BackendError; 2 | use futures::SinkExt; 3 | use nym_socks5::client::{Socks5ControlMessage, Socks5ControlMessageSender}; 4 | use tokio::task::JoinHandle; 5 | 6 | pub mod config; 7 | pub mod error; 8 | pub mod tasks; 9 | 10 | pub struct Socks5Server { 11 | control_tx: Socks5ControlMessageSender, 12 | exit_join_handler: JoinHandle<()>, 13 | _msg_rx: task::StatusReceiver, 14 | } 15 | 16 | impl Socks5Server { 17 | pub fn new( 18 | control_tx: Socks5ControlMessageSender, 19 | exit_join_handler: JoinHandle<()>, 20 | _msg_rx: task::StatusReceiver, 21 | ) -> Self { 22 | Socks5Server { 23 | control_tx, 24 | exit_join_handler, 25 | _msg_rx, 26 | } 27 | } 28 | 29 | pub async fn terminate(mut self) { 30 | // disconnect 31 | match self 32 | .control_tx 33 | .send(Socks5ControlMessage::Stop) 34 | .await 35 | .map_err(|err| { 36 | log::warn!("Failed trying to send disconnect signal: {err}"); 37 | BackendError::CoundNotSendDisconnectSignal 38 | }) { 39 | Ok(_) => log::info!("✅✅✅✅✅✅ SOCKS5 >>> Disconnected"), 40 | Err(e) => log::error!("Failed to disconnect SOCKS5: {}", e), 41 | } 42 | 43 | if let Err(e) = self.exit_join_handler.await { 44 | log::error!("Failed to join after existing SOCKS5: {:?}", e); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /heliosup/install: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | # modified from https://github.com/foundry-rs/foundry/blob/master/foundryup/install 5 | 6 | REPO="a16z/helios" 7 | NAME=helios 8 | 9 | INSTALLER_NAME=${NAME}up 10 | 11 | DIR=$HOME/.$NAME 12 | BIN_DIR=$DIR/bin 13 | 14 | BIN_URL="https://raw.githubusercontent.com/$REPO/master/$INSTALLER_NAME/$INSTALLER_NAME" 15 | BIN_PATH="$BIN_DIR/$INSTALLER_NAME" 16 | 17 | # Create the bin directory and binary if it doesn't exist. 18 | mkdir -p $BIN_DIR 19 | curl -# -L $BIN_URL -o $BIN_PATH 20 | chmod +x $BIN_PATH 21 | 22 | # Store the correct profile file (i.e. .profile for bash or .zshrc for ZSH). 23 | case $SHELL in 24 | */zsh) 25 | PROFILE=$HOME/.zshrc 26 | PREF_SHELL=zsh 27 | ;; 28 | */bash) 29 | PROFILE=$HOME/.bashrc 30 | PREF_SHELL=bash 31 | ;; 32 | */fish) 33 | PROFILE=$HOME/.config/fish/config.fish 34 | PREF_SHELL=fish 35 | ;; 36 | *) 37 | echo "$INSTALLER_NAME: could not detect shell, manually add ${BIN_DIR} to your PATH." 38 | exit 1 39 | esac 40 | 41 | # Only add installer if it isn't already in PATH. 42 | if [[ ":$PATH:" != *":${BIN_DIR}:"* ]]; then 43 | # Add the foundryup directory to the path and ensure the old PATH variables remain. 44 | echo >> $PROFILE && echo "export PATH=\"\$PATH:$BIN_DIR\"" >> $PROFILE 45 | fi 46 | 47 | echo && echo "Detected your preferred shell is ${PREF_SHELL} and added ${INSTALLER_NAME} to PATH. Run 'source ${PROFILE}' or start a new terminal session to use $INSTALLER_NAME." 48 | echo "Then, simply run '$INSTALLER_NAME' to install $NAME." 49 | -------------------------------------------------------------------------------- /examples/basic.rs: -------------------------------------------------------------------------------- 1 | use std::{path::PathBuf, str::FromStr}; 2 | 3 | use env_logger::Env; 4 | use ethers::{types::Address, utils}; 5 | use eyre::Result; 6 | use helios::{config::networks::Network, prelude::*}; 7 | 8 | #[tokio::main] 9 | async fn main() -> Result<()> { 10 | env_logger::Builder::from_env(Env::default().default_filter_or("info")).init(); 11 | 12 | let untrusted_rpc_url = "https://eth-mainnet.g.alchemy.com/v2/"; 13 | log::info!("Using untrusted RPC URL [REDACTED]"); 14 | 15 | let consensus_rpc = "https://www.lightclientdata.org"; 16 | log::info!("Using consensus RPC URL: {}", consensus_rpc); 17 | 18 | let mut client: Client = ClientBuilder::new() 19 | .network(Network::MAINNET) 20 | .consensus_rpc(consensus_rpc) 21 | .execution_rpc(untrusted_rpc_url) 22 | .load_external_fallback() 23 | .data_dir(PathBuf::from("/tmp/helios")) 24 | .build()?; 25 | 26 | log::info!( 27 | "Built client on network \"{}\" with external checkpoint fallbacks", 28 | Network::MAINNET 29 | ); 30 | 31 | client.start().await?; 32 | 33 | let head_block_num = client.get_block_number().await?; 34 | let addr = Address::from_str("0x00000000219ab540356cBB839Cbe05303d7705Fa")?; 35 | let block = BlockTag::Latest; 36 | let balance = client.get_balance(&addr, block).await?; 37 | 38 | log::info!("synced up to block: {}", head_block_num); 39 | log::info!( 40 | "balance of deposit contract: {}", 41 | utils::format_ether(balance) 42 | ); 43 | 44 | Ok(()) 45 | } 46 | -------------------------------------------------------------------------------- /examples/mixnet.rs: -------------------------------------------------------------------------------- 1 | use std::{path::PathBuf, str::FromStr}; 2 | 3 | use env_logger::Env; 4 | use ethers::{types::Address, utils}; 5 | use eyre::Result; 6 | use helios::{config::networks::Network, prelude::*}; 7 | 8 | #[tokio::main] 9 | async fn main() -> Result<()> { 10 | env_logger::Builder::from_env(Env::default().default_filter_or("info")).init(); 11 | 12 | let untrusted_rpc_url = "https://eth-mainnet.g.alchemy.com/v2/X3DvnCxZ9oGOQ8tNTm5fDUFsPLjYVX4B"; 13 | log::info!("Using untrusted RPC URL [REDACTED]"); 14 | 15 | let consensus_rpc = "https://www.lightclientdata.org"; 16 | log::info!("Using consensus RPC URL: {}", consensus_rpc); 17 | 18 | let mut client: Client = ClientBuilder::new() 19 | .network(Network::MAINNET) 20 | .consensus_rpc(consensus_rpc) 21 | .execution_rpc(untrusted_rpc_url) 22 | .use_mixnet() 23 | .data_dir(PathBuf::from("/tmp/helios")) 24 | .build()?; 25 | 26 | log::info!( 27 | "Built client on network \"{}\" with external checkpoint fallbacks", 28 | Network::MAINNET 29 | ); 30 | 31 | client.start().await?; 32 | log::info!("Started client"); 33 | 34 | let head_block_num = client.get_block_number().await?; 35 | let addr = Address::from_str("0x00000000219ab540356cBB839Cbe05303d7705Fa")?; 36 | let block = BlockTag::Number(head_block_num); 37 | let balance = client.get_balance(&addr, block).await?; 38 | 39 | log::info!("synced up to block: {}", head_block_num); 40 | log::info!( 41 | "balance of deposit contract: {}", 42 | utils::format_ether(balance) 43 | ); 44 | 45 | client.terminate().await; 46 | 47 | Ok(()) 48 | } 49 | -------------------------------------------------------------------------------- /common/src/types.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | 3 | use serde::{de::Error, Deserialize}; 4 | use ssz_rs::Vector; 5 | 6 | pub type Bytes32 = Vector; 7 | 8 | #[derive(Debug, Clone, Copy)] 9 | pub enum BlockTag { 10 | Latest, 11 | Finalized, 12 | Number(u64), 13 | } 14 | 15 | impl Display for BlockTag { 16 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 17 | let formatted = match self { 18 | Self::Latest => "latest".to_string(), 19 | Self::Finalized => "finalized".to_string(), 20 | Self::Number(num) => num.to_string(), 21 | }; 22 | 23 | write!(f, "{formatted}") 24 | } 25 | } 26 | 27 | impl<'de> Deserialize<'de> for BlockTag { 28 | fn deserialize(deserializer: D) -> Result 29 | where 30 | D: serde::Deserializer<'de>, 31 | { 32 | let block: String = serde::Deserialize::deserialize(deserializer)?; 33 | let parse_error = D::Error::custom("could not parse block tag"); 34 | 35 | let block_tag = match block.as_str() { 36 | "latest" => BlockTag::Latest, 37 | "finalized" => BlockTag::Finalized, 38 | _ => match block.strip_prefix("0x") { 39 | Some(hex_block) => { 40 | let num = u64::from_str_radix(hex_block, 16).map_err(|_| parse_error)?; 41 | 42 | BlockTag::Number(num) 43 | } 44 | None => { 45 | let num = block.parse().map_err(|_| parse_error)?; 46 | 47 | BlockTag::Number(num) 48 | } 49 | }, 50 | }; 51 | 52 | Ok(block_tag) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /benches/file_db.rs: -------------------------------------------------------------------------------- 1 | use client::database::Database; 2 | use config::Config; 3 | use criterion::{criterion_group, criterion_main, Criterion}; 4 | use helios::prelude::FileDB; 5 | use tempfile::tempdir; 6 | 7 | mod harness; 8 | 9 | criterion_main!(file_db); 10 | criterion_group! { 11 | name = file_db; 12 | config = Criterion::default(); 13 | targets = save_checkpoint, load_checkpoint 14 | } 15 | 16 | /// Benchmark saving/writing a checkpoint to the file db. 17 | pub fn save_checkpoint(c: &mut Criterion) { 18 | c.bench_function("save_checkpoint", |b| { 19 | let checkpoint = vec![1, 2, 3]; 20 | b.iter(|| { 21 | let data_dir = Some(tempdir().unwrap().into_path()); 22 | let config = Config { 23 | data_dir, 24 | ..Default::default() 25 | }; 26 | let db = FileDB::new(&config).unwrap(); 27 | db.save_checkpoint(checkpoint.clone()).unwrap(); 28 | }) 29 | }); 30 | } 31 | 32 | /// Benchmark loading a checkpoint from the file db. 33 | pub fn load_checkpoint(c: &mut Criterion) { 34 | c.bench_function("load_checkpoint", |b| { 35 | // First write to the db 36 | let data_dir = Some(tempdir().unwrap().into_path()); 37 | let config = Config { 38 | data_dir, 39 | ..Default::default() 40 | }; 41 | let db = FileDB::new(&config).unwrap(); 42 | let written_checkpoint = vec![1; 32]; 43 | db.save_checkpoint(written_checkpoint.clone()).unwrap(); 44 | 45 | // Then read from the db 46 | b.iter(|| { 47 | let checkpoint = db.load_checkpoint().unwrap(); 48 | assert_eq!(checkpoint, written_checkpoint.clone()); 49 | }) 50 | }); 51 | } 52 | -------------------------------------------------------------------------------- /consensus/testdata/finality.json: -------------------------------------------------------------------------------- 1 | { 2 | "attested_header": { 3 | "slot": "3818190", 4 | "proposer_index": "25281", 5 | "parent_root": "0x1dd492af3d3032ed3860b80dd665cbe6532540f9ea0e4eab94509edde1c415fb", 6 | "state_root": "0x49d23f99f6870b0c1e911cbd1fbb92adc5a7d95cb8cda1f5f0f6086ccb098324", 7 | "body_root": "0xa8f89861ef4ae81125bec76fa200a28150005ab3fc5f4b58743eddfbb6c9e23b" 8 | }, 9 | "finalized_header": { 10 | "slot": "3818112", 11 | "proposer_index": "160102", 12 | "parent_root": "0xd8621bdbaa5bf4b3947f56af917b2558bf1d3006aef58811ad80c555f0410af8", 13 | "state_root": "0xcd46eb0959ebedaa6b78b270b8280981bb41205d9c604c1d9a547167286fa9d6", 14 | "body_root": "0xc94f81458235e538bc6160099b8f7fdb73bce9d63a68e7def926dfcfe0427635" 15 | }, 16 | "finality_branch": [ 17 | "0x14d2010000000000000000000000000000000000000000000000000000000000", 18 | "0x869277312d2050cc3a76274661a03beb3f0cbc19c75e91f2bac5505723f192c4", 19 | "0x5030f60861b1b0a96f0c4804558926bc76875110374c49fce679dc12854a8313", 20 | "0x1365f7a14a5b13886b5afe6e3aa0e831baacc30e26bb314392a6273b088d9b96", 21 | "0xeebcbfb7a668bbe769764651ce9819be3b8c4d19ef30bd97772b106472a4df05", 22 | "0x87538264f92a515e0d0706858a76ef7819d53658545a17e9648eba40bc3880fc" 23 | ], 24 | "sync_aggregate": { 25 | "sync_committee_bits": "0x7df83fd6ff5b7fdbffeb77efbfefefdfefddf8f7e5f7ffbffdfbfd5fbeefbfff8f3fff7dffdfde7c6d7f55d7fffbcfdee7cfadbd5fcbbfbde59fff637fffed7f", 26 | "sync_committee_signature": "0x8f7cd24ce4d07f81319d8263a0b4cd35635ff1b476bdb5b23f4fb3677c82a895863e430f494279e75df88b493cb160460d2965f2c0c06a4ea5a9878eb318f46b73e473d923fd6b985ac66122118428e881347ee37c177e6810601952d3c225d9" 27 | }, 28 | "signature_slot": "3818191" 29 | } 30 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | jobs: 10 | check: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | - uses: actions-rs/toolchain@v1 15 | with: 16 | profile: minimal 17 | toolchain: nightly 18 | override: true 19 | - uses: Swatinem/rust-cache@v2 20 | - uses: actions-rs/cargo@v1 21 | with: 22 | command: check 23 | 24 | test: 25 | runs-on: ubuntu-latest 26 | steps: 27 | - uses: actions/checkout@v3 28 | - uses: actions-rs/toolchain@v1 29 | with: 30 | profile: minimal 31 | toolchain: nightly 32 | override: true 33 | - uses: Swatinem/rust-cache@v2 34 | - uses: actions-rs/cargo@v1 35 | with: 36 | command: test 37 | args: --all 38 | 39 | fmt: 40 | runs-on: ubuntu-latest 41 | steps: 42 | - uses: actions/checkout@v3 43 | - uses: actions-rs/toolchain@v1 44 | with: 45 | profile: minimal 46 | toolchain: nightly 47 | override: true 48 | components: rustfmt 49 | - uses: Swatinem/rust-cache@v2 50 | - uses: actions-rs/cargo@v1 51 | with: 52 | command: fmt 53 | args: --all -- --check 54 | 55 | clippy: 56 | runs-on: ubuntu-latest 57 | steps: 58 | - uses: actions/checkout@v3 59 | - uses: actions-rs/toolchain@v1 60 | with: 61 | profile: minimal 62 | toolchain: nightly 63 | override: true 64 | components: clippy 65 | - uses: Swatinem/rust-cache@v2 66 | - uses: actions-rs/cargo@v1 67 | with: 68 | command: clippy 69 | args: --all -- -D warnings 70 | -------------------------------------------------------------------------------- /execution/src/errors.rs: -------------------------------------------------------------------------------- 1 | use bytes::Bytes; 2 | use ethers::{ 3 | abi::AbiDecode, 4 | types::{Address, H256, U256}, 5 | }; 6 | use eyre::Report; 7 | use thiserror::Error; 8 | 9 | #[derive(Debug, Error)] 10 | pub enum ExecutionError { 11 | #[error("invalid account proof for address: {0}")] 12 | InvalidAccountProof(Address), 13 | #[error("invalid storage proof for address: {0}, slot: {1}")] 14 | InvalidStorageProof(Address, H256), 15 | #[error("code hash mismatch for address: {0}, found: {1}, expected: {2}")] 16 | CodeHashMismatch(Address, String, String), 17 | #[error("receipt root mismatch for tx: {0}")] 18 | ReceiptRootMismatch(String), 19 | #[error("missing transaction for tx: {0}")] 20 | MissingTransaction(String), 21 | #[error("could not prove receipt for tx: {0}")] 22 | NoReceiptForTransaction(String), 23 | #[error("missing log for transaction: {0}, index: {1}")] 24 | MissingLog(String, U256), 25 | #[error("too many logs to prove: {0}, current limit is: {1}")] 26 | TooManyLogsToProve(usize, usize), 27 | #[error("execution rpc is for the incorect network")] 28 | IncorrectRpcNetwork(), 29 | } 30 | 31 | /// Errors that can occur during evm.rs calls 32 | #[derive(Debug, Error)] 33 | pub enum EvmError { 34 | #[error("execution reverted: {0:?}")] 35 | Revert(Option), 36 | 37 | #[error("evm error: {0:?}")] 38 | Generic(String), 39 | 40 | #[error("evm execution failed: {0:?}")] 41 | Revm(revm::Return), 42 | 43 | #[error("rpc error: {0:?}")] 44 | RpcError(Report), 45 | } 46 | 47 | impl EvmError { 48 | pub fn decode_revert_reason(data: impl AsRef<[u8]>) -> Option { 49 | let data = data.as_ref(); 50 | 51 | // skip function selector 52 | if data.len() < 4 { 53 | return None; 54 | } 55 | String::decode(&data[4..]).ok() 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /examples/checkpoints.rs: -------------------------------------------------------------------------------- 1 | use eyre::Result; 2 | 3 | // From helios::config 4 | use config::{checkpoints, networks}; 5 | 6 | #[tokio::main] 7 | async fn main() -> Result<()> { 8 | // Construct the checkpoint fallback services. 9 | // The `build` method will fetch a list of [CheckpointFallbackService]s from a community-mainained list by ethPandaOps. 10 | // This list is NOT guaranteed to be secure, but is provided in good faith. 11 | // The raw list can be found here: https://github.com/ethpandaops/checkpoint-sync-health-checks/blob/master/_data/endpoints.yaml 12 | let cf = checkpoints::CheckpointFallback::new() 13 | .build() 14 | .await 15 | .unwrap(); 16 | 17 | // Fetch the latest goerli checkpoint 18 | let goerli_checkpoint = cf 19 | .fetch_latest_checkpoint(&networks::Network::GOERLI) 20 | .await 21 | .unwrap(); 22 | println!("Fetched latest goerli checkpoint: {goerli_checkpoint}"); 23 | 24 | // Fetch the latest mainnet checkpoint 25 | let mainnet_checkpoint = cf 26 | .fetch_latest_checkpoint(&networks::Network::MAINNET) 27 | .await 28 | .unwrap(); 29 | println!("Fetched latest mainnet checkpoint: {mainnet_checkpoint}"); 30 | 31 | // Let's get a list of all the fallback service endpoints for mainnet 32 | let endpoints = cf.get_all_fallback_endpoints(&networks::Network::MAINNET); 33 | println!("Fetched all mainnet fallback endpoints: {endpoints:?}"); 34 | 35 | // Since we built the checkpoint fallback services, we can also just get the raw checkpoint fallback services. 36 | // The `get_fallback_services` method returns a reference to the internal list of CheckpointFallbackService objects 37 | // for the given network. 38 | let services = cf.get_fallback_services(&networks::Network::MAINNET); 39 | println!("Fetched all mainnet fallback services: {services:?}"); 40 | 41 | Ok(()) 42 | } 43 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "helios" 3 | version = "0.2.0" 4 | edition = "2021" 5 | autobenches = false 6 | 7 | [workspace] 8 | members = [ 9 | "cli", 10 | "client", 11 | "common", 12 | "config", 13 | "consensus", 14 | "execution", 15 | "helios-ts", 16 | "nym-socks5", 17 | ] 18 | 19 | [dependencies] 20 | client = { path = "./client" } 21 | config = { path = "./config" } 22 | common = { path = "./common" } 23 | consensus = { path = "./consensus" } 24 | execution = { path = "./execution" } 25 | 26 | [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] 27 | tokio = { version = "1", features = ["full"] } 28 | eyre = "0.6.8" 29 | home = "0.5.4" 30 | ethers = "1.0.0" 31 | env_logger = "0.9.0" 32 | log = "0.4.17" 33 | tracing-test = "0.2.3" 34 | criterion = { version = "0.4", features = [ "async_tokio", "plotters" ]} 35 | plotters = "0.3.3" 36 | tempfile = "3.3.0" 37 | hex = "0.4.3" 38 | 39 | [patch.crates-io] 40 | ethers = { git = "https://github.com/gakonst/ethers-rs", rev = "c17c0c3c956f12d205a5ede3176599d8a30ca739" } 41 | 42 | [profile.release] 43 | strip = true 44 | opt-level = "z" 45 | lto = true 46 | codegen-units = 1 47 | panic = "abort" 48 | 49 | ###################################### 50 | # Examples 51 | ###################################### 52 | 53 | [[example]] 54 | name = "checkpoints" 55 | path = "examples/checkpoints.rs" 56 | 57 | [[example]] 58 | name = "basic" 59 | path = "examples/basic.rs" 60 | 61 | [[example]] 62 | name = "client" 63 | path = "examples/client.rs" 64 | 65 | [[example]] 66 | name = "config" 67 | path = "examples/config.rs" 68 | 69 | ###################################### 70 | # Benchmarks 71 | ###################################### 72 | 73 | [[bench]] 74 | name = "file_db" 75 | harness = false 76 | 77 | [[bench]] 78 | name = "get_balance" 79 | harness = false 80 | 81 | [[bench]] 82 | name = "get_code" 83 | harness = false 84 | 85 | [[bench]] 86 | name = "sync" 87 | harness = false 88 | -------------------------------------------------------------------------------- /consensus/src/rpc/mock_rpc.rs: -------------------------------------------------------------------------------- 1 | use std::{fs::read_to_string, path::PathBuf}; 2 | 3 | use super::ConsensusRpc; 4 | use crate::types::{BeaconBlock, Bootstrap, FinalityUpdate, OptimisticUpdate, Update}; 5 | use async_trait::async_trait; 6 | use eyre::Result; 7 | pub struct MockRpc { 8 | testdata: PathBuf, 9 | } 10 | 11 | #[cfg_attr(not(target_arch = "wasm32"), async_trait)] 12 | #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] 13 | impl ConsensusRpc for MockRpc { 14 | fn new(path: &str) -> Self { 15 | MockRpc { 16 | testdata: PathBuf::from(path), 17 | } 18 | } 19 | 20 | fn new_mixnet(path: &str) -> Self { 21 | MockRpc { 22 | testdata: PathBuf::from(path), 23 | } 24 | } 25 | 26 | async fn get_bootstrap(&self, _block_root: &'_ [u8]) -> Result { 27 | let bootstrap = read_to_string(self.testdata.join("bootstrap.json"))?; 28 | Ok(serde_json::from_str(&bootstrap)?) 29 | } 30 | 31 | async fn get_updates(&self, _period: u64, _count: u8) -> Result> { 32 | let updates = read_to_string(self.testdata.join("updates.json"))?; 33 | Ok(serde_json::from_str(&updates)?) 34 | } 35 | 36 | async fn get_finality_update(&self) -> Result { 37 | let finality = read_to_string(self.testdata.join("finality.json"))?; 38 | Ok(serde_json::from_str(&finality)?) 39 | } 40 | 41 | async fn get_optimistic_update(&self) -> Result { 42 | let optimistic = read_to_string(self.testdata.join("optimistic.json"))?; 43 | Ok(serde_json::from_str(&optimistic)?) 44 | } 45 | 46 | async fn get_block(&self, _slot: u64) -> Result { 47 | let block = read_to_string(self.testdata.join("blocks.json"))?; 48 | Ok(serde_json::from_str(&block)?) 49 | } 50 | 51 | async fn chain_id(&self) -> Result { 52 | eyre::bail!("not implemented") 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /execution/testdata/receipt.json: -------------------------------------------------------------------------------- 1 | { 2 | "transactionHash": "0x2dac1b27ab58b493f902dda8b63979a112398d747f1761c0891777c0983e591f", 3 | "blockHash": "0x6663f197e991f5a0bb235f33ec554b9bd48c37b4f5002d7ac2abdfa99f86ac14", 4 | "blockNumber": "0x72e9b5", 5 | "logs": [ 6 | { 7 | "transactionHash": "0x2dac1b27ab58b493f902dda8b63979a112398d747f1761c0891777c0983e591f", 8 | "address": "0x326c977e6efc84e512bb9c30f76e30c160ed06fb", 9 | "blockHash": "0x6663f197e991f5a0bb235f33ec554b9bd48c37b4f5002d7ac2abdfa99f86ac14", 10 | "blockNumber": "0x72e9b5", 11 | "data": "0x000000000000000000000000000000000000000000000001158e460913d00000", 12 | "logIndex": "0x0", 13 | "removed": false, 14 | "topics": [ 15 | "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", 16 | "0x0000000000000000000000004281ecf07378ee595c564a59048801330f3084ee", 17 | "0x0000000000000000000000007daccf9b3c1ae2fa5c55f1c978aeef700bc83be0" 18 | ], 19 | "transactionIndex": "0x0" 20 | } 21 | ], 22 | "contractAddress": null, 23 | "effectiveGasPrice": "0x9502f908", 24 | "cumulativeGasUsed": "0xca6a", 25 | "from": "0x4281ecf07378ee595c564a59048801330f3084ee", 26 | "gasUsed": "0xca6a", 27 | "logsBloom": "0x00000000000000040000000000000000000000000000100008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000004000000000020000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000800000000400000001000000000000000000000000000000000000000000000", 28 | "status": "0x1", 29 | "to": "0x326c977e6efc84e512bb9c30f76e30c160ed06fb", 30 | "transactionIndex": "0x0", 31 | "type": "0x2" 32 | } 33 | -------------------------------------------------------------------------------- /config/src/cli.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, path::PathBuf}; 2 | 3 | use figment::{providers::Serialized, value::Value}; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | /// Cli Config 7 | #[derive(Serialize, Deserialize, Debug, Clone, Default)] 8 | pub struct CliConfig { 9 | pub execution_rpc: Option, 10 | pub consensus_rpc: Option, 11 | pub checkpoint: Option>, 12 | pub rpc_port: Option, 13 | pub data_dir: PathBuf, 14 | pub fallback: Option, 15 | pub load_external_fallback: bool, 16 | pub strict_checkpoint_age: bool, 17 | pub use_mixnet: bool, 18 | } 19 | 20 | impl CliConfig { 21 | pub fn as_provider(&self, network: &str) -> Serialized> { 22 | let mut user_dict = HashMap::new(); 23 | 24 | if let Some(rpc) = &self.execution_rpc { 25 | user_dict.insert("execution_rpc", Value::from(rpc.clone())); 26 | } 27 | 28 | if let Some(rpc) = &self.consensus_rpc { 29 | user_dict.insert("consensus_rpc", Value::from(rpc.clone())); 30 | } 31 | 32 | if let Some(checkpoint) = &self.checkpoint { 33 | user_dict.insert("checkpoint", Value::from(hex::encode(checkpoint))); 34 | } 35 | 36 | if let Some(port) = self.rpc_port { 37 | user_dict.insert("rpc_port", Value::from(port)); 38 | } 39 | 40 | user_dict.insert("data_dir", Value::from(self.data_dir.to_str().unwrap())); 41 | 42 | if let Some(fallback) = &self.fallback { 43 | user_dict.insert("fallback", Value::from(fallback.clone())); 44 | } 45 | 46 | user_dict.insert( 47 | "load_external_fallback", 48 | Value::from(self.load_external_fallback), 49 | ); 50 | 51 | user_dict.insert( 52 | "strict_checkpoint_age", 53 | Value::from(self.strict_checkpoint_age), 54 | ); 55 | 56 | user_dict.insert("use_mixnet", Value::from(self.use_mixnet)); 57 | 58 | Serialized::from(user_dict, network) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /config/tests/checkpoints.rs: -------------------------------------------------------------------------------- 1 | use config::networks; 2 | use ethers::types::H256; 3 | 4 | #[tokio::test] 5 | async fn test_checkpoint_fallback() { 6 | let cf = config::checkpoints::CheckpointFallback::new(); 7 | 8 | assert_eq!(cf.services.get(&networks::Network::MAINNET), None); 9 | assert_eq!(cf.services.get(&networks::Network::GOERLI), None); 10 | 11 | assert_eq!( 12 | cf.networks, 13 | [networks::Network::MAINNET, networks::Network::GOERLI].to_vec() 14 | ); 15 | } 16 | 17 | #[tokio::test] 18 | async fn test_construct_checkpoints() { 19 | let cf = config::checkpoints::CheckpointFallback::new() 20 | .build() 21 | .await 22 | .unwrap(); 23 | 24 | assert!(cf.services[&networks::Network::MAINNET].len() > 1); 25 | assert!(cf.services[&networks::Network::GOERLI].len() > 1); 26 | } 27 | 28 | #[tokio::test] 29 | async fn test_fetch_latest_checkpoints() { 30 | let cf = config::checkpoints::CheckpointFallback::new() 31 | .build() 32 | .await 33 | .unwrap(); 34 | let checkpoint = cf 35 | .fetch_latest_checkpoint(&networks::Network::GOERLI) 36 | .await 37 | .unwrap(); 38 | assert!(checkpoint != H256::zero()); 39 | let checkpoint = cf 40 | .fetch_latest_checkpoint(&networks::Network::MAINNET) 41 | .await 42 | .unwrap(); 43 | assert!(checkpoint != H256::zero()); 44 | } 45 | 46 | #[tokio::test] 47 | async fn test_get_all_fallback_endpoints() { 48 | let cf = config::checkpoints::CheckpointFallback::new() 49 | .build() 50 | .await 51 | .unwrap(); 52 | let urls = cf.get_all_fallback_endpoints(&networks::Network::MAINNET); 53 | assert!(!urls.is_empty()); 54 | let urls = cf.get_all_fallback_endpoints(&networks::Network::GOERLI); 55 | assert!(!urls.is_empty()); 56 | } 57 | 58 | #[tokio::test] 59 | async fn test_get_healthy_fallback_endpoints() { 60 | let cf = config::checkpoints::CheckpointFallback::new() 61 | .build() 62 | .await 63 | .unwrap(); 64 | let urls = cf.get_healthy_fallback_endpoints(&networks::Network::MAINNET); 65 | assert!(!urls.is_empty()); 66 | let urls = cf.get_healthy_fallback_endpoints(&networks::Network::GOERLI); 67 | assert!(!urls.is_empty()); 68 | } 69 | -------------------------------------------------------------------------------- /client/src/errors.rs: -------------------------------------------------------------------------------- 1 | use common::errors::BlockNotFoundError; 2 | use execution::errors::EvmError; 3 | use eyre::Report; 4 | use thiserror::Error; 5 | 6 | /// Errors that can occur during Node calls 7 | #[derive(Debug, Error)] 8 | pub enum NodeError { 9 | #[error(transparent)] 10 | ExecutionEvmError(#[from] EvmError), 11 | 12 | #[error("socks5 error: {0}")] 13 | Socks5Error(#[from] nym_socks5::error::BackendError), 14 | 15 | #[error("execution error: {0}")] 16 | ExecutionError(Report), 17 | 18 | #[error("consensus payload error: {0}")] 19 | ConsensusPayloadError(Report), 20 | 21 | #[error("execution payload error: {0}")] 22 | ExecutionPayloadError(Report), 23 | 24 | #[error("consensus client creation error: {0}")] 25 | ConsensusClientCreationError(Report), 26 | 27 | #[error("execution client creation error: {0}")] 28 | ExecutionClientCreationError(Report), 29 | 30 | #[error("consensus advance error: {0}")] 31 | ConsensusAdvanceError(Report), 32 | 33 | #[error("consensus sync error: {0}")] 34 | ConsensusSyncError(Report), 35 | 36 | #[error(transparent)] 37 | BlockNotFoundError(#[from] BlockNotFoundError), 38 | } 39 | 40 | #[cfg(not(target_arch = "wasm32"))] 41 | impl NodeError { 42 | pub fn to_json_rpsee_error(self) -> jsonrpsee::core::Error { 43 | match self { 44 | NodeError::ExecutionEvmError(evm_err) => match evm_err { 45 | EvmError::Revert(data) => { 46 | let mut msg = "execution reverted".to_string(); 47 | if let Some(reason) = data.as_ref().and_then(EvmError::decode_revert_reason) { 48 | msg = format!("{msg}: {reason}") 49 | } 50 | jsonrpsee::core::Error::Call(jsonrpsee::types::error::CallError::Custom( 51 | jsonrpsee::types::error::ErrorObject::owned( 52 | 3, 53 | msg, 54 | data.map(|data| format!("0x{}", hex::encode(data))), 55 | ), 56 | )) 57 | } 58 | _ => jsonrpsee::core::Error::Custom(evm_err.to_string()), 59 | }, 60 | _ => jsonrpsee::core::Error::Custom(self.to_string()), 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /nym-socks5/src/error.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2023 - Nym Technologies SA 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | use client_core::error::ClientCoreError; 5 | use serde::{Serialize, Serializer}; 6 | use thiserror::Error; 7 | 8 | #[allow(unused)] 9 | #[derive(Error, Debug)] 10 | pub enum BackendError { 11 | #[error("{source}")] 12 | ReqwestError { 13 | #[from] 14 | source: reqwest::Error, 15 | }, 16 | #[error("I/O error: {source}")] 17 | IoError { 18 | #[from] 19 | source: std::io::Error, 20 | }, 21 | #[error("String formatting error: {source}")] 22 | FmtError { 23 | #[from] 24 | source: std::fmt::Error, 25 | }, 26 | #[error("{source}")] 27 | SerdeJsonError { 28 | #[from] 29 | source: serde_json::Error, 30 | }, 31 | #[error("{source}")] 32 | ClientCoreError { 33 | #[from] 34 | source: ClientCoreError, 35 | }, 36 | #[error("Could not send disconnect signal to the SOCKS5 client")] 37 | CoundNotSendDisconnectSignal, 38 | #[error("No service provider set")] 39 | NoServiceProviderSet, 40 | #[error("No gateway provider set")] 41 | NoGatewaySet, 42 | #[error("Initialization failed with a panic")] 43 | InitializationPanic, 44 | #[error("Could not get config id before gateway is set")] 45 | CouldNotGetIdWithoutGateway, 46 | #[error("Could initialize without gateway set")] 47 | CouldNotInitWithoutGateway, 48 | #[error("Could initialize without service provider set")] 49 | CouldNotInitWithoutServiceProvider, 50 | #[error("Could not get file name")] 51 | CouldNotGetFilename, 52 | #[error("Could not get config file location")] 53 | CouldNotGetConfigFilename, 54 | #[error("Could not load existing gateway configuration")] 55 | CouldNotLoadExistingGatewayConfiguration(std::io::Error), 56 | #[error("Unable to open a new window")] 57 | NewWindowError, 58 | } 59 | 60 | impl Serialize for BackendError { 61 | fn serialize(&self, serializer: S) -> std::result::Result 62 | where 63 | S: Serializer, 64 | { 65 | serializer.collect_str(self) 66 | } 67 | } 68 | 69 | // Local crate level Result alias 70 | pub(crate) type Result = std::result::Result; 71 | -------------------------------------------------------------------------------- /client/src/database.rs: -------------------------------------------------------------------------------- 1 | #[cfg(not(target_arch = "wasm32"))] 2 | use std::{ 3 | fs, 4 | io::{Read, Write}, 5 | path::PathBuf, 6 | }; 7 | 8 | use config::Config; 9 | use eyre::Result; 10 | 11 | pub trait Database { 12 | fn new(config: &Config) -> Result 13 | where 14 | Self: Sized; 15 | fn save_checkpoint(&self, checkpoint: Vec) -> Result<()>; 16 | fn load_checkpoint(&self) -> Result>; 17 | } 18 | 19 | #[cfg(not(target_arch = "wasm32"))] 20 | pub struct FileDB { 21 | data_dir: PathBuf, 22 | default_checkpoint: Vec, 23 | } 24 | 25 | #[cfg(not(target_arch = "wasm32"))] 26 | impl Database for FileDB { 27 | fn new(config: &Config) -> Result { 28 | if let Some(data_dir) = &config.data_dir { 29 | return Ok(FileDB { 30 | data_dir: data_dir.to_path_buf(), 31 | default_checkpoint: config.default_checkpoint.clone(), 32 | }); 33 | } 34 | 35 | eyre::bail!("data dir not in config") 36 | } 37 | 38 | fn save_checkpoint(&self, checkpoint: Vec) -> Result<()> { 39 | fs::create_dir_all(&self.data_dir)?; 40 | 41 | let mut f = fs::OpenOptions::new() 42 | .write(true) 43 | .create(true) 44 | .truncate(true) 45 | .open(self.data_dir.join("checkpoint"))?; 46 | 47 | f.write_all(checkpoint.as_slice())?; 48 | 49 | Ok(()) 50 | } 51 | 52 | fn load_checkpoint(&self) -> Result> { 53 | let mut buf = Vec::new(); 54 | 55 | let res = fs::OpenOptions::new() 56 | .read(true) 57 | .open(self.data_dir.join("checkpoint")) 58 | .map(|mut f| f.read_to_end(&mut buf)); 59 | 60 | if buf.len() == 32 && res.is_ok() { 61 | Ok(buf) 62 | } else { 63 | Ok(self.default_checkpoint.clone()) 64 | } 65 | } 66 | } 67 | 68 | pub struct ConfigDB { 69 | checkpoint: Vec, 70 | } 71 | 72 | impl Database for ConfigDB { 73 | fn new(config: &Config) -> Result { 74 | Ok(Self { 75 | checkpoint: config 76 | .checkpoint 77 | .clone() 78 | .unwrap_or(config.default_checkpoint.clone()), 79 | }) 80 | } 81 | 82 | fn load_checkpoint(&self) -> Result> { 83 | Ok(self.checkpoint.clone()) 84 | } 85 | 86 | fn save_checkpoint(&self, _checkpoint: Vec) -> Result<()> { 87 | Ok(()) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /benches/get_code.rs: -------------------------------------------------------------------------------- 1 | use criterion::{criterion_group, criterion_main, Criterion}; 2 | use ethers::prelude::*; 3 | use helios::types::BlockTag; 4 | use std::str::FromStr; 5 | 6 | mod harness; 7 | 8 | criterion_main!(get_code); 9 | criterion_group! { 10 | name = get_code; 11 | config = Criterion::default().sample_size(10); 12 | targets = bench_mainnet_get_code, bench_goerli_get_code 13 | } 14 | 15 | /// Benchmark mainnet get code call. 16 | /// Address: 0x00000000219ab540356cbb839cbe05303d7705fa (beacon chain deposit address) 17 | pub fn bench_mainnet_get_code(c: &mut Criterion) { 18 | c.bench_function("get_code", |b| { 19 | // Create a new multi-threaded tokio runtime. 20 | let rt = tokio::runtime::Builder::new_multi_thread() 21 | .enable_all() 22 | .build() 23 | .unwrap(); 24 | 25 | // Construct a mainnet client using our harness and tokio runtime. 26 | let client = std::sync::Arc::new(harness::construct_mainnet_client(&rt).unwrap()); 27 | 28 | // Get the beacon chain deposit contract address. 29 | let addr = Address::from_str("0x00000000219ab540356cbb839cbe05303d7705fa").unwrap(); 30 | let block = BlockTag::Latest; 31 | 32 | // Execute the benchmark asynchronously. 33 | b.to_async(rt).iter(|| async { 34 | let inner = std::sync::Arc::clone(&client); 35 | inner.get_code(&addr, block).await.unwrap() 36 | }) 37 | }); 38 | } 39 | 40 | /// Benchmark goerli get code call. 41 | /// Address: 0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6 (goerli weth) 42 | pub fn bench_goerli_get_code(c: &mut Criterion) { 43 | c.bench_function("get_code", |b| { 44 | // Create a new multi-threaded tokio runtime. 45 | let rt = tokio::runtime::Builder::new_multi_thread() 46 | .enable_all() 47 | .build() 48 | .unwrap(); 49 | 50 | // Construct a goerli client using our harness and tokio runtime. 51 | let client = std::sync::Arc::new(harness::construct_goerli_client(&rt).unwrap()); 52 | 53 | // Get the beacon chain deposit contract address. 54 | let addr = Address::from_str("0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6").unwrap(); 55 | let block = BlockTag::Latest; 56 | 57 | // Execute the benchmark asynchronously. 58 | b.to_async(rt).iter(|| async { 59 | let inner = std::sync::Arc::clone(&client); 60 | inner.get_code(&addr, block).await.unwrap() 61 | }) 62 | }); 63 | } 64 | -------------------------------------------------------------------------------- /benches/get_balance.rs: -------------------------------------------------------------------------------- 1 | use criterion::{criterion_group, criterion_main, Criterion}; 2 | use ethers::prelude::*; 3 | use helios::types::BlockTag; 4 | use std::str::FromStr; 5 | 6 | mod harness; 7 | 8 | criterion_main!(get_balance); 9 | criterion_group! { 10 | name = get_balance; 11 | config = Criterion::default().sample_size(10); 12 | targets = bench_mainnet_get_balance, bench_goerli_get_balance 13 | } 14 | 15 | /// Benchmark mainnet get balance. 16 | /// Address: 0x00000000219ab540356cbb839cbe05303d7705fa (beacon chain deposit address) 17 | pub fn bench_mainnet_get_balance(c: &mut Criterion) { 18 | c.bench_function("get_balance", |b| { 19 | // Create a new multi-threaded tokio runtime. 20 | let rt = tokio::runtime::Builder::new_multi_thread() 21 | .enable_all() 22 | .build() 23 | .unwrap(); 24 | 25 | // Construct a mainnet client using our harness and tokio runtime. 26 | let client = std::sync::Arc::new(harness::construct_mainnet_client(&rt).unwrap()); 27 | 28 | // Get the beacon chain deposit contract address. 29 | let addr = Address::from_str("0x00000000219ab540356cbb839cbe05303d7705fa").unwrap(); 30 | let block = BlockTag::Latest; 31 | 32 | // Execute the benchmark asynchronously. 33 | b.to_async(rt).iter(|| async { 34 | let inner = std::sync::Arc::clone(&client); 35 | inner.get_balance(&addr, block).await.unwrap() 36 | }) 37 | }); 38 | } 39 | 40 | /// Benchmark goerli get balance. 41 | /// Address: 0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6 (goerli weth) 42 | pub fn bench_goerli_get_balance(c: &mut Criterion) { 43 | c.bench_function("get_balance", |b| { 44 | // Create a new multi-threaded tokio runtime. 45 | let rt = tokio::runtime::Builder::new_multi_thread() 46 | .enable_all() 47 | .build() 48 | .unwrap(); 49 | 50 | // Construct a goerli client using our harness and tokio runtime. 51 | let client = std::sync::Arc::new(harness::construct_goerli_client(&rt).unwrap()); 52 | 53 | // Get the beacon chain deposit contract address. 54 | let addr = Address::from_str("0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6").unwrap(); 55 | let block = BlockTag::Latest; 56 | 57 | // Execute the benchmark asynchronously. 58 | b.to_async(rt).iter(|| async { 59 | let inner = std::sync::Arc::clone(&client); 60 | inner.get_balance(&addr, block).await.unwrap() 61 | }) 62 | }); 63 | } 64 | -------------------------------------------------------------------------------- /execution/src/rpc/mock_rpc.rs: -------------------------------------------------------------------------------- 1 | use std::{fs::read_to_string, path::PathBuf}; 2 | 3 | use async_trait::async_trait; 4 | use common::utils::hex_str_to_bytes; 5 | use ethers::types::{ 6 | transaction::eip2930::AccessList, Address, EIP1186ProofResponse, Filter, Log, Transaction, 7 | TransactionReceipt, H256, 8 | }; 9 | use eyre::{eyre, Result}; 10 | 11 | use crate::types::CallOpts; 12 | 13 | use super::ExecutionRpc; 14 | 15 | #[derive(Clone)] 16 | pub struct MockRpc { 17 | path: PathBuf, 18 | } 19 | 20 | #[cfg_attr(not(target_arch = "wasm32"), async_trait)] 21 | #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] 22 | impl ExecutionRpc for MockRpc { 23 | fn new(rpc: &str) -> Result { 24 | let path = PathBuf::from(rpc); 25 | Ok(MockRpc { path }) 26 | } 27 | 28 | fn new_mixnet(rpc: &str) -> Result 29 | where 30 | Self: Sized, 31 | { 32 | let path = PathBuf::from(rpc); 33 | Ok(MockRpc { path }) 34 | } 35 | 36 | async fn get_proof( 37 | &self, 38 | _address: &Address, 39 | _slots: &[H256], 40 | _block: u64, 41 | ) -> Result { 42 | let proof = read_to_string(self.path.join("proof.json"))?; 43 | Ok(serde_json::from_str(&proof)?) 44 | } 45 | 46 | async fn create_access_list(&self, _opts: &CallOpts, _block: u64) -> Result { 47 | Err(eyre!("not implemented")) 48 | } 49 | 50 | async fn get_code(&self, _address: &Address, _block: u64) -> Result> { 51 | let code = read_to_string(self.path.join("code.json"))?; 52 | hex_str_to_bytes(&code[0..code.len() - 1]) 53 | } 54 | 55 | async fn send_raw_transaction(&self, _bytes: &[u8]) -> Result { 56 | Err(eyre!("not implemented")) 57 | } 58 | 59 | async fn get_transaction_receipt(&self, _tx_hash: &H256) -> Result> { 60 | let receipt = read_to_string(self.path.join("receipt.json"))?; 61 | Ok(serde_json::from_str(&receipt)?) 62 | } 63 | 64 | async fn get_transaction(&self, _tx_hash: &H256) -> Result> { 65 | let tx = read_to_string(self.path.join("transaction.json"))?; 66 | Ok(serde_json::from_str(&tx)?) 67 | } 68 | 69 | async fn get_logs(&self, _filter: &Filter) -> Result> { 70 | let logs = read_to_string(self.path.join("logs.json"))?; 71 | Ok(serde_json::from_str(&logs)?) 72 | } 73 | 74 | async fn chain_id(&self) -> Result { 75 | Err(eyre!("not implemented")) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /consensus/src/utils.rs: -------------------------------------------------------------------------------- 1 | use common::{types::Bytes32, utils::bytes32_to_node}; 2 | use eyre::Result; 3 | use milagro_bls::{AggregateSignature, PublicKey}; 4 | use ssz_rs::prelude::*; 5 | 6 | use crate::types::{Header, SignatureBytes}; 7 | 8 | pub fn calc_sync_period(slot: u64) -> u64 { 9 | let epoch = slot / 32; // 32 slots per epoch 10 | epoch / 256 // 256 epochs per sync committee 11 | } 12 | 13 | pub fn is_aggregate_valid(sig_bytes: &SignatureBytes, msg: &[u8], pks: &[&PublicKey]) -> bool { 14 | let sig_res = AggregateSignature::from_bytes(sig_bytes); 15 | match sig_res { 16 | Ok(sig) => sig.fast_aggregate_verify(msg, pks), 17 | Err(_) => false, 18 | } 19 | } 20 | 21 | pub fn is_proof_valid( 22 | attested_header: &Header, 23 | leaf_object: &mut L, 24 | branch: &[Bytes32], 25 | depth: usize, 26 | index: usize, 27 | ) -> bool { 28 | let res: Result = (move || { 29 | let leaf_hash = leaf_object.hash_tree_root()?; 30 | let state_root = bytes32_to_node(&attested_header.state_root)?; 31 | let branch = branch_to_nodes(branch.to_vec())?; 32 | 33 | let is_valid = is_valid_merkle_branch(&leaf_hash, branch.iter(), depth, index, &state_root); 34 | Ok(is_valid) 35 | })(); 36 | 37 | if let Ok(is_valid) = res { 38 | is_valid 39 | } else { 40 | false 41 | } 42 | } 43 | 44 | #[derive(SimpleSerialize, Default, Debug)] 45 | struct SigningData { 46 | object_root: Bytes32, 47 | domain: Bytes32, 48 | } 49 | 50 | #[derive(SimpleSerialize, Default, Debug)] 51 | struct ForkData { 52 | current_version: Vector, 53 | genesis_validator_root: Bytes32, 54 | } 55 | 56 | pub fn compute_signing_root(object_root: Bytes32, domain: Bytes32) -> Result { 57 | let mut data = SigningData { 58 | object_root, 59 | domain, 60 | }; 61 | Ok(data.hash_tree_root()?) 62 | } 63 | 64 | pub fn compute_domain( 65 | domain_type: &[u8], 66 | fork_version: Vector, 67 | genesis_root: Bytes32, 68 | ) -> Result { 69 | let fork_data_root = compute_fork_data_root(fork_version, genesis_root)?; 70 | let start = domain_type; 71 | let end = &fork_data_root.as_bytes()[..28]; 72 | let d = [start, end].concat(); 73 | Ok(d.to_vec().try_into().unwrap()) 74 | } 75 | 76 | fn compute_fork_data_root( 77 | current_version: Vector, 78 | genesis_validator_root: Bytes32, 79 | ) -> Result { 80 | let mut fork_data = ForkData { 81 | current_version, 82 | genesis_validator_root, 83 | }; 84 | Ok(fork_data.hash_tree_root()?) 85 | } 86 | 87 | pub fn branch_to_nodes(branch: Vec) -> Result> { 88 | branch 89 | .iter() 90 | .map(bytes32_to_node) 91 | .collect::>>() 92 | } 93 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![warn(missing_debug_implementations, rust_2018_idioms, unreachable_pub)] 2 | #![deny(rustdoc::broken_intra_doc_links)] 3 | #![doc(test( 4 | no_crate_inject, 5 | attr(deny(warnings, rust_2018_idioms), allow(dead_code, unused_variables)) 6 | ))] 7 | 8 | //! # Ethereum light client written in Rust. 9 | //! 10 | //! > helios is a fully trustless, efficient, and portable Ethereum light client written in Rust. 11 | //! 12 | //! Helios converts an untrusted centralized RPC endpoint into a safe unmanipulable local RPC for its users. It syncs in seconds, requires no storage, and is lightweight enough to run on mobile devices. 13 | //! 14 | //! The entire size of Helios's binary is 13Mb and should be easy to compile into WebAssembly. This makes it a perfect target to embed directly inside wallets and dapps. 15 | //! 16 | //! ## Quickstart: `prelude` 17 | //! 18 | //! The prelude imports all the necessary data types and traits from helios. Use this to quickly bootstrap a new project. 19 | //! 20 | //! ```no_run 21 | //! # #[allow(unused)] 22 | //! use helios::prelude::*; 23 | //! ``` 24 | //! 25 | //! Examples on how you can use the types imported by the prelude can be found in 26 | //! the [`examples` directory of the repository](https://github.com/a16z/helios/tree/master/examples) 27 | //! and in the `tests/` directories of each crate. 28 | //! 29 | //! ## Breakdown of exported helios modules 30 | //! 31 | //! ### `client` 32 | //! 33 | //! The `client` module exports three main types: `Client`, `ClientBuilder`, and `FileDB`. 34 | //! 35 | //! `ClientBuilder` is a builder for the `Client` type. It allows you to configure the client using the fluent builder pattern. 36 | //! 37 | //! `Client` serves Ethereum RPC endpoints locally that call a node on the backend. 38 | //! 39 | //! Finally, the `FileDB` type is a simple local database. It is used by the `Client` to store checkpoint data. 40 | //! 41 | //! ### `config` 42 | //! 43 | //! The `config` module provides the configuration types for all of helios. It is used by the `ClientBuilder` to configure the `Client`. 44 | //! 45 | //! ### `types` 46 | //! 47 | //! Generic types used across helios. 48 | //! 49 | //! ### `errors` 50 | //! 51 | //! Errors used across helios. 52 | 53 | pub mod client { 54 | pub use client::{ 55 | database::{ConfigDB, FileDB}, 56 | Client, ClientBuilder, 57 | }; 58 | } 59 | 60 | pub mod config { 61 | pub use config::{checkpoints, networks, Config}; 62 | } 63 | 64 | pub mod types { 65 | pub use common::types::BlockTag; 66 | pub use execution::types::{Account, CallOpts, ExecutionBlock, Transactions}; 67 | } 68 | 69 | pub mod errors { 70 | pub use common::errors::*; 71 | pub use consensus::errors::*; 72 | pub use execution::errors::*; 73 | } 74 | 75 | pub mod prelude { 76 | pub use crate::client::*; 77 | pub use crate::config::*; 78 | pub use crate::errors::*; 79 | pub use crate::types::*; 80 | } 81 | -------------------------------------------------------------------------------- /nym-socks5/src/config.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2023 - Nym Technologies SA 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | use crate::error::{BackendError, Result}; 5 | use client_core::config::Config as BaseConfig; 6 | use config_common::NymConfig; 7 | use nym_socks5::client::config::Config as Socks5Config; 8 | use tap::TapFallible; 9 | 10 | #[derive(Debug)] 11 | pub struct Config { 12 | socks5: Socks5Config, 13 | } 14 | 15 | impl Config { 16 | pub fn new>(id: S, provider_mix_address: S) -> Self { 17 | Config { 18 | socks5: Socks5Config::new(id, provider_mix_address), 19 | } 20 | } 21 | 22 | pub fn get_socks5(&self) -> &Socks5Config { 23 | &self.socks5 24 | } 25 | 26 | pub fn get_base(&self) -> &BaseConfig { 27 | self.socks5.get_base() 28 | } 29 | 30 | pub fn get_base_mut(&mut self) -> &mut BaseConfig { 31 | self.socks5.get_base_mut() 32 | } 33 | 34 | pub fn init(id: &str, service_provider: &str) -> Result<()> { 35 | // use mainnet 36 | network_defaults::setup_env(None); 37 | 38 | log::info!("Initialising..."); 39 | 40 | let id = id.to_owned(); 41 | let service_provider = service_provider.to_owned(); 42 | 43 | // The client initialization was originally not written for this use case, so there are 44 | // lots of ways it can panic. Until we have proper error handling in the init code for the 45 | // clients we'll catch any panics here by spawning a new runtime in a separate thread. 46 | std::thread::spawn(move || { 47 | tokio::runtime::Runtime::new() 48 | .expect("Failed to create tokio runtime") 49 | .block_on(async move { init_socks5_config(id, service_provider).await }) 50 | }) 51 | .join() 52 | .map_err(|_| BackendError::InitializationPanic)??; 53 | 54 | log::info!("Configuration saved 🚀"); 55 | Ok(()) 56 | } 57 | } 58 | 59 | pub async fn init_socks5_config(id: String, provider_address: String) -> Result<()> { 60 | let mut config = Config::new(&id, &provider_address); 61 | 62 | if let Ok(raw_validators) = std::env::var(config_common::defaults::var_names::NYM_API) { 63 | config 64 | .get_base_mut() 65 | .set_custom_nym_apis(config_common::parse_urls(&raw_validators)); 66 | } 67 | 68 | let gateway = client_core::init::setup_gateway_from_config::( 69 | true, 70 | None, 71 | config.get_base(), 72 | false, 73 | ) 74 | .await?; 75 | 76 | config.get_base_mut().set_gateway_endpoint(gateway); 77 | config.get_base_mut().set_no_cover_traffic(); 78 | 79 | config.get_socks5().save_to_file(None).tap_err(|_| { 80 | log::error!("Failed to save the config file"); 81 | })?; 82 | 83 | let address = client_core::init::get_client_address_from_stored_keys(config.get_base())?; 84 | log::info!("The address of this client is: {}", address); 85 | Ok(()) 86 | } 87 | -------------------------------------------------------------------------------- /config/src/networks.rs: -------------------------------------------------------------------------------- 1 | use common::utils::hex_str_to_bytes; 2 | use serde::{Deserialize, Serialize}; 3 | use strum::{Display, EnumIter}; 4 | 5 | use crate::base::BaseConfig; 6 | use crate::types::{ChainConfig, Fork, Forks}; 7 | 8 | #[derive( 9 | Debug, 10 | Clone, 11 | Copy, 12 | Serialize, 13 | Deserialize, 14 | EnumIter, 15 | Display, 16 | Hash, 17 | Eq, 18 | PartialEq, 19 | PartialOrd, 20 | Ord, 21 | )] 22 | pub enum Network { 23 | MAINNET, 24 | GOERLI, 25 | } 26 | 27 | impl Network { 28 | pub fn to_base_config(&self) -> BaseConfig { 29 | match self { 30 | Self::MAINNET => mainnet(), 31 | Self::GOERLI => goerli(), 32 | } 33 | } 34 | } 35 | 36 | pub fn mainnet() -> BaseConfig { 37 | BaseConfig { 38 | default_checkpoint: hex_str_to_bytes( 39 | "0x766647f3c4e1fc91c0db9a9374032ae038778411fbff222974e11f2e3ce7dadf", 40 | ) 41 | .unwrap(), 42 | rpc_port: 8545, 43 | consensus_rpc: Some("https://www.lightclientdata.org".to_string()), 44 | chain: ChainConfig { 45 | chain_id: 1, 46 | genesis_time: 1606824023, 47 | genesis_root: hex_str_to_bytes( 48 | "0x4b363db94e286120d76eb905340fdd4e54bfe9f06bf33ff6cf5ad27f511bfe95", 49 | ) 50 | .unwrap(), 51 | }, 52 | forks: Forks { 53 | genesis: Fork { 54 | epoch: 0, 55 | fork_version: hex_str_to_bytes("0x00000000").unwrap(), 56 | }, 57 | altair: Fork { 58 | epoch: 74240, 59 | fork_version: hex_str_to_bytes("0x01000000").unwrap(), 60 | }, 61 | bellatrix: Fork { 62 | epoch: 144896, 63 | fork_version: hex_str_to_bytes("0x02000000").unwrap(), 64 | }, 65 | }, 66 | max_checkpoint_age: 1_209_600, // 14 days 67 | } 68 | } 69 | 70 | pub fn goerli() -> BaseConfig { 71 | BaseConfig { 72 | default_checkpoint: hex_str_to_bytes( 73 | "0xd4344682866dbede543395ecf5adf9443a27f423a4b00f270458e7932686ced1", 74 | ) 75 | .unwrap(), 76 | rpc_port: 8545, 77 | consensus_rpc: None, 78 | chain: ChainConfig { 79 | chain_id: 5, 80 | genesis_time: 1616508000, 81 | genesis_root: hex_str_to_bytes( 82 | "0x043db0d9a83813551ee2f33450d23797757d430911a9320530ad8a0eabc43efb", 83 | ) 84 | .unwrap(), 85 | }, 86 | forks: Forks { 87 | genesis: Fork { 88 | epoch: 0, 89 | fork_version: hex_str_to_bytes("0x00001020").unwrap(), 90 | }, 91 | altair: Fork { 92 | epoch: 36660, 93 | fork_version: hex_str_to_bytes("0x01001020").unwrap(), 94 | }, 95 | bellatrix: Fork { 96 | epoch: 112260, 97 | fork_version: hex_str_to_bytes("0x02001020").unwrap(), 98 | }, 99 | }, 100 | max_checkpoint_age: 1_209_600, // 14 days 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /rpc.md: -------------------------------------------------------------------------------- 1 | # Helios Remote Procedure Calls 2 | 3 | Helios provides a variety of RPC methods for interacting with the Ethereum network. These methods are exposed via the `Client` struct. The RPC methods follow the [Ethereum JSON RPC Spec](https://ethereum.github.io/execution-apis/api-documentation). See [examples](./examples/readme.rs) of running remote procedure calls with Helios. 4 | 5 | ## RPC Methods 6 | 7 | | RPC Method | Client Function | Description | Example | 8 | | ---------- | --------------- | ----------- | ------- | 9 | | `eth_getBalance` | `get_balance` | Returns the balance of the account given an address. | `client.get_balance(&self, address: &str, block: BlockTag)` | 10 | | `eth_getTransactionCount` | `get_nonce` | Returns the number of transactions sent from the given address. | `client.get_nonce(&self, address: &str, block: BlockTag)` | 11 | | `eth_getCode` | `get_code` | Returns the code at a given address. | `client.get_code(&self, address: &str, block: BlockTag)` | 12 | | `eth_call` | `call` | Executes a new message call immediately without creating a transaction on the blockchain. | `client.call(&self, opts: CallOpts, block: BlockTag)` | 13 | | `eth_estimateGas` | `estimate_gas` | Generates and returns an estimate of how much gas is necessary to allow the transaction to complete. | `client.estimate_gas(&self, opts: CallOpts)` | 14 | | `eth_getChainId` | `chain_id` | Returns the chain ID of the current network. | `client.chain_id(&self)` | 15 | | `eth_gasPrice` | `gas_price` | Returns the current price per gas in wei. | `client.gas_price(&self)` | 16 | | `eth_maxPriorityFeePerGas` | `max_priority_fee_per_gas` | Returns the current max priority fee per gas in wei. | `client.max_priority_fee_per_gas(&self)` | 17 | | `eth_blockNumber` | `block_number` | Returns the number of the most recent block. | `client.block_number(&self)` | 18 | | `eth_getBlockByNumber` | `get_block_by_number` | Returns the information of a block by number. | `get_block_by_number(&self, block: BlockTag, full_tx: bool)` | 19 | | `eth_getBlockByHash` | `get_block_by_hash` | Returns the information of a block by hash. | `get_block_by_hash(&self, hash: &str, full_tx: bool)` | 20 | | `eth_sendRawTransaction` | `send_raw_transaction` | Submits a raw transaction to the network. | `client.send_raw_transaction(&self, bytes: &str)` | 21 | | `eth_getTransactionReceipt` | `get_transaction_receipt` | Returns the receipt of a transaction by transaction hash. | `client.get_transaction_receipt(&self, hash: &str)` | 22 | | `eth_getLogs` | `get_logs` | Returns an array of logs matching the filter. | `client.get_logs(&self, filter: Filter)` | 23 | | `eth_getStorageAt` | `get_storage_at` | Returns the value from a storage position at a given address. | `client.get_storage_at(&self, address: &str, slot: H256, block: BlockTag)` | 24 | | `eth_getBlockTransactionCountByHash` | `get_block_transaction_count_by_hash` | Returns the number of transactions in a block from a block matching the transaction hash. | `client.get_block_transaction_count_by_hash(&self, hash: &str)` | 25 | | `eth_getBlockTransactionCountByNumber` | `get_block_transaction_count_by_number` | Returns the number of transactions in a block from a block matching the block number. | `client.get_block_transaction_count_by_number(&self, block: BlockTag)` | 26 | | `eth_coinbase` | `get_coinbase` | Returns the client coinbase address. | `client.get_coinbase(&self)` | 27 | | `eth_syncing` | `syncing` | Returns an object with data about the sync status or false. | `client.syncing(&self)` | -------------------------------------------------------------------------------- /benches/sync.rs: -------------------------------------------------------------------------------- 1 | use criterion::{criterion_group, criterion_main, Criterion}; 2 | use ethers::types::Address; 3 | use helios::types::BlockTag; 4 | 5 | mod harness; 6 | 7 | criterion_main!(sync); 8 | criterion_group! { 9 | name = sync; 10 | config = Criterion::default().sample_size(10); 11 | targets = 12 | bench_full_sync, 13 | bench_full_sync_with_call, 14 | bench_full_sync_checkpoint_fallback, 15 | bench_full_sync_with_call_checkpoint_fallback, 16 | } 17 | 18 | /// Benchmark full client sync. 19 | pub fn bench_full_sync(c: &mut Criterion) { 20 | // Externally, let's fetch the latest checkpoint from our fallback service so as not to benchmark the checkpoint fetch. 21 | let checkpoint = harness::await_future(harness::fetch_mainnet_checkpoint()).unwrap(); 22 | let checkpoint = hex::encode(checkpoint); 23 | 24 | // On client construction, it will sync to the latest checkpoint using our fetched checkpoint. 25 | c.bench_function("full_sync", |b| { 26 | b.to_async(harness::construct_runtime()).iter(|| async { 27 | let _client = std::sync::Arc::new( 28 | harness::construct_mainnet_client_with_checkpoint(&checkpoint) 29 | .await 30 | .unwrap(), 31 | ); 32 | }) 33 | }); 34 | } 35 | 36 | /// Benchmark full client sync. 37 | /// Address: 0x00000000219ab540356cbb839cbe05303d7705fa (beacon chain deposit address) 38 | pub fn bench_full_sync_with_call(c: &mut Criterion) { 39 | // Externally, let's fetch the latest checkpoint from our fallback service so as not to benchmark the checkpoint fetch. 40 | let checkpoint = harness::await_future(harness::fetch_mainnet_checkpoint()).unwrap(); 41 | let checkpoint = hex::encode(checkpoint); 42 | 43 | // On client construction, it will sync to the latest checkpoint using our fetched checkpoint. 44 | c.bench_function("full_sync_call", |b| { 45 | b.to_async(harness::construct_runtime()).iter(|| async { 46 | let client = std::sync::Arc::new( 47 | harness::construct_mainnet_client_with_checkpoint(&checkpoint) 48 | .await 49 | .unwrap(), 50 | ); 51 | let addr = "0x00000000219ab540356cbb839cbe05303d7705fa" 52 | .parse::
() 53 | .unwrap(); 54 | let block = BlockTag::Latest; 55 | client.get_balance(&addr, block).await.unwrap() 56 | }) 57 | }); 58 | } 59 | 60 | /// Benchmark full client sync with checkpoint fallback. 61 | pub fn bench_full_sync_checkpoint_fallback(c: &mut Criterion) { 62 | c.bench_function("full_sync_fallback", |b| { 63 | let rt = harness::construct_runtime(); 64 | b.iter(|| { 65 | let _client = std::sync::Arc::new(harness::construct_mainnet_client(&rt).unwrap()); 66 | }) 67 | }); 68 | } 69 | 70 | /// Benchmark full client sync with a call and checkpoint fallback. 71 | /// Address: 0x00000000219ab540356cbb839cbe05303d7705fa (beacon chain deposit address) 72 | pub fn bench_full_sync_with_call_checkpoint_fallback(c: &mut Criterion) { 73 | c.bench_function("full_sync_call", |b| { 74 | let addr = "0x00000000219ab540356cbb839cbe05303d7705fa"; 75 | let rt = harness::construct_runtime(); 76 | b.iter(|| { 77 | let client = std::sync::Arc::new(harness::construct_mainnet_client(&rt).unwrap()); 78 | harness::get_balance(&rt, client, addr).unwrap(); 79 | }) 80 | }); 81 | } 82 | -------------------------------------------------------------------------------- /config/src/config.rs: -------------------------------------------------------------------------------- 1 | use figment::{ 2 | providers::{Format, Serialized, Toml}, 3 | Figment, 4 | }; 5 | use serde::Deserialize; 6 | use std::{path::PathBuf, process::exit}; 7 | 8 | use crate::base::BaseConfig; 9 | use crate::cli::CliConfig; 10 | use crate::networks; 11 | use crate::types::{ChainConfig, Forks}; 12 | use crate::utils::{bytes_deserialize, bytes_opt_deserialize}; 13 | 14 | #[derive(Deserialize, Debug, Default)] 15 | pub struct Config { 16 | pub consensus_rpc: String, 17 | pub execution_rpc: String, 18 | pub rpc_port: Option, 19 | #[serde(deserialize_with = "bytes_deserialize")] 20 | pub default_checkpoint: Vec, 21 | #[serde(default)] 22 | #[serde(deserialize_with = "bytes_opt_deserialize")] 23 | pub checkpoint: Option>, 24 | pub data_dir: Option, 25 | pub chain: ChainConfig, 26 | pub forks: Forks, 27 | pub max_checkpoint_age: u64, 28 | pub fallback: Option, 29 | pub load_external_fallback: bool, 30 | pub strict_checkpoint_age: bool, 31 | pub use_mixnet: bool, 32 | } 33 | 34 | impl Config { 35 | pub fn from_file(config_path: &PathBuf, network: &str, cli_config: &CliConfig) -> Self { 36 | let base_config = match network { 37 | "mainnet" => networks::mainnet(), 38 | "goerli" => networks::goerli(), 39 | _ => BaseConfig::default(), 40 | }; 41 | 42 | let base_provider = Serialized::from(base_config, network); 43 | let toml_provider = Toml::file(config_path).nested(); 44 | let cli_provider = cli_config.as_provider(network); 45 | 46 | let config_res = Figment::new() 47 | .merge(base_provider) 48 | .merge(toml_provider) 49 | .merge(cli_provider) 50 | .select(network) 51 | .extract(); 52 | 53 | match config_res { 54 | Ok(config) => config, 55 | Err(err) => { 56 | match err.kind { 57 | figment::error::Kind::MissingField(field) => { 58 | let field = field.replace('_', "-"); 59 | 60 | println!("\x1b[91merror\x1b[0m: missing configuration field: {field}"); 61 | 62 | println!("\n\ttry supplying the propoper command line argument: --{field}"); 63 | 64 | println!("\talternatively, you can add the field to your helios.toml file or as an environment variable"); 65 | println!("\nfor more information, check the github README"); 66 | } 67 | _ => println!("cannot parse configuration: {err}"), 68 | } 69 | exit(1); 70 | } 71 | } 72 | } 73 | 74 | pub fn fork_version(&self, slot: u64) -> Vec { 75 | let epoch = slot / 32; 76 | 77 | if epoch >= self.forks.bellatrix.epoch { 78 | self.forks.bellatrix.fork_version.clone() 79 | } else if epoch >= self.forks.altair.epoch { 80 | self.forks.altair.fork_version.clone() 81 | } else { 82 | self.forks.genesis.fork_version.clone() 83 | } 84 | } 85 | 86 | pub fn to_base_config(&self) -> BaseConfig { 87 | BaseConfig { 88 | rpc_port: self.rpc_port.unwrap_or(8545), 89 | consensus_rpc: Some(self.consensus_rpc.clone()), 90 | default_checkpoint: self.default_checkpoint.clone(), 91 | chain: self.chain.clone(), 92 | forks: self.forks.clone(), 93 | max_checkpoint_age: self.max_checkpoint_age, 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /helios-ts/lib.ts: -------------------------------------------------------------------------------- 1 | import init, { Client } from "./pkg/index"; 2 | 3 | 4 | export async function createHeliosProvider(config: Config): Promise { 5 | const wasmData = require("./pkg/index_bg.wasm"); 6 | await init(wasmData); 7 | return new HeliosProvider(config); 8 | } 9 | 10 | /// An EIP-1193 compliant Ethereum provider. Treat this the same as you 11 | /// would window.ethereum when constructing an ethers or web3 provider. 12 | export class HeliosProvider { 13 | #client; 14 | #chainId; 15 | 16 | /// Do not use this constructor. Instead use the createHeliosProvider function. 17 | constructor(config: Config) { 18 | const executionRpc = config.executionRpc; 19 | const consensusRpc = config.consensusRpc; 20 | const checkpoint = config.checkpoint; 21 | const network = config.network ?? Network.MAINNET; 22 | 23 | this.#client = new Client(executionRpc, consensusRpc, network, checkpoint); 24 | this.#chainId = this.#client.chain_id(); 25 | } 26 | 27 | async sync() { 28 | await this.#client.sync(); 29 | } 30 | 31 | async request(req: Request): Promise { 32 | switch(req.method) { 33 | case "eth_getBalance": { 34 | return this.#client.get_balance(req.params[0], req.params[1]); 35 | }; 36 | case "eth_chainId": { 37 | return this.#chainId; 38 | }; 39 | case "eth_blockNumber": { 40 | return this.#client.get_block_number(); 41 | }; 42 | case "eth_getTransactionByHash": { 43 | let tx = await this.#client.get_transaction_by_hash(req.params[0]); 44 | return mapToObj(tx); 45 | }; 46 | case "eth_getTransactionCount": { 47 | return this.#client.get_transaction_count(req.params[0], req.params[1]); 48 | }; 49 | case "eth_getBlockTransactionCountByHash": { 50 | return this.#client.get_block_transaction_count_by_hash(req.params[0]); 51 | }; 52 | case "eth_getBlockTransactionCountByNumber": { 53 | return this.#client.get_block_transaction_count_by_number(req.params[0]); 54 | }; 55 | case "eth_getCode": { 56 | return this.#client.get_code(req.params[0], req.params[1]); 57 | }; 58 | case "eth_call": { 59 | return this.#client.call(req.params[0], req.params[1]); 60 | }; 61 | case "eth_estimateGas": { 62 | return this.#client.estimate_gas(req.params[0]); 63 | }; 64 | case "eth_gasPrice": { 65 | return this.#client.gas_price(); 66 | }; 67 | case "eth_maxPriorityFeePerGas": { 68 | return this.#client.max_priority_fee_per_gas(); 69 | }; 70 | case "eth_sendRawTransaction": { 71 | return this.#client.send_raw_transaction(req.params[0]); 72 | }; 73 | case "eth_getTransactionReceipt": { 74 | return this.#client.get_transaction_receipt(req.params[0]); 75 | }; 76 | case "eth_getLogs": { 77 | return this.#client.get_logs(req.params[0]); 78 | }; 79 | case "net_version": { 80 | return this.#chainId; 81 | }; 82 | } 83 | } 84 | } 85 | 86 | export type Config = { 87 | executionRpc: string, 88 | consensusRpc?: string, 89 | checkpoint?: string, 90 | network?: Network, 91 | } 92 | 93 | export enum Network { 94 | MAINNET = "mainnet", 95 | GOERLI = "goerli", 96 | } 97 | 98 | type Request = { 99 | method: string, 100 | params: any[], 101 | } 102 | 103 | function mapToObj(map: Map | undefined): Object | undefined { 104 | if(!map) return undefined; 105 | 106 | return Array.from(map).reduce((obj: any, [key, value]) => { 107 | obj[key] = value; 108 | return obj; 109 | }, {}); 110 | } 111 | 112 | -------------------------------------------------------------------------------- /nym-socks5/src/tasks.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2023 - Nym Technologies SA 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | use tap::TapFallible; 5 | use tokio::task::JoinHandle; 6 | 7 | use client_core::config::{ClientCoreConfigTrait, GatewayEndpointConfig}; 8 | use config_common::NymConfig; 9 | use futures::channel::mpsc; 10 | use nym_socks5::client::NymClient as Socks5NymClient; 11 | use nym_socks5::client::{config::Config as Socks5Config, Socks5ControlMessageSender}; 12 | 13 | use crate::error::Result; 14 | 15 | pub type ExitStatusReceiver = futures::channel::oneshot::Receiver; 16 | 17 | /// Status messages sent by the SOCKS5 client task to the main tauri task. 18 | #[derive(Debug)] 19 | pub enum Socks5ExitStatusMessage { 20 | /// The SOCKS5 task successfully stopped 21 | Stopped, 22 | /// The SOCKS5 task failed to start 23 | Failed(Box), 24 | } 25 | 26 | /// The main SOCKS5 client task. It loads the configuration from file determined by the `id`. 27 | pub fn start_nym_socks5_client( 28 | id: &str, 29 | ) -> Result<( 30 | Socks5ControlMessageSender, 31 | task::StatusReceiver, 32 | ExitStatusReceiver, 33 | GatewayEndpointConfig, 34 | )> { 35 | log::info!("Loading config from file: {id}"); 36 | let config = Socks5Config::load_from_file(id) 37 | .tap_err(|_| log::warn!("Failed to load configuration file"))?; 38 | let used_gateway = config.get_base().get_gateway_endpoint().clone(); 39 | 40 | let socks5_client = Socks5NymClient::new(config); 41 | log::info!("Starting socks5 client"); 42 | 43 | // Channel to send control messages to the socks5 client 44 | let (socks5_ctrl_tx, socks5_ctrl_rx) = mpsc::unbounded(); 45 | 46 | // Channel to send status update messages from the background socks5 task to the frontend. 47 | let (socks5_status_tx, socks5_status_rx) = mpsc::channel(128); 48 | 49 | // Channel to signal back to the main task when the socks5 client finishes, and why 50 | let (socks5_exit_tx, socks5_exit_rx) = futures::channel::oneshot::channel(); 51 | 52 | // Spawn a separate runtime for the socks5 client so we can forcefully terminate. 53 | // Once we can gracefully shutdown the socks5 client we can get rid of this. 54 | // The status channel is used to both get the state of the task, and if it's closed, to check 55 | // for panic. 56 | std::thread::spawn(|| { 57 | let result = tokio::runtime::Runtime::new() 58 | .expect("Failed to create runtime for SOCKS5 client") 59 | .block_on(async move { 60 | socks5_client 61 | .run_and_listen(socks5_ctrl_rx, socks5_status_tx) 62 | .await 63 | }); 64 | 65 | if let Err(err) = result { 66 | log::error!("SOCKS5 proxy failed: {err}"); 67 | socks5_exit_tx 68 | .send(Socks5ExitStatusMessage::Failed(err)) 69 | .expect("Failed to send status message back to main task"); 70 | return; 71 | } 72 | 73 | log::info!("SOCKS5 task finished"); 74 | socks5_exit_tx 75 | .send(Socks5ExitStatusMessage::Stopped) 76 | .expect("Failed to send status message back to main task"); 77 | }); 78 | 79 | Ok(( 80 | socks5_ctrl_tx, 81 | socks5_status_rx, 82 | socks5_exit_rx, 83 | used_gateway, 84 | )) 85 | } 86 | 87 | pub fn start_disconnect_listener(exit_status_receiver: ExitStatusReceiver) -> JoinHandle<()> { 88 | log::trace!("Starting disconnect listener"); 89 | tokio::spawn(async move { 90 | match exit_status_receiver.await { 91 | Ok(Socks5ExitStatusMessage::Stopped) => { 92 | log::info!("SOCKS5 task reported it has finished"); 93 | } 94 | Ok(Socks5ExitStatusMessage::Failed(err)) => { 95 | log::info!("SOCKS5 task reported error: {}", err); 96 | } 97 | Err(_) => { 98 | log::info!("SOCKS5 task appears to have stopped abruptly"); 99 | } 100 | } 101 | }) 102 | } 103 | -------------------------------------------------------------------------------- /cli/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | path::PathBuf, 3 | process::exit, 4 | str::FromStr, 5 | sync::{Arc, Mutex}, 6 | }; 7 | 8 | use clap::Parser; 9 | use common::utils::hex_str_to_bytes; 10 | use dirs::home_dir; 11 | use env_logger::Env; 12 | use eyre::Result; 13 | 14 | use client::{database::FileDB, Client, ClientBuilder}; 15 | use config::{CliConfig, Config}; 16 | use futures::executor::block_on; 17 | use log::{error, info}; 18 | 19 | #[tokio::main] 20 | async fn main() -> Result<()> { 21 | env_logger::Builder::from_env(Env::default().default_filter_or("info")).init(); 22 | 23 | let config = get_config(); 24 | let mut client = match ClientBuilder::new().config(config).build() { 25 | Ok(client) => client, 26 | Err(err) => { 27 | error!("{}", err); 28 | exit(1); 29 | } 30 | }; 31 | 32 | if let Err(err) = client.start().await { 33 | error!("{}", err); 34 | exit(1); 35 | } 36 | 37 | register_shutdown_handler(client); 38 | std::future::pending().await 39 | } 40 | 41 | fn register_shutdown_handler(client: Client) { 42 | let client = Arc::new(client); 43 | let shutdown_counter = Arc::new(Mutex::new(0)); 44 | 45 | ctrlc::set_handler(move || { 46 | let mut counter = shutdown_counter.lock().unwrap(); 47 | *counter += 1; 48 | 49 | let counter_value = *counter; 50 | 51 | if counter_value == 3 { 52 | info!("forced shutdown"); 53 | exit(0); 54 | } 55 | 56 | info!( 57 | "shutting down... press ctrl-c {} more times to force quit", 58 | 3 - counter_value 59 | ); 60 | 61 | if counter_value == 1 { 62 | let client = client.clone(); 63 | std::thread::spawn(move || { 64 | block_on(client.shutdown()); 65 | exit(0); 66 | }); 67 | } 68 | }) 69 | .expect("could not register shutdown handler"); 70 | } 71 | 72 | fn get_config() -> Config { 73 | let cli = Cli::parse(); 74 | 75 | let config_path = home_dir().unwrap().join(".helios/helios.toml"); 76 | 77 | let cli_config = cli.as_cli_config(); 78 | 79 | Config::from_file(&config_path, &cli.network, &cli_config) 80 | } 81 | 82 | #[derive(Parser)] 83 | struct Cli { 84 | #[clap(short, long, default_value = "mainnet")] 85 | network: String, 86 | #[clap(short = 'p', long, env)] 87 | rpc_port: Option, 88 | #[clap(short = 'w', long, env)] 89 | checkpoint: Option, 90 | #[clap(short, long, env)] 91 | execution_rpc: Option, 92 | #[clap(short, long, env)] 93 | consensus_rpc: Option, 94 | #[clap(short, long, env)] 95 | data_dir: Option, 96 | #[clap(short = 'f', long, env)] 97 | fallback: Option, 98 | #[clap(short = 'l', long, env)] 99 | load_external_fallback: bool, 100 | #[clap(short = 's', long, env)] 101 | strict_checkpoint_age: bool, 102 | #[clap(short = 'm', long, env)] 103 | use_mixnet: bool, 104 | } 105 | 106 | impl Cli { 107 | fn as_cli_config(&self) -> CliConfig { 108 | let checkpoint = self 109 | .checkpoint 110 | .as_ref() 111 | .map(|c| hex_str_to_bytes(c).expect("invalid checkpoint")); 112 | 113 | CliConfig { 114 | checkpoint, 115 | execution_rpc: self.execution_rpc.clone(), 116 | consensus_rpc: self.consensus_rpc.clone(), 117 | data_dir: self.get_data_dir(), 118 | rpc_port: self.rpc_port, 119 | fallback: self.fallback.clone(), 120 | load_external_fallback: self.load_external_fallback, 121 | strict_checkpoint_age: self.strict_checkpoint_age, 122 | use_mixnet: self.use_mixnet, 123 | } 124 | } 125 | 126 | fn get_data_dir(&self) -> PathBuf { 127 | if let Some(dir) = &self.data_dir { 128 | PathBuf::from_str(dir).expect("cannot find data dir") 129 | } else { 130 | home_dir() 131 | .unwrap() 132 | .join(format!(".helios/data/{}", self.network)) 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /execution/src/types.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, fmt}; 2 | 3 | use ethers::{ 4 | prelude::{Address, H256, U256}, 5 | types::Transaction, 6 | }; 7 | use eyre::Result; 8 | use serde::{ser::SerializeSeq, Deserialize, Serialize}; 9 | 10 | use common::utils::u64_to_hex_string; 11 | 12 | #[derive(Default, Debug, Clone)] 13 | pub struct Account { 14 | pub balance: U256, 15 | pub nonce: u64, 16 | pub code_hash: H256, 17 | pub code: Vec, 18 | pub storage_hash: H256, 19 | pub slots: HashMap, 20 | } 21 | 22 | #[derive(Deserialize, Serialize, Debug, Clone)] 23 | #[serde(rename_all = "camelCase")] 24 | pub struct ExecutionBlock { 25 | #[serde(serialize_with = "serialize_u64_string")] 26 | pub number: u64, 27 | pub base_fee_per_gas: U256, 28 | pub difficulty: U256, 29 | #[serde(serialize_with = "serialize_bytes")] 30 | pub extra_data: Vec, 31 | #[serde(serialize_with = "serialize_u64_string")] 32 | pub gas_limit: u64, 33 | #[serde(serialize_with = "serialize_u64_string")] 34 | pub gas_used: u64, 35 | pub hash: H256, 36 | #[serde(serialize_with = "serialize_bytes")] 37 | pub logs_bloom: Vec, 38 | pub miner: Address, 39 | pub mix_hash: H256, 40 | pub nonce: String, 41 | pub parent_hash: H256, 42 | pub receipts_root: H256, 43 | pub sha3_uncles: H256, 44 | #[serde(serialize_with = "serialize_u64_string")] 45 | pub size: u64, 46 | pub state_root: H256, 47 | #[serde(serialize_with = "serialize_u64_string")] 48 | pub timestamp: u64, 49 | #[serde(serialize_with = "serialize_u64_string")] 50 | pub total_difficulty: u64, 51 | #[serde(serialize_with = "serialize_transactions")] 52 | pub transactions: Transactions, 53 | pub transactions_root: H256, 54 | pub uncles: Vec, 55 | } 56 | 57 | #[derive(Deserialize, Serialize, Debug, Clone)] 58 | pub enum Transactions { 59 | Hashes(Vec), 60 | Full(Vec), 61 | } 62 | 63 | #[derive(Deserialize, Serialize, Clone)] 64 | #[serde(rename_all = "camelCase")] 65 | pub struct CallOpts { 66 | pub from: Option
, 67 | pub to: Address, 68 | pub gas: Option, 69 | pub gas_price: Option, 70 | pub value: Option, 71 | #[serde(default, deserialize_with = "bytes_deserialize")] 72 | pub data: Option>, 73 | } 74 | 75 | impl fmt::Debug for CallOpts { 76 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 77 | f.debug_struct("CallOpts") 78 | .field("from", &self.from) 79 | .field("to", &self.to) 80 | .field("value", &self.value) 81 | .field("data", &hex::encode(self.data.clone().unwrap_or_default())) 82 | .finish() 83 | } 84 | } 85 | 86 | fn bytes_deserialize<'de, D>(deserializer: D) -> Result>, D::Error> 87 | where 88 | D: serde::Deserializer<'de>, 89 | { 90 | let bytes: Option = serde::Deserialize::deserialize(deserializer)?; 91 | match bytes { 92 | Some(bytes) => { 93 | let bytes = hex::decode(bytes.strip_prefix("0x").unwrap()).unwrap(); 94 | Ok(Some(bytes.to_vec())) 95 | } 96 | None => Ok(None), 97 | } 98 | } 99 | 100 | fn serialize_bytes(bytes: &Vec, s: S) -> Result 101 | where 102 | S: serde::Serializer, 103 | { 104 | let bytes_str = format!("0x{}", hex::encode(bytes)); 105 | s.serialize_str(&bytes_str) 106 | } 107 | 108 | fn serialize_u64_string(x: &u64, s: S) -> Result 109 | where 110 | S: serde::Serializer, 111 | { 112 | let num_string = u64_to_hex_string(*x); 113 | s.serialize_str(&num_string) 114 | } 115 | 116 | fn serialize_transactions(txs: &Transactions, s: S) -> Result 117 | where 118 | S: serde::Serializer, 119 | { 120 | match txs { 121 | Transactions::Hashes(hashes) => { 122 | let mut seq = s.serialize_seq(Some(hashes.len()))?; 123 | for hash in hashes { 124 | seq.serialize_element(&hash)?; 125 | } 126 | 127 | seq.end() 128 | } 129 | Transactions::Full(txs) => { 130 | let mut seq = s.serialize_seq(Some(txs.len()))?; 131 | for tx in txs { 132 | seq.serialize_element(&tx)?; 133 | } 134 | 135 | seq.end() 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /benches/harness.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | use std::{str::FromStr, sync::Arc}; 4 | 5 | use ::client::{database::ConfigDB, Client}; 6 | use ethers::{ 7 | abi::Address, 8 | types::{H256, U256}, 9 | }; 10 | use helios::{client, config::networks, types::BlockTag}; 11 | 12 | /// Fetches the latest mainnet checkpoint from the fallback service. 13 | /// 14 | /// Uses the [CheckpointFallback](config::CheckpointFallback). 15 | /// The `build` method will fetch a list of [CheckpointFallbackService](config::CheckpointFallbackService)s from a community-mainained list by ethPandaOps. 16 | /// This list is NOT guaranteed to be secure, but is provided in good faith. 17 | /// The raw list can be found here: https://github.com/ethpandaops/checkpoint-sync-health-checks/blob/master/_data/endpoints.yaml 18 | pub async fn fetch_mainnet_checkpoint() -> eyre::Result { 19 | let cf = config::CheckpointFallback::new().build().await.unwrap(); 20 | cf.fetch_latest_checkpoint(&networks::Network::MAINNET) 21 | .await 22 | } 23 | 24 | /// Constructs a mainnet [Client](client::Client) for benchmark usage. 25 | /// 26 | /// Requires a [Runtime](tokio::runtime::Runtime) to be passed in by reference. 27 | /// The client is parameterized with a [FileDB](client::FileDB). 28 | /// It will also use the environment variable `MAINNET_RPC_URL` to connect to a mainnet node. 29 | /// The client will use `https://www.lightclientdata.org` as the consensus RPC. 30 | pub fn construct_mainnet_client( 31 | rt: &tokio::runtime::Runtime, 32 | ) -> eyre::Result> { 33 | rt.block_on(inner_construct_mainnet_client()) 34 | } 35 | 36 | pub async fn inner_construct_mainnet_client() -> eyre::Result> { 37 | let benchmark_rpc_url = std::env::var("MAINNET_RPC_URL")?; 38 | let mut client = client::ClientBuilder::new() 39 | .network(networks::Network::MAINNET) 40 | .consensus_rpc("https://www.lightclientdata.org") 41 | .execution_rpc(&benchmark_rpc_url) 42 | .load_external_fallback() 43 | .build()?; 44 | client.start().await?; 45 | Ok(client) 46 | } 47 | 48 | pub async fn construct_mainnet_client_with_checkpoint( 49 | checkpoint: &str, 50 | ) -> eyre::Result> { 51 | let benchmark_rpc_url = std::env::var("MAINNET_RPC_URL")?; 52 | let mut client = client::ClientBuilder::new() 53 | .network(networks::Network::MAINNET) 54 | .consensus_rpc("https://www.lightclientdata.org") 55 | .execution_rpc(&benchmark_rpc_url) 56 | .checkpoint(checkpoint) 57 | .build()?; 58 | client.start().await?; 59 | Ok(client) 60 | } 61 | 62 | /// Create a tokio multi-threaded runtime. 63 | /// 64 | /// # Panics 65 | /// 66 | /// Panics if the runtime cannot be created. 67 | pub fn construct_runtime() -> tokio::runtime::Runtime { 68 | tokio::runtime::Builder::new_multi_thread() 69 | .enable_all() 70 | .build() 71 | .unwrap() 72 | } 73 | 74 | /// Constructs a goerli client for benchmark usage. 75 | /// 76 | /// Requires a [Runtime](tokio::runtime::Runtime) to be passed in by reference. 77 | /// The client is parameterized with a [FileDB](client::FileDB). 78 | /// It will also use the environment variable `GOERLI_RPC_URL` to connect to a mainnet node. 79 | /// The client will use `http://testing.prater.beacon-api.nimbus.team` as the consensus RPC. 80 | pub fn construct_goerli_client( 81 | rt: &tokio::runtime::Runtime, 82 | ) -> eyre::Result> { 83 | rt.block_on(async { 84 | let benchmark_rpc_url = std::env::var("GOERLI_RPC_URL")?; 85 | let mut client = client::ClientBuilder::new() 86 | .network(networks::Network::GOERLI) 87 | .consensus_rpc("http://testing.prater.beacon-api.nimbus.team") 88 | .execution_rpc(&benchmark_rpc_url) 89 | .load_external_fallback() 90 | .build()?; 91 | client.start().await?; 92 | Ok(client) 93 | }) 94 | } 95 | 96 | /// Gets the balance of the given address on mainnet. 97 | pub fn get_balance( 98 | rt: &tokio::runtime::Runtime, 99 | client: Arc>, 100 | address: &str, 101 | ) -> eyre::Result { 102 | rt.block_on(async { 103 | let block = BlockTag::Latest; 104 | let address = Address::from_str(address)?; 105 | client.get_balance(&address, block).await 106 | }) 107 | } 108 | 109 | // h/t @ https://github.com/smrpn 110 | // rev: https://github.com/smrpn/casbin-rs/commit/7a0a75d8075440ee65acdac3ee9c0de6fcbd5c48 111 | pub fn await_future, T>(future: F) -> T { 112 | tokio::runtime::Runtime::new().unwrap().block_on(future) 113 | } 114 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | build-mac-arm: 8 | runs-on: macos-latest 9 | steps: 10 | - name: checkout 11 | uses: actions/checkout@v1 12 | 13 | - name: install rust nightly 14 | uses: actions-rs/toolchain@v1 15 | with: 16 | toolchain: nightly 17 | default: true 18 | override: true 19 | 20 | - name: install target 21 | run: rustup target add aarch64-apple-darwin 22 | 23 | - uses: Swatinem/rust-cache@v2 24 | 25 | - name: build 26 | run: cargo build --package cli --release --target aarch64-apple-darwin 27 | 28 | - name: archive 29 | run: gtar -czvf "helios_darwin_arm64.tar.gz" -C ./target/aarch64-apple-darwin/release helios 30 | 31 | - name: generate tag name 32 | id: tag 33 | run: | 34 | echo "::set-output name=release_tag::nightly-${GITHUB_SHA}" 35 | 36 | - name: release 37 | uses: softprops/action-gh-release@v1 38 | with: 39 | tag_name: ${{ steps.tag.outputs.release_tag }} 40 | prerelease: true 41 | files: | 42 | helios_darwin_arm64.tar.gz 43 | env: 44 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 45 | 46 | build-mac-amd: 47 | runs-on: macos-latest 48 | steps: 49 | - name: checkout 50 | uses: actions/checkout@v1 51 | 52 | - name: install rust nightly 53 | uses: actions-rs/toolchain@v1 54 | with: 55 | toolchain: nightly 56 | default: true 57 | override: true 58 | 59 | - name: install target 60 | run: rustup target add x86_64-apple-darwin 61 | 62 | - uses: Swatinem/rust-cache@v2 63 | 64 | - name: build 65 | run: cargo build --package cli --release --target x86_64-apple-darwin 66 | 67 | - name: archive 68 | run: gtar -czvf "helios_darwin_amd64.tar.gz" -C ./target/x86_64-apple-darwin/release helios 69 | 70 | - name: generate tag name 71 | id: tag 72 | run: | 73 | echo "::set-output name=release_tag::nightly-${GITHUB_SHA}" 74 | 75 | - name: release 76 | uses: softprops/action-gh-release@v1 77 | with: 78 | tag_name: ${{ steps.tag.outputs.release_tag }} 79 | prerelease: true 80 | files: | 81 | helios_darwin_amd64.tar.gz 82 | env: 83 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 84 | 85 | build-linux-arm: 86 | runs-on: ubuntu-latest 87 | steps: 88 | - name: checkout 89 | uses: actions/checkout@v1 90 | 91 | - name: install rust nightly 92 | uses: actions-rs/toolchain@v1 93 | with: 94 | toolchain: nightly 95 | default: true 96 | override: true 97 | 98 | - name: install target 99 | run: rustup target add aarch64-unknown-linux-gnu 100 | 101 | - name: install dependencies 102 | run: | 103 | sudo apt-get update -y 104 | sudo apt-get install -y gcc-aarch64-linux-gnu 105 | echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc" >> $GITHUB_ENV 106 | 107 | - uses: Swatinem/rust-cache@v2 108 | 109 | - name: build 110 | run: cargo build --package cli --release --target aarch64-unknown-linux-gnu 111 | 112 | - name: archive 113 | run: tar -czvf "helios_linux_arm64.tar.gz" -C ./target/aarch64-unknown-linux-gnu/release helios 114 | 115 | - name: generate tag name 116 | id: tag 117 | run: | 118 | echo "::set-output name=release_tag::nightly-${GITHUB_SHA}" 119 | 120 | - name: release 121 | uses: softprops/action-gh-release@v1 122 | with: 123 | tag_name: ${{ steps.tag.outputs.release_tag }} 124 | prerelease: true 125 | files: | 126 | helios_linux_arm64.tar.gz 127 | env: 128 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 129 | 130 | build-linux-amd: 131 | runs-on: ubuntu-latest 132 | steps: 133 | - name: checkout 134 | uses: actions/checkout@v1 135 | 136 | - name: install rust nightly 137 | uses: actions-rs/toolchain@v1 138 | with: 139 | toolchain: nightly 140 | default: true 141 | override: true 142 | 143 | - name: install target 144 | run: rustup target add x86_64-unknown-linux-gnu 145 | 146 | - uses: Swatinem/rust-cache@v2 147 | 148 | - name: build 149 | run: cargo build --package cli --release --target x86_64-unknown-linux-gnu 150 | 151 | - name: archive 152 | run: tar -czvf "helios_linux_amd64.tar.gz" -C ./target/x86_64-unknown-linux-gnu/release helios 153 | 154 | - name: generate tag name 155 | id: tag 156 | run: | 157 | echo "::set-output name=release_tag::nightly-${GITHUB_SHA}" 158 | 159 | - name: release 160 | uses: softprops/action-gh-release@v1 161 | with: 162 | tag_name: ${{ steps.tag.outputs.release_tag }} 163 | prerelease: true 164 | files: | 165 | helios_linux_amd64.tar.gz 166 | env: 167 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 168 | -------------------------------------------------------------------------------- /execution/src/rpc/http_rpc.rs: -------------------------------------------------------------------------------- 1 | use std::str::FromStr; 2 | 3 | use async_trait::async_trait; 4 | use ethers::prelude::{Address, Http}; 5 | use ethers::providers::{HttpRateLimitRetryPolicy, Middleware, Provider, RetryClient}; 6 | use ethers::types::transaction::eip2718::TypedTransaction; 7 | use ethers::types::transaction::eip2930::AccessList; 8 | use ethers::types::{ 9 | BlockId, Bytes, EIP1186ProofResponse, Eip1559TransactionRequest, Filter, Log, Transaction, 10 | TransactionReceipt, H256, U256, 11 | }; 12 | use eyre::Result; 13 | 14 | use crate::types::CallOpts; 15 | use common::errors::RpcError; 16 | 17 | use super::ExecutionRpc; 18 | 19 | pub struct HttpRpc { 20 | url: String, 21 | provider: Provider>, 22 | } 23 | 24 | impl Clone for HttpRpc { 25 | fn clone(&self) -> Self { 26 | Self::new(&self.url).unwrap() 27 | } 28 | } 29 | 30 | #[cfg_attr(not(target_arch = "wasm32"), async_trait)] 31 | #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] 32 | impl ExecutionRpc for HttpRpc { 33 | fn new(rpc: &str) -> Result { 34 | let http = Http::from_str(rpc)?; 35 | let mut client = RetryClient::new(http, Box::new(HttpRateLimitRetryPolicy), 100, 50); 36 | client.set_compute_units(300); 37 | 38 | let provider = Provider::new(client); 39 | 40 | Ok(HttpRpc { 41 | url: rpc.to_string(), 42 | provider, 43 | }) 44 | } 45 | 46 | fn new_mixnet(rpc: &str) -> Result 47 | where 48 | Self: Sized, 49 | { 50 | let url = reqwest::Url::parse(rpc)?; 51 | let client = reqwest::Client::builder() 52 | .proxy(reqwest::Proxy::all("socks5h://127.0.0.1:1080").unwrap()) 53 | .build() 54 | .unwrap(); 55 | let http = Http::new_with_client(url, client); 56 | let mut client = RetryClient::new(http, Box::new(HttpRateLimitRetryPolicy), 100, 50); 57 | client.set_compute_units(300); 58 | 59 | let provider = Provider::new(client); 60 | 61 | Ok(HttpRpc { 62 | url: rpc.to_string(), 63 | provider, 64 | }) 65 | } 66 | 67 | async fn get_proof( 68 | &self, 69 | address: &Address, 70 | slots: &[H256], 71 | block: u64, 72 | ) -> Result { 73 | let block = Some(BlockId::from(block)); 74 | let proof_response = self 75 | .provider 76 | .get_proof(*address, slots.to_vec(), block) 77 | .await 78 | .map_err(|e| RpcError::new("get_proof", e))?; 79 | 80 | Ok(proof_response) 81 | } 82 | 83 | async fn create_access_list(&self, opts: &CallOpts, block: u64) -> Result { 84 | let block = Some(BlockId::from(block)); 85 | 86 | let mut raw_tx = Eip1559TransactionRequest::new(); 87 | raw_tx.to = Some(opts.to.into()); 88 | raw_tx.from = opts.from; 89 | raw_tx.value = opts.value; 90 | raw_tx.gas = Some(opts.gas.unwrap_or(U256::from(100_000_000))); 91 | raw_tx.max_fee_per_gas = Some(U256::zero()); 92 | raw_tx.max_priority_fee_per_gas = Some(U256::zero()); 93 | raw_tx.data = opts 94 | .data 95 | .as_ref() 96 | .map(|data| Bytes::from(data.as_slice().to_owned())); 97 | 98 | let tx = TypedTransaction::Eip1559(raw_tx); 99 | let list = self 100 | .provider 101 | .create_access_list(&tx, block) 102 | .await 103 | .map_err(|e| RpcError::new("create_access_list", e))?; 104 | 105 | Ok(list.access_list) 106 | } 107 | 108 | async fn get_code(&self, address: &Address, block: u64) -> Result> { 109 | let block = Some(BlockId::from(block)); 110 | let code = self 111 | .provider 112 | .get_code(*address, block) 113 | .await 114 | .map_err(|e| RpcError::new("get_code", e))?; 115 | 116 | Ok(code.to_vec()) 117 | } 118 | 119 | async fn send_raw_transaction(&self, bytes: &[u8]) -> Result { 120 | let bytes = Bytes::from(bytes.to_owned()); 121 | let tx = self 122 | .provider 123 | .send_raw_transaction(bytes) 124 | .await 125 | .map_err(|e| RpcError::new("send_raw_transaction", e))?; 126 | 127 | Ok(tx.tx_hash()) 128 | } 129 | 130 | async fn get_transaction_receipt(&self, tx_hash: &H256) -> Result> { 131 | let receipt = self 132 | .provider 133 | .get_transaction_receipt(*tx_hash) 134 | .await 135 | .map_err(|e| RpcError::new("get_transaction_receipt", e))?; 136 | 137 | Ok(receipt) 138 | } 139 | 140 | async fn get_transaction(&self, tx_hash: &H256) -> Result> { 141 | Ok(self 142 | .provider 143 | .get_transaction(*tx_hash) 144 | .await 145 | .map_err(|e| RpcError::new("get_transaction", e))?) 146 | } 147 | 148 | async fn get_logs(&self, filter: &Filter) -> Result> { 149 | Ok(self 150 | .provider 151 | .get_logs(filter) 152 | .await 153 | .map_err(|e| RpcError::new("get_logs", e))?) 154 | } 155 | 156 | async fn chain_id(&self) -> Result { 157 | Ok(self 158 | .provider 159 | .get_chainid() 160 | .await 161 | .map_err(|e| RpcError::new("chain_id", e))? 162 | .as_u64()) 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /consensus/src/rpc/nimbus_rpc.rs: -------------------------------------------------------------------------------- 1 | use async_trait::async_trait; 2 | use eyre::Result; 3 | use std::cmp; 4 | 5 | use super::ConsensusRpc; 6 | use crate::constants::MAX_REQUEST_LIGHT_CLIENT_UPDATES; 7 | use crate::types::*; 8 | use common::errors::RpcError; 9 | 10 | #[derive(Debug)] 11 | pub struct NimbusRpc { 12 | rpc: String, 13 | client: reqwest::Client, 14 | } 15 | 16 | #[cfg_attr(not(target_arch = "wasm32"), async_trait)] 17 | #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] 18 | impl ConsensusRpc for NimbusRpc { 19 | fn new(rpc: &str) -> Self { 20 | let client = reqwest::Client::new(); 21 | NimbusRpc { 22 | rpc: rpc.to_string(), 23 | client, 24 | } 25 | } 26 | 27 | fn new_mixnet(rpc: &str) -> Self { 28 | let client = reqwest::Client::builder() 29 | .proxy(reqwest::Proxy::all("socks5h://127.0.0.1:1080").unwrap()) 30 | .build() 31 | .unwrap(); 32 | NimbusRpc { 33 | rpc: rpc.to_string(), 34 | client, 35 | } 36 | } 37 | 38 | async fn get_bootstrap(&self, block_root: &'_ [u8]) -> Result { 39 | let root_hex = hex::encode(block_root); 40 | let req = format!( 41 | "{}/eth/v1/beacon/light_client/bootstrap/0x{}", 42 | self.rpc, root_hex 43 | ); 44 | 45 | let res = self 46 | .client 47 | .get(req) 48 | .send() 49 | .await 50 | .map_err(|e| RpcError::new("bootstrap", e))? 51 | .json::() 52 | .await 53 | .map_err(|e| RpcError::new("bootstrap", e))?; 54 | 55 | Ok(res.data) 56 | } 57 | 58 | async fn get_updates(&self, period: u64, count: u8) -> Result> { 59 | let count = cmp::min(count, MAX_REQUEST_LIGHT_CLIENT_UPDATES); 60 | let req = format!( 61 | "{}/eth/v1/beacon/light_client/updates?start_period={}&count={}", 62 | self.rpc, period, count 63 | ); 64 | 65 | let res = self 66 | .client 67 | .get(req) 68 | .send() 69 | .await 70 | .map_err(|e| RpcError::new("updates", e))? 71 | .json::() 72 | .await 73 | .map_err(|e| RpcError::new("updates", e))?; 74 | 75 | Ok(res.iter().map(|d| d.data.clone()).collect()) 76 | } 77 | 78 | async fn get_finality_update(&self) -> Result { 79 | let req = format!("{}/eth/v1/beacon/light_client/finality_update", self.rpc); 80 | let res = self 81 | .client 82 | .get(req) 83 | .send() 84 | .await 85 | .map_err(|e| RpcError::new("finality_update", e))? 86 | .json::() 87 | .await 88 | .map_err(|e| RpcError::new("finality_update", e))?; 89 | 90 | Ok(res.data) 91 | } 92 | 93 | async fn get_optimistic_update(&self) -> Result { 94 | let req = format!("{}/eth/v1/beacon/light_client/optimistic_update", self.rpc); 95 | let res = self 96 | .client 97 | .get(req) 98 | .send() 99 | .await 100 | .map_err(|e| RpcError::new("optimistic_update", e))? 101 | .json::() 102 | .await 103 | .map_err(|e| RpcError::new("optimistic_update", e))?; 104 | 105 | Ok(res.data) 106 | } 107 | 108 | async fn get_block(&self, slot: u64) -> Result { 109 | let req = format!("{}/eth/v2/beacon/blocks/{}", self.rpc, slot); 110 | let res = self 111 | .client 112 | .get(req) 113 | .send() 114 | .await 115 | .map_err(|e| RpcError::new("blocks", e))? 116 | .json::() 117 | .await 118 | .map_err(|e| RpcError::new("blocks", e))?; 119 | 120 | Ok(res.data.message) 121 | } 122 | 123 | async fn chain_id(&self) -> Result { 124 | let req = format!("{}/eth/v1/config/spec", self.rpc); 125 | let res = self 126 | .client 127 | .get(req) 128 | .send() 129 | .await 130 | .map_err(|e| RpcError::new("spec", e))? 131 | .json::() 132 | .await 133 | .map_err(|e| RpcError::new("spec", e))?; 134 | 135 | Ok(res.data.chain_id) 136 | } 137 | } 138 | 139 | #[derive(serde::Deserialize, Debug)] 140 | struct BeaconBlockResponse { 141 | data: BeaconBlockData, 142 | } 143 | 144 | #[derive(serde::Deserialize, Debug)] 145 | struct BeaconBlockData { 146 | message: BeaconBlock, 147 | } 148 | 149 | type UpdateResponse = Vec; 150 | 151 | #[derive(serde::Deserialize, Debug)] 152 | struct UpdateData { 153 | data: Update, 154 | } 155 | 156 | #[derive(serde::Deserialize, Debug)] 157 | struct FinalityUpdateResponse { 158 | data: FinalityUpdate, 159 | } 160 | 161 | #[derive(serde::Deserialize, Debug)] 162 | struct OptimisticUpdateResponse { 163 | data: OptimisticUpdate, 164 | } 165 | 166 | #[derive(serde::Deserialize, Debug)] 167 | struct BootstrapResponse { 168 | data: Bootstrap, 169 | } 170 | 171 | #[derive(serde::Deserialize, Debug)] 172 | struct SpecResponse { 173 | data: Spec, 174 | } 175 | 176 | #[derive(serde::Deserialize, Debug)] 177 | struct Spec { 178 | #[serde(rename = "DEPOSIT_NETWORK_ID", deserialize_with = "u64_deserialize")] 179 | chain_id: u64, 180 | } 181 | -------------------------------------------------------------------------------- /config.md: -------------------------------------------------------------------------------- 1 | ## Helios Configuration 2 | 3 | All configuration options can be set on a per-network level in `~/.helios/helios.toml`. 4 | 5 | #### Comprehensive Example 6 | 7 | ```toml 8 | [mainnet] 9 | # The consensus rpc to use. This should be a trusted rpc endpoint. Defaults to "https://www.lightclientdata.org". 10 | consensus_rpc = "https://www.lightclientdata.org" 11 | # [REQUIRED] The execution rpc to use. This should be a trusted rpc endpoint. 12 | execution_rpc = "https://eth-mainnet.g.alchemy.com/v2/XXXXX" 13 | # The port to run the JSON-RPC server on. By default, Helios will use port 8545. 14 | rpc_port = 8545 15 | # The latest checkpoint. This should be a trusted checkpoint that is no greater than ~2 weeks old. 16 | # If you are unsure what checkpoint to use, you can skip this option and set either `load_external_fallback` or `fallback` values (described below) to fetch a checkpoint. Though this is not recommended and less secure. 17 | checkpoint = "0x85e6151a246e8fdba36db27a0c7678a575346272fe978c9281e13a8b26cdfa68" 18 | # The directory to store the checkpoint database in. If not provided, Helios will use "~/.helios/data/mainnet", where `mainnet` is the network. 19 | # It is recommended to set this directory to a persistent location mapped to a fast storage device. 20 | data_dir = "/home/user/.helios/mainnet" 21 | # The maximum age of a checkpoint in seconds. If the checkpoint is older than this, Helios will attempt to fetch a new checkpoint. 22 | max_checkpoint_age = 86400 23 | # A checkpoint fallback is used if no checkpoint is provided or the given checkpoint is too old. 24 | # This is expected to be a trusted checkpoint sync api (like provided in https://github.com/ethpandaops/checkpoint-sync-health-checks/blob/master/_data/endpoints.yaml). 25 | fallback = "https://sync-mainnet.beaconcha.in" 26 | # If no checkpoint is provided, or the checkpoint is too old, Helios will attempt to dynamically fetch a checkpoint from a maintained list of checkpoint sync apis. 27 | # NOTE: This is an insecure feature and not recommended for production use. Checkpoint manipulation is possible. 28 | load_external_fallback = true 29 | 30 | [goerli] 31 | # The consensus rpc to use. This should be a trusted rpc endpoint. Defaults to Nimbus testnet. 32 | consensus_rpc = "http://testing.prater.beacon-api.nimbus.team" 33 | # [REQUIRED] The execution rpc to use. This should be a trusted rpc endpoint. 34 | execution_rpc = "https://eth-goerli.g.alchemy.com/v2/XXXXX" 35 | # The port to run the JSON-RPC server on. By default, Helios will use port 8545. 36 | rpc_port = 8545 37 | # The latest checkpoint. This should be a trusted checkpoint that is no greater than ~2 weeks old. 38 | # If you are unsure what checkpoint to use, you can skip this option and set either `load_external_fallback` or `fallback` values (described below) to fetch a checkpoint. Though this is not recommended and less secure. 39 | checkpoint = "0xb5c375696913865d7c0e166d87bc7c772b6210dc9edf149f4c7ddc6da0dd4495" 40 | # The directory to store the checkpoint database in. If not provided, Helios will use "~/.helios/data/goerli", where `goerli` is the network. 41 | # It is recommended to set this directory to a persistent location mapped to a fast storage device. 42 | data_dir = "/home/user/.helios/goerli" 43 | # The maximum age of a checkpoint in seconds. If the checkpoint is older than this, Helios will attempt to fetch a new checkpoint. 44 | max_checkpoint_age = 86400 45 | # A checkpoint fallback is used if no checkpoint is provided or the given checkpoint is too old. 46 | # This is expected to be a trusted checkpoint sync api (like provided in https://github.com/ethpandaops/checkpoint-sync-health-checks/blob/master/_data/endpoints.yaml). 47 | fallback = "https://sync-goerli.beaconcha.in" 48 | # If no checkpoint is provided, or the checkpoint is too old, Helios will attempt to dynamically fetch a checkpoint from a maintained list of checkpoint sync apis. 49 | # NOTE: This is an insecure feature and not recommended for production use. Checkpoint manipulation is possible. 50 | load_external_fallback = true 51 | ``` 52 | 53 | 54 | #### Options 55 | 56 | All configuration options below are available on a per-network level, where network is specified by a header (eg `[mainnet]` or `[goerli]`). Many of these options can be configured through cli flags as well. See [README.md](./README.md#additional-options) or run `helios --help` for more information. 57 | 58 | 59 | - `consensus_rpc` - The URL of the consensus RPC endpoint used to fetch the latest beacon chain head and sync status. This must be a consenus node that supports the light client beaconchain api. We recommend using Nimbus for this. If no consensus rpc is supplied, it defaults to `https://www.lightclientdata.org` which is run by us. 60 | 61 | - `execution_rpc` - The URL of the execution RPC endpoint used to fetch the latest execution chain head and sync status. This must be an execution node that supports the light client execution api. We recommend using Geth for this. 62 | 63 | - `rpc_port` - The port to run the JSON-RPC server on. By default, Helios will use port 8545. 64 | 65 | - `checkpoint` - The latest checkpoint. This should be a trusted checkpoint that is no greater than ~2 weeks old. If you are unsure what checkpoint to use, you can skip this option and set either `load_external_fallback` or `fallback` values (described below) to fetch a checkpoint. Though this is not recommended and less secure. 66 | 67 | - `data_dir` - The directory to store the checkpoint database in. If not provided, Helios will use "~/.helios/data/", where `` is the network. It is recommended to set this directory to a persistent location mapped to a fast storage device. 68 | 69 | - `max_checkpoint_age` - The maximum age of a checkpoint in seconds. If the checkpoint is older than this, Helios will attempt to fetch a new checkpoint. 70 | 71 | - `fallback` - A checkpoint fallback is used if no checkpoint is provided or the given checkpoint is too old. This is expected to be a trusted checkpoint sync api (eg https://sync-mainnet.beaconcha.in). An extensive list of checkpoint sync apis can be found here: https://github.com/ethpandaops/checkpoint-sync-health-checks/blob/master/_data/endpoints.yaml. 72 | 73 | - `load_external_fallback` - If no checkpoint is provided, or the checkpoint is too old, Helios will attempt to dynamically fetch a checkpoint from a maintained list of checkpoint sync apis. NOTE: This is an insecure feature and not recommended for production use. Checkpoint manipulation is possible. 74 | 75 | -------------------------------------------------------------------------------- /execution/testdata/proof.json: -------------------------------------------------------------------------------- 1 | { 2 | "address": "0x14f9d4af749609c1438528c0cce1cc3f6d411c47", 3 | "accountProof": [ 4 | "0xf90211a071e881b8a14ffdfd37073cbfbcb6bb2c9231d0559440027fd00f0e8a2235aa34a0e84ac559924013eabff34253b21d302c3c30ad0496cae6a1ae2d93171f0ee2d7a0a6ef9fb82ab93a5ba0153998b19aaf272a7050506d8c3405964296d5f10b8d6aa04c95a5d292a544afefc594a747620e39eb172f8ba9bd1082668e0843cffefeefa0815f25eddd6abf24135df25e695fbe45567a1a8ee9bb83df9482b9e1ac5527dba0b9234f356908b44cc9ed4171894f6f40f4122fda4e5f5c82e984deec601a87fda0fe947db5baa6c4ccc8655f3cd3631c220f8dc07aafa44d2147be5d2ab21af555a0c75c86ef435e5d58f8161ca09e838b53af78e0b81f17ab67c702dcf465c48f08a05d4eb9221936f698f2338f560ead42b115a6dd409b15108579f082773166b0b4a052ac45eb7d301b373044ead47d2d4ee6f2313979540bca0ae798c0ccb3e61421a0b7433a507e6ffeef9624872b21a73c3fd5fb3d8f781d28da44b2bedda5208d5aa0522aa006cfd459900bf06651a03b426e6fcfd5532e093a807641bc10bbae9691a06f8fe64c907d1d2b75e411858e701c263a0622ef9335ae1433ea9e3bdf9d8315a024e3319b5c09742a511fc2d342eeaa1f1eb41a2a1c78f7a1ac9dab1bbd65f0d2a03bf8e1b5ced29303bb0045bf8b4fc6ae52f07d3136620d296901f494d7eaeebda0071e94edd3d657ad6f01ac0ca870dd174d48b904d4767906658c1db62fd716b780", 5 | "0xf90211a0373c65c436fc03fe9fe3bc74631719de2de9ee93143830404f74f6d55ee8083da0e56bb1ef97ce46a4dfd53142a5f51986ce2103d68c2f609650173a65a5d74d03a040b5dac451091440dfd30276ad1bef64aafd51eebdda7071c8bd1337a606de19a027f8fadfbf130af518f236e2e337746585cc4d321f745d4caa24f1dc00676b05a00e5c404b0a407dbdd598ab842048fb294c910ef39f390adbc32bce46b2e34a71a0e43291d8b64138dd133accade91bd6a1a10b5bee1b336817f03ff375a17d7b1ca064d673999038d81f854510ad959267827e3bcd2841bae4ed3ad4e80c227acaf0a0c023b60b354c91de5144503087c0e47a8053a4ae94b81bb0201f4840cdc89ff6a0975eaf9e2c261da99052309b82ff2bd7675b005da56efa0c0efc341e3b68ed0fa088d9a8a786c5ed346781f0d470b90b82f0c7cac8c070959859fe3aba8e906b5ca0e8697941f517a90668257af41b975c0f1cdedc9762c3585e27b2eaec7eb41c4ca0f72a74ba3efd9e397daaa4ec939865e250978947b69e7069e026916201945ee4a07242ec29142d129f6927395d6ce481405f85d1453344529d99f98e20272fbc1aa06f33fb0bd4c80da83e3e85010cfeed3b19a665157e19806086d34c3b11846e67a0a4464bf01a4296d323821535817b20db9986f1ebcbcedd84803a09c53f4f0588a0e1d393beb40edab4c0aa74c566807f295c33a4a58f91d27f705b289fe41a51c980", 6 | "0xf90211a0d8a32a972ccc40d028cfe098181fd498491d3205dedf45212119a40263608d79a0eba342773c0e6d2968acfcd885c54d5ab37bd5f85188fe46c764ed0e18f58350a0b94b1708163349d0281a4815d8320241fca4184f270f2ac5fcecd0f6eae9e3b1a043ddbf76e40f7cf55dcc248ee64bb3097e27553737ac53984d783f055b9caf3ea0843d7e62a73d0027d096fbb66e0f469043d4115ee22b1f28b5f1137a26ca8cdba0f015b37db0287d99cf919af2143e2d11302de8a679e21c1c5c66f5087158aa3da04d3464647b9bd42b67d4285446559e2bf719883a80526b18f79330898906b5fba0f6f7ab5218a48cc0e204b3fb68dbddf7ac79b168f0325ca915bb3ed1a3130be2a08910831666244e6983919fee3c477826f08255edee9351a9f865df1fc1c8d40ca04903a704c077aac79e59b5c44c3b79e3f6716015309a29b069b55453d3d15773a0b2ea8736d58291c8c31dbc20185702717f8c533b656fa1f570b4c52030c05479a0d04ddbbd951b8e5f37c5819e242d6d341bf3239519b8d4e1f874fa2de3082a4aa0397a19edf6b5f64a9b04d1428fd5ac953178c23510e3dcf50bb9614e556c5cbfa0bfb66a915ec5bc5344c3c7282ee62e94234664090763a1dab43a3f20e6199b31a01c2f46a9fc0f12c1a5f1f7ce20cac8d2827ee82b6e2acbea4c9628f81b1d130da05222b1d0263ac9bad858462ef349e6347b04167d5f2a62400332fcc18a595f2d80", 7 | "0xf90211a0060b9dbbd560313dfa11d86e56f8117d76130d197f08a796770fcaca646b7e1fa0cbb0b6eb9383b11c038ff8a9d30df57042e819c6e4e83666486a9fc094ee3ec5a049de57ac42fa5258213ccff70ef43d6bba4948e0dbfc5851a42ca84c1255cc23a0957940bcb472f09053fbda2f73c59868dfd4a865dad792a1bc5dd7957f8f5376a05b1bde3ba31401a9a93e10156d06117aaacc4f9f56a0441f10ba8848f8f954a8a0cfa085c19fcc9e290c999abc7209e469d76a4115219d87e8f361d15829fe2d53a0849e508de302c48ce95e7ae5ff20969bccdd36e69c9e11f25fd9a2dd16b36319a03a56898360443bb212bd44bb6ffda43dc36912f6c7cfd9a4bb39e58c936270f9a068842c2394fcc47c2e87802e88aa1465d37f6e97828e1df33d1178e6b14692c3a0b052b4c48baff415ca25c11b9825696404226ec65bbfa0bbc214d61984e5aceea02bfdb2153896c75a21927912d7e422d811f30cdf0edbcb6cdf955398781d6b45a06e932ff247483a4dc084d3860a29ee16040b8002b564405bb9db6045f988d61da074734042a667e98e49ea665b5a90109a8fca222f0c0813d0ff6bae785497cd60a04ad321ecd7939bb4b754ee3b76fe1b06efd3a6d583fccf2c3049cfcfb7d27473a0890dbb8238beb7875f636725343cc47214582fb5e0c0937920815a37339a6960a0469b936445ed7be59190a11c49e1198d7b584d5e59d62d793265fbe8ce0a937480", 8 | "0xf90211a01a991ab74e62008f16acbf7fbe7119469aea98f0b05308d279da5aa897fb9ce5a0dcd7d6ac32c17dda96d77f14115771757d9b3245bda0a1d457d3352b616437f9a0610fa0acd1afcc1b3209d9e561a619d729cfda4bfa639bf4108f63fd6666340aa0539fc55db43500d6b2abd40a064fc800fe814522921d2528df7afd8e80670b08a08065e71d206bf24e631661ecc0fb1351fc65b851f66a1fcf25b882fbccd69d8ba0e3c85725c51e251bf16bd8ddf396613454175833ecc083a20794a71a1b0ce91fa06768a52005a756d65276e791abbc6e64f0974044a4a1bfc0effd8e660778447ca05d1061535cdb4e2775810401d5f9d5f78765e1669ba2a3c003aae461466bb253a01e6f9ab043ac334d887f6c92b358acdc64709cbd725dedf53159b71ae1bdebfda0c799a3e48366551e882f3a0d494a3d9438def0a618089c2922da38f6fd9031c0a0019fa068a9bd23bdab5982bc0da260e11abdf4164445f67945ffd2512a74e27ba0df54f3672852d15d637ecf05a465c7213bfaea546dcad7a3653ba474067fe248a08f28bdc021a4753466a2c0e77cd5ac412c6863155f03a97ae9e2de7dd27edf8da035ae5bb7ee628fcc2bf3bbe12bbe4b5f5b1e5a6a4fdd5f7752dcd6b27d57ec77a07dc8f59a0a12dfbf045ff82165af0101b32e5cbf3d7ab2903a21919b9978efb7a069232faca7617690aa62a02344d2a2d54b8529d963d9f7c95a02ea3519cd41da80", 9 | "0xf8f18080a00c668cef94cedbc21f3f2ff5795442f424c67c3bcbd300328660d8f2e6543b908080a05c86dc654307d6f084bc46ee572324e89bb3965211cf0216f03a5511f179d2daa0d6cabba9da3391114dbe866d21cd6bfde3496bb8f69786b1b1fafa224795226ba0d6d362d0161e94d6787b2165d61bf0ccc1b51c351b6178b647d1df2e1be69274a01338e1219a75007137581b7299d61cd66d74d3e7eed44e73cf166033c0030ed380a06b8cb01c204ddf7ae87ae9531ab3713d7679a630496e79c127c5d4662823d724a04d29b6aac81dfac338997a2a66a9338531732efb7f32ffb2a81df61a2291f8f78080808080", 10 | "0xf851808080808080808080808080a032914585f84a102eefeca772a4f807beb731bfbc6b2c407c7685d0e1a294e6dba07784d7e02bf6c6a29570c630c76231a4ee251ba305f57148c1e140c1dcfe88d5808080", 11 | "0xf86c9d3801d9ae5e62cc3f6bcde556599e2fe63f1fadf1dc7318cf0ae6482f84b84cf84a0186048c27395000a0f98486b87474ffe02e56b9e4b45558b99928e4c67bcd517918849e77bd68d113a0c6ca0679d7242fa080596f2fe2e6b172d9b927a6b52278343826e33745854327" 12 | ], 13 | "balance": "0x48c27395000", 14 | "codeHash": "0xc6ca0679d7242fa080596f2fe2e6b172d9b927a6b52278343826e33745854327", 15 | "nonce": "0x1", 16 | "storageHash": "0xf98486b87474ffe02e56b9e4b45558b99928e4c67bcd517918849e77bd68d113", 17 | "storageProof": [] 18 | } 19 | -------------------------------------------------------------------------------- /helios-ts/src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate console_error_panic_hook; 2 | extern crate web_sys; 3 | 4 | use std::str::FromStr; 5 | 6 | use common::types::BlockTag; 7 | use ethers::types::{Address, Filter, H256}; 8 | use execution::types::CallOpts; 9 | use wasm_bindgen::prelude::*; 10 | 11 | use client::database::ConfigDB; 12 | use config::{networks, Config}; 13 | 14 | #[allow(unused_macros)] 15 | macro_rules! log { 16 | ( $( $t:tt )* ) => { 17 | web_sys::console::log_1(&format!( $( $t )* ).into()); 18 | } 19 | } 20 | 21 | #[wasm_bindgen] 22 | pub struct Client { 23 | inner: client::Client, 24 | chain_id: u64, 25 | } 26 | 27 | #[wasm_bindgen] 28 | impl Client { 29 | #[wasm_bindgen(constructor)] 30 | pub fn new( 31 | execution_rpc: String, 32 | consensus_rpc: Option, 33 | network: String, 34 | checkpoint: Option, 35 | ) -> Self { 36 | console_error_panic_hook::set_once(); 37 | 38 | let base = match network.as_str() { 39 | "mainnet" => networks::mainnet(), 40 | "goerli" => networks::goerli(), 41 | _ => panic!("invalid network"), 42 | }; 43 | 44 | let chain_id = base.chain.chain_id; 45 | 46 | let checkpoint = Some( 47 | checkpoint 48 | .as_ref() 49 | .map(|c| c.strip_prefix("0x").unwrap_or(c.as_str())) 50 | .map(|c| hex::decode(c).unwrap()) 51 | .unwrap_or(base.default_checkpoint), 52 | ); 53 | 54 | let consensus_rpc = consensus_rpc.unwrap_or(base.consensus_rpc.unwrap()); 55 | 56 | let config = Config { 57 | execution_rpc, 58 | consensus_rpc, 59 | checkpoint, 60 | 61 | chain: base.chain, 62 | forks: base.forks, 63 | 64 | ..Default::default() 65 | }; 66 | 67 | let inner: client::Client = 68 | client::ClientBuilder::new().config(config).build().unwrap(); 69 | 70 | Self { inner, chain_id } 71 | } 72 | 73 | #[wasm_bindgen] 74 | pub async fn sync(&mut self) { 75 | self.inner.start().await.unwrap() 76 | } 77 | 78 | #[wasm_bindgen] 79 | pub fn chain_id(&self) -> u32 { 80 | self.chain_id as u32 81 | } 82 | 83 | #[wasm_bindgen] 84 | pub async fn get_block_number(&self) -> u32 { 85 | self.inner.get_block_number().await.unwrap() as u32 86 | } 87 | 88 | #[wasm_bindgen] 89 | pub async fn get_balance(&self, addr: JsValue, block: JsValue) -> String { 90 | let addr: Address = serde_wasm_bindgen::from_value(addr).unwrap(); 91 | let block: BlockTag = serde_wasm_bindgen::from_value(block).unwrap(); 92 | self.inner 93 | .get_balance(&addr, block) 94 | .await 95 | .unwrap() 96 | .to_string() 97 | } 98 | 99 | #[wasm_bindgen] 100 | pub async fn get_transaction_by_hash(&self, hash: String) -> JsValue { 101 | let hash = H256::from_str(&hash).unwrap(); 102 | let tx = self.inner.get_transaction_by_hash(&hash).await.unwrap(); 103 | serde_wasm_bindgen::to_value(&tx).unwrap() 104 | } 105 | 106 | #[wasm_bindgen] 107 | pub async fn get_transaction_count(&self, addr: JsValue, block: JsValue) -> u32 { 108 | let addr: Address = serde_wasm_bindgen::from_value(addr).unwrap(); 109 | let block: BlockTag = serde_wasm_bindgen::from_value(block).unwrap(); 110 | self.inner.get_nonce(&addr, block).await.unwrap() as u32 111 | } 112 | 113 | #[wasm_bindgen] 114 | pub async fn get_block_transaction_count_by_hash(&self, hash: JsValue) -> u32 { 115 | let hash: H256 = serde_wasm_bindgen::from_value(hash).unwrap(); 116 | self.inner 117 | .get_block_transaction_count_by_hash(&hash.as_bytes().to_vec()) 118 | .await 119 | .unwrap() as u32 120 | } 121 | 122 | #[wasm_bindgen] 123 | pub async fn get_block_transaction_count_by_number(&self, block: JsValue) -> u32 { 124 | let block: BlockTag = serde_wasm_bindgen::from_value(block).unwrap(); 125 | self.inner 126 | .get_block_transaction_count_by_number(block) 127 | .await 128 | .unwrap() as u32 129 | } 130 | 131 | #[wasm_bindgen] 132 | pub async fn get_code(&self, addr: JsValue, block: JsValue) -> String { 133 | let addr: Address = serde_wasm_bindgen::from_value(addr).unwrap(); 134 | let block: BlockTag = serde_wasm_bindgen::from_value(block).unwrap(); 135 | let code = self.inner.get_code(&addr, block).await.unwrap(); 136 | format!("0x{}", hex::encode(code)) 137 | } 138 | 139 | #[wasm_bindgen] 140 | pub async fn call(&self, opts: JsValue, block: JsValue) -> String { 141 | let opts: CallOpts = serde_wasm_bindgen::from_value(opts).unwrap(); 142 | let block: BlockTag = serde_wasm_bindgen::from_value(block).unwrap(); 143 | let res = self.inner.call(&opts, block).await.unwrap(); 144 | format!("0x{}", hex::encode(res)) 145 | } 146 | 147 | #[wasm_bindgen] 148 | pub async fn estimate_gas(&self, opts: JsValue) -> u32 { 149 | let opts: CallOpts = serde_wasm_bindgen::from_value(opts).unwrap(); 150 | self.inner.estimate_gas(&opts).await.unwrap() as u32 151 | } 152 | 153 | #[wasm_bindgen] 154 | pub async fn gas_price(&self) -> JsValue { 155 | let price = self.inner.get_gas_price().await.unwrap(); 156 | serde_wasm_bindgen::to_value(&price).unwrap() 157 | } 158 | 159 | #[wasm_bindgen] 160 | pub async fn max_priority_fee_per_gas(&self) -> JsValue { 161 | let price = self.inner.get_priority_fee().await.unwrap(); 162 | serde_wasm_bindgen::to_value(&price).unwrap() 163 | } 164 | 165 | #[wasm_bindgen] 166 | pub async fn send_raw_transaction(&self, tx: String) -> JsValue { 167 | let tx = hex::decode(tx).unwrap(); 168 | let hash = self.inner.send_raw_transaction(&tx).await.unwrap(); 169 | serde_wasm_bindgen::to_value(&hash).unwrap() 170 | } 171 | 172 | #[wasm_bindgen] 173 | pub async fn get_transaction_receipt(&self, tx: JsValue) -> JsValue { 174 | let tx: H256 = serde_wasm_bindgen::from_value(tx).unwrap(); 175 | let receipt = self.inner.get_transaction_receipt(&tx).await.unwrap(); 176 | serde_wasm_bindgen::to_value(&receipt).unwrap() 177 | } 178 | 179 | #[wasm_bindgen] 180 | pub async fn get_logs(&self, filter: JsValue) -> JsValue { 181 | let filter: Filter = serde_wasm_bindgen::from_value(filter).unwrap(); 182 | let logs = self.inner.get_logs(&filter).await.unwrap(); 183 | serde_wasm_bindgen::to_value(&logs).unwrap() 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /execution/src/proof.rs: -------------------------------------------------------------------------------- 1 | use ethers::types::{Bytes, EIP1186ProofResponse}; 2 | use ethers::utils::keccak256; 3 | use ethers::utils::rlp::{decode_list, RlpStream}; 4 | 5 | pub fn verify_proof(proof: &Vec, root: &[u8], path: &Vec, value: &Vec) -> bool { 6 | let mut expected_hash = root.to_vec(); 7 | let mut path_offset = 0; 8 | 9 | for (i, node) in proof.iter().enumerate() { 10 | if expected_hash != keccak256(node).to_vec() { 11 | return false; 12 | } 13 | 14 | let node_list: Vec> = decode_list(node); 15 | 16 | if node_list.len() == 17 { 17 | if i == proof.len() - 1 { 18 | // exclusion proof 19 | let nibble = get_nibble(path, path_offset); 20 | let node = &node_list[nibble as usize]; 21 | 22 | if node.is_empty() && is_empty_value(value) { 23 | return true; 24 | } 25 | } else { 26 | let nibble = get_nibble(path, path_offset); 27 | expected_hash = node_list[nibble as usize].clone(); 28 | 29 | path_offset += 1; 30 | } 31 | } else if node_list.len() == 2 { 32 | if i == proof.len() - 1 { 33 | // exclusion proof 34 | if !paths_match(&node_list[0], skip_length(&node_list[0]), path, path_offset) 35 | && is_empty_value(value) 36 | { 37 | return true; 38 | } 39 | 40 | // inclusion proof 41 | if &node_list[1] == value { 42 | return paths_match( 43 | &node_list[0], 44 | skip_length(&node_list[0]), 45 | path, 46 | path_offset, 47 | ); 48 | } 49 | } else { 50 | let node_path = &node_list[0]; 51 | let prefix_length = shared_prefix_length(path, path_offset, node_path); 52 | if prefix_length < node_path.len() * 2 - skip_length(node_path) { 53 | // The proof shows a divergent path, but we're not 54 | // at the end of the proof, so something's wrong. 55 | return false; 56 | } 57 | path_offset += prefix_length; 58 | expected_hash = node_list[1].clone(); 59 | } 60 | } else { 61 | return false; 62 | } 63 | } 64 | 65 | false 66 | } 67 | 68 | fn paths_match(p1: &Vec, s1: usize, p2: &Vec, s2: usize) -> bool { 69 | let len1 = p1.len() * 2 - s1; 70 | let len2 = p2.len() * 2 - s2; 71 | 72 | if len1 != len2 { 73 | return false; 74 | } 75 | 76 | for offset in 0..len1 { 77 | let n1 = get_nibble(p1, s1 + offset); 78 | let n2 = get_nibble(p2, s2 + offset); 79 | 80 | if n1 != n2 { 81 | return false; 82 | } 83 | } 84 | 85 | true 86 | } 87 | 88 | #[allow(dead_code)] 89 | fn get_rest_path(p: &Vec, s: usize) -> String { 90 | let mut ret = String::new(); 91 | for i in s..p.len() * 2 { 92 | let n = get_nibble(p, i); 93 | ret += &format!("{n:01x}"); 94 | } 95 | ret 96 | } 97 | 98 | fn is_empty_value(value: &Vec) -> bool { 99 | let mut stream = RlpStream::new(); 100 | stream.begin_list(4); 101 | stream.append_empty_data(); 102 | stream.append_empty_data(); 103 | let empty_storage_hash = "56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421"; 104 | stream.append(&hex::decode(empty_storage_hash).unwrap()); 105 | let empty_code_hash = "c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470"; 106 | stream.append(&hex::decode(empty_code_hash).unwrap()); 107 | let empty_account = stream.out(); 108 | 109 | let is_empty_slot = value.len() == 1 && value[0] == 0x80; 110 | let is_empty_account = value == &empty_account; 111 | is_empty_slot || is_empty_account 112 | } 113 | 114 | fn shared_prefix_length(path: &Vec, path_offset: usize, node_path: &Vec) -> usize { 115 | let skip_length = skip_length(node_path); 116 | 117 | let len = std::cmp::min( 118 | node_path.len() * 2 - skip_length, 119 | path.len() * 2 - path_offset, 120 | ); 121 | let mut prefix_len = 0; 122 | 123 | for i in 0..len { 124 | let path_nibble = get_nibble(path, i + path_offset); 125 | let node_path_nibble = get_nibble(node_path, i + skip_length); 126 | 127 | if path_nibble == node_path_nibble { 128 | prefix_len += 1; 129 | } else { 130 | break; 131 | } 132 | } 133 | 134 | prefix_len 135 | } 136 | 137 | fn skip_length(node: &Vec) -> usize { 138 | if node.is_empty() { 139 | return 0; 140 | } 141 | 142 | let nibble = get_nibble(node, 0); 143 | match nibble { 144 | 0 => 2, 145 | 1 => 1, 146 | 2 => 2, 147 | 3 => 1, 148 | _ => 0, 149 | } 150 | } 151 | 152 | fn get_nibble(path: &[u8], offset: usize) -> u8 { 153 | let byte = path[offset / 2]; 154 | if offset % 2 == 0 { 155 | byte >> 4 156 | } else { 157 | byte & 0xF 158 | } 159 | } 160 | 161 | pub fn encode_account(proof: &EIP1186ProofResponse) -> Vec { 162 | let mut stream = RlpStream::new_list(4); 163 | stream.append(&proof.nonce); 164 | stream.append(&proof.balance); 165 | stream.append(&proof.storage_hash); 166 | stream.append(&proof.code_hash); 167 | let encoded = stream.out(); 168 | encoded.to_vec() 169 | } 170 | 171 | #[cfg(test)] 172 | mod tests { 173 | use crate::proof::shared_prefix_length; 174 | 175 | #[tokio::test] 176 | async fn test_shared_prefix_length() { 177 | // We compare the path starting from the 6th nibble i.e. the 6 in 0x6f 178 | let path: Vec = vec![0x12, 0x13, 0x14, 0x6f, 0x6c, 0x64, 0x21]; 179 | let path_offset = 6; 180 | // Our node path matches only the first 5 nibbles of the path 181 | let node_path: Vec = vec![0x6f, 0x6c, 0x63, 0x21]; 182 | let shared_len = shared_prefix_length(&path, path_offset, &node_path); 183 | assert_eq!(shared_len, 5); 184 | 185 | // Now we compare the path starting from the 5th nibble i.e. the 4 in 0x14 186 | let path: Vec = vec![0x12, 0x13, 0x14, 0x6f, 0x6c, 0x64, 0x21]; 187 | let path_offset = 5; 188 | // Our node path matches only the first 7 nibbles of the path 189 | // Note the first nibble is 1, so we skip 1 nibble 190 | let node_path: Vec = vec![0x14, 0x6f, 0x6c, 0x64, 0x11]; 191 | let shared_len = shared_prefix_length(&path, path_offset, &node_path); 192 | assert_eq!(shared_len, 7); 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /execution/tests/execution.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | use std::str::FromStr; 3 | 4 | use ethers::types::{Address, Filter, H256, U256}; 5 | use ssz_rs::{List, Vector}; 6 | 7 | use common::utils::hex_str_to_bytes; 8 | use consensus::types::ExecutionPayload; 9 | use execution::rpc::mock_rpc::MockRpc; 10 | use execution::ExecutionClient; 11 | 12 | fn get_client() -> ExecutionClient { 13 | ExecutionClient::new("testdata/").unwrap() 14 | } 15 | 16 | #[tokio::test] 17 | async fn test_get_account() { 18 | let execution = get_client(); 19 | let address = Address::from_str("14f9D4aF749609c1438528C0Cce1cC3f6D411c47").unwrap(); 20 | 21 | let payload = ExecutionPayload { 22 | state_root: Vector::from_iter( 23 | hex_str_to_bytes("0xaa02f5db2ee75e3da400d10f3c30e894b6016ce8a2501680380a907b6674ce0d") 24 | .unwrap(), 25 | ), 26 | ..ExecutionPayload::default() 27 | }; 28 | 29 | let account = execution 30 | .get_account(&address, None, &payload) 31 | .await 32 | .unwrap(); 33 | 34 | assert_eq!( 35 | account.balance, 36 | U256::from_str_radix("48c27395000", 16).unwrap() 37 | ); 38 | } 39 | 40 | #[tokio::test] 41 | async fn test_get_account_bad_proof() { 42 | let execution = get_client(); 43 | let address = Address::from_str("14f9D4aF749609c1438528C0Cce1cC3f6D411c47").unwrap(); 44 | let payload = ExecutionPayload::default(); 45 | 46 | let account_res = execution.get_account(&address, None, &payload).await; 47 | 48 | assert!(account_res.is_err()); 49 | } 50 | 51 | #[tokio::test] 52 | async fn test_get_tx() { 53 | let execution = get_client(); 54 | let tx_hash = 55 | H256::from_str("2dac1b27ab58b493f902dda8b63979a112398d747f1761c0891777c0983e591f").unwrap(); 56 | 57 | let mut payload = ExecutionPayload::default(); 58 | payload.transactions.push(List::from_iter(hex_str_to_bytes("0x02f8b20583623355849502f900849502f91082ea6094326c977e6efc84e512bb9c30f76e30c160ed06fb80b844a9059cbb0000000000000000000000007daccf9b3c1ae2fa5c55f1c978aeef700bc83be0000000000000000000000000000000000000000000000001158e460913d00000c080a0e1445466b058b6f883c0222f1b1f3e2ad9bee7b5f688813d86e3fa8f93aa868ca0786d6e7f3aefa8fe73857c65c32e4884d8ba38d0ecfb947fbffb82e8ee80c167").unwrap())); 59 | 60 | let mut payloads = BTreeMap::new(); 61 | payloads.insert(7530933, payload); 62 | 63 | let tx = execution 64 | .get_transaction(&tx_hash, &payloads) 65 | .await 66 | .unwrap() 67 | .unwrap(); 68 | 69 | assert_eq!(tx.hash(), tx_hash); 70 | } 71 | 72 | #[tokio::test] 73 | async fn test_get_tx_bad_proof() { 74 | let execution = get_client(); 75 | let tx_hash = 76 | H256::from_str("2dac1b27ab58b493f902dda8b63979a112398d747f1761c0891777c0983e591f").unwrap(); 77 | 78 | let payload = ExecutionPayload::default(); 79 | 80 | let mut payloads = BTreeMap::new(); 81 | payloads.insert(7530933, payload); 82 | 83 | let tx_res = execution.get_transaction(&tx_hash, &payloads).await; 84 | 85 | assert!(tx_res.is_err()); 86 | } 87 | 88 | #[tokio::test] 89 | async fn test_get_tx_not_included() { 90 | let execution = get_client(); 91 | let tx_hash = 92 | H256::from_str("2dac1b27ab58b493f902dda8b63979a112398d747f1761c0891777c0983e591f").unwrap(); 93 | 94 | let payloads = BTreeMap::new(); 95 | 96 | let tx_opt = execution 97 | .get_transaction(&tx_hash, &payloads) 98 | .await 99 | .unwrap(); 100 | 101 | assert!(tx_opt.is_none()); 102 | } 103 | 104 | #[tokio::test] 105 | async fn test_get_logs() { 106 | let execution = get_client(); 107 | let mut payload = ExecutionPayload { 108 | receipts_root: Vector::from_iter( 109 | hex_str_to_bytes("dd82a78eccb333854f0c99e5632906e092d8a49c27a21c25cae12b82ec2a113f") 110 | .unwrap(), 111 | ), 112 | ..ExecutionPayload::default() 113 | }; 114 | 115 | payload.transactions.push(List::from_iter(hex_str_to_bytes("0x02f8b20583623355849502f900849502f91082ea6094326c977e6efc84e512bb9c30f76e30c160ed06fb80b844a9059cbb0000000000000000000000007daccf9b3c1ae2fa5c55f1c978aeef700bc83be0000000000000000000000000000000000000000000000001158e460913d00000c080a0e1445466b058b6f883c0222f1b1f3e2ad9bee7b5f688813d86e3fa8f93aa868ca0786d6e7f3aefa8fe73857c65c32e4884d8ba38d0ecfb947fbffb82e8ee80c167").unwrap())); 116 | 117 | let mut payloads = BTreeMap::new(); 118 | payloads.insert(7530933, payload); 119 | 120 | let filter = Filter::new(); 121 | let logs = execution.get_logs(&filter, &payloads).await.unwrap(); 122 | 123 | let tx_hash = 124 | H256::from_str("2dac1b27ab58b493f902dda8b63979a112398d747f1761c0891777c0983e591f").unwrap(); 125 | 126 | assert!(!logs.is_empty()); 127 | assert!(logs[0].transaction_hash.is_some()); 128 | assert!(logs[0].transaction_hash.unwrap() == tx_hash); 129 | } 130 | 131 | #[tokio::test] 132 | async fn test_get_receipt() { 133 | let execution = get_client(); 134 | let tx_hash = 135 | H256::from_str("2dac1b27ab58b493f902dda8b63979a112398d747f1761c0891777c0983e591f").unwrap(); 136 | 137 | let mut payload = ExecutionPayload { 138 | receipts_root: Vector::from_iter( 139 | hex_str_to_bytes("dd82a78eccb333854f0c99e5632906e092d8a49c27a21c25cae12b82ec2a113f") 140 | .unwrap(), 141 | ), 142 | ..ExecutionPayload::default() 143 | }; 144 | 145 | payload.transactions.push(List::from_iter(hex_str_to_bytes("0x02f8b20583623355849502f900849502f91082ea6094326c977e6efc84e512bb9c30f76e30c160ed06fb80b844a9059cbb0000000000000000000000007daccf9b3c1ae2fa5c55f1c978aeef700bc83be0000000000000000000000000000000000000000000000001158e460913d00000c080a0e1445466b058b6f883c0222f1b1f3e2ad9bee7b5f688813d86e3fa8f93aa868ca0786d6e7f3aefa8fe73857c65c32e4884d8ba38d0ecfb947fbffb82e8ee80c167").unwrap())); 146 | 147 | let mut payloads = BTreeMap::new(); 148 | payloads.insert(7530933, payload); 149 | 150 | let receipt = execution 151 | .get_transaction_receipt(&tx_hash, &payloads) 152 | .await 153 | .unwrap() 154 | .unwrap(); 155 | 156 | assert_eq!(receipt.transaction_hash, tx_hash); 157 | } 158 | 159 | #[tokio::test] 160 | async fn test_get_receipt_bad_proof() { 161 | let execution = get_client(); 162 | let tx_hash = 163 | H256::from_str("2dac1b27ab58b493f902dda8b63979a112398d747f1761c0891777c0983e591f").unwrap(); 164 | 165 | let mut payload = ExecutionPayload::default(); 166 | payload.transactions.push(List::from_iter(hex_str_to_bytes("0x02f8b20583623355849502f900849502f91082ea6094326c977e6efc84e512bb9c30f76e30c160ed06fb80b844a9059cbb0000000000000000000000007daccf9b3c1ae2fa5c55f1c978aeef700bc83be0000000000000000000000000000000000000000000000001158e460913d00000c080a0e1445466b058b6f883c0222f1b1f3e2ad9bee7b5f688813d86e3fa8f93aa868ca0786d6e7f3aefa8fe73857c65c32e4884d8ba38d0ecfb947fbffb82e8ee80c167").unwrap())); 167 | 168 | let mut payloads = BTreeMap::new(); 169 | payloads.insert(7530933, payload); 170 | 171 | let receipt_res = execution.get_transaction_receipt(&tx_hash, &payloads).await; 172 | 173 | assert!(receipt_res.is_err()); 174 | } 175 | 176 | #[tokio::test] 177 | async fn test_get_receipt_not_included() { 178 | let execution = get_client(); 179 | let tx_hash = 180 | H256::from_str("2dac1b27ab58b493f902dda8b63979a112398d747f1761c0891777c0983e591f").unwrap(); 181 | 182 | let payloads = BTreeMap::new(); 183 | let receipt_opt = execution 184 | .get_transaction_receipt(&tx_hash, &payloads) 185 | .await 186 | .unwrap(); 187 | 188 | assert!(receipt_opt.is_none()); 189 | } 190 | 191 | #[tokio::test] 192 | async fn test_get_block() { 193 | let execution = get_client(); 194 | let payload = ExecutionPayload { 195 | block_number: 12345, 196 | ..ExecutionPayload::default() 197 | }; 198 | 199 | let block = execution.get_block(&payload, false).await.unwrap(); 200 | 201 | assert_eq!(block.number, 12345); 202 | } 203 | 204 | #[tokio::test] 205 | async fn test_get_tx_by_block_hash_and_index() { 206 | let execution = get_client(); 207 | let tx_hash = 208 | H256::from_str("2dac1b27ab58b493f902dda8b63979a112398d747f1761c0891777c0983e591f").unwrap(); 209 | 210 | let mut payload = ExecutionPayload { 211 | block_number: 7530933, 212 | ..ExecutionPayload::default() 213 | }; 214 | payload.transactions.push(List::from_iter(hex_str_to_bytes("0x02f8b20583623355849502f900849502f91082ea6094326c977e6efc84e512bb9c30f76e30c160ed06fb80b844a9059cbb0000000000000000000000007daccf9b3c1ae2fa5c55f1c978aeef700bc83be0000000000000000000000000000000000000000000000001158e460913d00000c080a0e1445466b058b6f883c0222f1b1f3e2ad9bee7b5f688813d86e3fa8f93aa868ca0786d6e7f3aefa8fe73857c65c32e4884d8ba38d0ecfb947fbffb82e8ee80c167").unwrap())); 215 | 216 | let tx = execution 217 | .get_transaction_by_block_hash_and_index(&payload, 0) 218 | .await 219 | .unwrap() 220 | .unwrap(); 221 | assert_eq!(tx.hash(), tx_hash); 222 | } 223 | -------------------------------------------------------------------------------- /execution/src/evm.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::{BTreeMap, HashMap}, 3 | str::FromStr, 4 | sync::Arc, 5 | thread, 6 | }; 7 | 8 | use bytes::Bytes; 9 | use common::{errors::BlockNotFoundError, types::BlockTag}; 10 | use ethers::{ 11 | abi::ethereum_types::BigEndianHash, 12 | types::transaction::eip2930::AccessListItem, 13 | types::{Address, H160, H256, U256}, 14 | }; 15 | use eyre::{Report, Result}; 16 | use futures::{executor::block_on, future::join_all}; 17 | use log::trace; 18 | use revm::{AccountInfo, Bytecode, Database, Env, TransactOut, TransactTo, EVM}; 19 | 20 | use consensus::types::ExecutionPayload; 21 | 22 | use crate::{ 23 | constants::PARALLEL_QUERY_BATCH_SIZE, 24 | errors::EvmError, 25 | rpc::ExecutionRpc, 26 | types::{Account, CallOpts}, 27 | }; 28 | 29 | use super::ExecutionClient; 30 | 31 | pub struct Evm<'a, R: ExecutionRpc> { 32 | evm: EVM>, 33 | chain_id: u64, 34 | } 35 | 36 | impl<'a, R: ExecutionRpc> Evm<'a, R> { 37 | pub fn new( 38 | execution: Arc>, 39 | current_payload: &'a ExecutionPayload, 40 | payloads: &'a BTreeMap, 41 | chain_id: u64, 42 | ) -> Self { 43 | let mut evm: EVM> = EVM::new(); 44 | let db = ProofDB::new(execution, current_payload, payloads); 45 | evm.database(db); 46 | 47 | Evm { evm, chain_id } 48 | } 49 | 50 | pub async fn call(&mut self, opts: &CallOpts) -> Result, EvmError> { 51 | let account_map = self.batch_fetch_accounts(opts).await?; 52 | self.evm.db.as_mut().unwrap().set_accounts(account_map); 53 | 54 | self.evm.env = self.get_env(opts); 55 | let tx = self.evm.transact().0; 56 | 57 | match tx.exit_reason { 58 | revm::Return::Revert => match tx.out { 59 | TransactOut::Call(bytes) => Err(EvmError::Revert(Some(bytes))), 60 | _ => Err(EvmError::Revert(None)), 61 | }, 62 | revm::Return::Return | revm::Return::Stop => { 63 | if let Some(err) = &self.evm.db.as_ref().unwrap().error { 64 | return Err(EvmError::Generic(err.clone())); 65 | } 66 | 67 | match tx.out { 68 | TransactOut::None => Err(EvmError::Generic("Invalid Call".to_string())), 69 | TransactOut::Create(..) => Err(EvmError::Generic("Invalid Call".to_string())), 70 | TransactOut::Call(bytes) => Ok(bytes.to_vec()), 71 | } 72 | } 73 | _ => Err(EvmError::Revm(tx.exit_reason)), 74 | } 75 | } 76 | 77 | pub async fn estimate_gas(&mut self, opts: &CallOpts) -> Result { 78 | let account_map = self.batch_fetch_accounts(opts).await?; 79 | self.evm.db.as_mut().unwrap().set_accounts(account_map); 80 | 81 | self.evm.env = self.get_env(opts); 82 | let tx = self.evm.transact().0; 83 | let gas = tx.gas_used; 84 | 85 | match tx.exit_reason { 86 | revm::Return::Revert => match tx.out { 87 | TransactOut::Call(bytes) => Err(EvmError::Revert(Some(bytes))), 88 | _ => Err(EvmError::Revert(None)), 89 | }, 90 | revm::Return::Return | revm::Return::Stop => { 91 | if let Some(err) = &self.evm.db.as_ref().unwrap().error { 92 | return Err(EvmError::Generic(err.clone())); 93 | } 94 | 95 | // overestimate to avoid out of gas reverts 96 | let gas_scaled = (1.10 * gas as f64) as u64; 97 | Ok(gas_scaled) 98 | } 99 | _ => Err(EvmError::Revm(tx.exit_reason)), 100 | } 101 | } 102 | 103 | async fn batch_fetch_accounts( 104 | &self, 105 | opts: &CallOpts, 106 | ) -> Result, EvmError> { 107 | let db = self.evm.db.as_ref().unwrap(); 108 | let rpc = db.execution.rpc.clone(); 109 | let payload = db.current_payload.clone(); 110 | let execution = db.execution.clone(); 111 | let block = db.current_payload.block_number; 112 | 113 | let opts_moved = CallOpts { 114 | from: opts.from, 115 | to: opts.to, 116 | value: opts.value, 117 | data: opts.data.clone(), 118 | gas: opts.gas, 119 | gas_price: opts.gas_price, 120 | }; 121 | 122 | let mut list = rpc 123 | .create_access_list(&opts_moved, block) 124 | .await 125 | .map_err(EvmError::RpcError)? 126 | .0; 127 | 128 | let from_access_entry = AccessListItem { 129 | address: opts_moved.from.unwrap_or_default(), 130 | storage_keys: Vec::default(), 131 | }; 132 | 133 | let to_access_entry = AccessListItem { 134 | address: opts_moved.to, 135 | storage_keys: Vec::default(), 136 | }; 137 | 138 | let producer_account = AccessListItem { 139 | address: Address::from_slice(&payload.fee_recipient), 140 | storage_keys: Vec::default(), 141 | }; 142 | 143 | list.push(from_access_entry); 144 | list.push(to_access_entry); 145 | list.push(producer_account); 146 | 147 | let mut account_map = HashMap::new(); 148 | for chunk in list.chunks(PARALLEL_QUERY_BATCH_SIZE) { 149 | let account_chunk_futs = chunk.iter().map(|account| { 150 | let account_fut = execution.get_account( 151 | &account.address, 152 | Some(account.storage_keys.as_slice()), 153 | &payload, 154 | ); 155 | async move { (account.address, account_fut.await) } 156 | }); 157 | 158 | let account_chunk = join_all(account_chunk_futs).await; 159 | 160 | account_chunk 161 | .into_iter() 162 | .filter(|i| i.1.is_ok()) 163 | .for_each(|(key, value)| { 164 | account_map.insert(key, value.ok().unwrap()); 165 | }); 166 | } 167 | 168 | Ok(account_map) 169 | } 170 | 171 | fn get_env(&self, opts: &CallOpts) -> Env { 172 | let mut env = Env::default(); 173 | let payload = &self.evm.db.as_ref().unwrap().current_payload; 174 | 175 | env.tx.transact_to = TransactTo::Call(opts.to); 176 | env.tx.caller = opts.from.unwrap_or(Address::zero()); 177 | env.tx.value = opts.value.unwrap_or(U256::from(0)); 178 | env.tx.data = Bytes::from(opts.data.clone().unwrap_or_default()); 179 | env.tx.gas_limit = opts.gas.map(|v| v.as_u64()).unwrap_or(u64::MAX); 180 | env.tx.gas_price = opts.gas_price.unwrap_or(U256::zero()); 181 | 182 | env.block.number = U256::from(payload.block_number); 183 | env.block.coinbase = Address::from_slice(&payload.fee_recipient); 184 | env.block.timestamp = U256::from(payload.timestamp); 185 | env.block.difficulty = U256::from_little_endian(&payload.prev_randao); 186 | 187 | env.cfg.chain_id = self.chain_id.into(); 188 | 189 | env 190 | } 191 | } 192 | 193 | struct ProofDB<'a, R: ExecutionRpc> { 194 | execution: Arc>, 195 | current_payload: &'a ExecutionPayload, 196 | payloads: &'a BTreeMap, 197 | accounts: HashMap, 198 | error: Option, 199 | } 200 | 201 | impl<'a, R: ExecutionRpc> ProofDB<'a, R> { 202 | pub fn new( 203 | execution: Arc>, 204 | current_payload: &'a ExecutionPayload, 205 | payloads: &'a BTreeMap, 206 | ) -> Self { 207 | ProofDB { 208 | execution, 209 | current_payload, 210 | payloads, 211 | accounts: HashMap::new(), 212 | error: None, 213 | } 214 | } 215 | 216 | pub fn set_accounts(&mut self, accounts: HashMap) { 217 | self.accounts = accounts; 218 | } 219 | 220 | fn get_account(&mut self, address: Address, slots: &[H256]) -> Result { 221 | let execution = self.execution.clone(); 222 | let payload = self.current_payload.clone(); 223 | let slots = slots.to_owned(); 224 | 225 | let handle = thread::spawn(move || { 226 | let account_fut = execution.get_account(&address, Some(&slots), &payload); 227 | // let runtime = Runtime::new()?; 228 | // runtime.block_on(account_fut) 229 | block_on(account_fut) 230 | }); 231 | 232 | handle.join().unwrap() 233 | } 234 | } 235 | 236 | impl<'a, R: ExecutionRpc> Database for ProofDB<'a, R> { 237 | type Error = Report; 238 | 239 | fn basic(&mut self, address: H160) -> Result, Report> { 240 | if is_precompile(&address) { 241 | return Ok(Some(AccountInfo::default())); 242 | } 243 | 244 | trace!( 245 | "fetch basic evm state for address=0x{}", 246 | hex::encode(address.as_bytes()) 247 | ); 248 | 249 | let account = match self.accounts.get(&address) { 250 | Some(account) => account.clone(), 251 | None => self.get_account(address, &[])?, 252 | }; 253 | 254 | let bytecode = Bytecode::new_raw(Bytes::from(account.code.clone())); 255 | Ok(Some(AccountInfo::new( 256 | account.balance, 257 | account.nonce, 258 | bytecode, 259 | ))) 260 | } 261 | 262 | fn block_hash(&mut self, number: U256) -> Result { 263 | let number = number.as_u64(); 264 | let payload = self 265 | .payloads 266 | .get(&number) 267 | .ok_or(BlockNotFoundError::new(BlockTag::Number(number)))?; 268 | Ok(H256::from_slice(&payload.block_hash)) 269 | } 270 | 271 | fn storage(&mut self, address: H160, slot: U256) -> Result { 272 | trace!( 273 | "fetch evm state for address=0x{}, slot={}", 274 | hex::encode(address.as_bytes()), 275 | slot 276 | ); 277 | 278 | let slot = H256::from_uint(&slot); 279 | 280 | Ok(match self.accounts.get(&address) { 281 | Some(account) => match account.slots.get(&slot) { 282 | Some(slot) => *slot, 283 | None => *self 284 | .get_account(address, &[slot])? 285 | .slots 286 | .get(&slot) 287 | .unwrap(), 288 | }, 289 | None => *self 290 | .get_account(address, &[slot])? 291 | .slots 292 | .get(&slot) 293 | .unwrap(), 294 | }) 295 | } 296 | 297 | fn code_by_hash(&mut self, _code_hash: H256) -> Result { 298 | Err(eyre::eyre!("should never be called")) 299 | } 300 | } 301 | 302 | fn is_precompile(address: &Address) -> bool { 303 | address.le(&Address::from_str("0x0000000000000000000000000000000000000009").unwrap()) 304 | && address.gt(&Address::zero()) 305 | } 306 | -------------------------------------------------------------------------------- /config/src/checkpoints.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use ethers::types::H256; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | use crate::networks; 7 | 8 | /// The location where the list of checkpoint services are stored. 9 | pub const CHECKPOINT_SYNC_SERVICES_LIST: &str = "https://raw.githubusercontent.com/ethpandaops/checkpoint-sync-health-checks/master/_data/endpoints.yaml"; 10 | 11 | #[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)] 12 | pub struct RawSlotResponse { 13 | pub data: RawSlotResponseData, 14 | } 15 | 16 | #[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)] 17 | pub struct RawSlotResponseData { 18 | pub slots: Vec, 19 | } 20 | 21 | #[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)] 22 | pub struct Slot { 23 | pub slot: u64, 24 | pub block_root: Option, 25 | pub state_root: Option, 26 | pub epoch: u64, 27 | pub time: StartEndTime, 28 | } 29 | 30 | #[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)] 31 | pub struct StartEndTime { 32 | /// An ISO 8601 formatted UTC timestamp. 33 | pub start_time: String, 34 | /// An ISO 8601 formatted UTC timestamp. 35 | pub end_time: String, 36 | } 37 | 38 | /// A health check for the checkpoint sync service. 39 | #[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)] 40 | pub struct Health { 41 | /// If the node is healthy. 42 | pub result: bool, 43 | /// An [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) UTC timestamp. 44 | pub date: String, 45 | } 46 | 47 | /// A checkpoint fallback service. 48 | #[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)] 49 | pub struct CheckpointFallbackService { 50 | /// The endpoint for the checkpoint sync service. 51 | pub endpoint: String, 52 | /// The checkpoint sync service name. 53 | pub name: String, 54 | /// The service state. 55 | pub state: bool, 56 | /// If the service is verified. 57 | pub verification: bool, 58 | /// Contact information for the service maintainers. 59 | pub contacts: Option, 60 | /// Service Notes 61 | pub notes: Option, 62 | /// The service health check. 63 | pub health: Vec, 64 | } 65 | 66 | /// The CheckpointFallback manages checkpoint fallback services. 67 | #[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)] 68 | pub struct CheckpointFallback { 69 | /// Services Map 70 | pub services: HashMap>, 71 | /// A list of supported networks to build. 72 | /// Default: [mainnet, goerli] 73 | pub networks: Vec, 74 | } 75 | 76 | impl CheckpointFallback { 77 | /// Constructs a new checkpoint fallback service. 78 | pub fn new() -> Self { 79 | Self { 80 | services: Default::default(), 81 | networks: [networks::Network::MAINNET, networks::Network::GOERLI].to_vec(), 82 | } 83 | } 84 | 85 | /// Build the checkpoint fallback service from the community-maintained list by [ethPandaOps](https://github.com/ethpandaops). 86 | /// 87 | /// The list is defined in [ethPandaOps/checkpoint-fallback-service](https://github.com/ethpandaops/checkpoint-sync-health-checks/blob/master/_data/endpoints.yaml). 88 | pub async fn build(mut self) -> eyre::Result { 89 | // Fetch the services 90 | let client = reqwest::Client::new(); 91 | let res = client.get(CHECKPOINT_SYNC_SERVICES_LIST).send().await?; 92 | let yaml = res.text().await?; 93 | 94 | // Parse the yaml content results. 95 | let list: serde_yaml::Value = serde_yaml::from_str(&yaml)?; 96 | 97 | // Construct the services mapping from network <> list of services 98 | let mut services = HashMap::new(); 99 | for network in &self.networks { 100 | // Try to parse list of checkpoint fallback services 101 | let service_list = list 102 | .get(network.to_string().to_lowercase()) 103 | .ok_or_else(|| { 104 | eyre::eyre!(format!("missing {network} fallback checkpoint services")) 105 | })?; 106 | let parsed: Vec = 107 | serde_yaml::from_value(service_list.clone())?; 108 | services.insert(*network, parsed); 109 | } 110 | self.services = services; 111 | 112 | Ok(self) 113 | } 114 | 115 | /// Fetch the latest checkpoint from the checkpoint fallback service. 116 | pub async fn fetch_latest_checkpoint( 117 | &self, 118 | network: &crate::networks::Network, 119 | ) -> eyre::Result { 120 | let services = &self.get_healthy_fallback_services(network); 121 | Self::fetch_latest_checkpoint_from_services(&services[..]).await 122 | } 123 | 124 | async fn query_service(endpoint: &str) -> Option { 125 | let client = reqwest::Client::new(); 126 | let constructed_url = Self::construct_url(endpoint); 127 | let res = client.get(&constructed_url).send().await.ok()?; 128 | let raw: RawSlotResponse = res.json().await.ok()?; 129 | Some(raw) 130 | } 131 | 132 | /// Fetch the latest checkpoint from a list of checkpoint fallback services. 133 | pub async fn fetch_latest_checkpoint_from_services( 134 | services: &[CheckpointFallbackService], 135 | ) -> eyre::Result { 136 | // Iterate over all mainnet checkpoint sync services and get the latest checkpoint slot for each. 137 | let tasks: Vec<_> = services 138 | .iter() 139 | .map(|service| async move { 140 | let service = service.clone(); 141 | match Self::query_service(&service.endpoint).await { 142 | Some(raw) => { 143 | if raw.data.slots.is_empty() { 144 | return Err(eyre::eyre!("no slots")); 145 | } 146 | 147 | let slot = raw 148 | .data 149 | .slots 150 | .iter() 151 | .find(|s| s.block_root.is_some()) 152 | .ok_or(eyre::eyre!("no valid slots"))?; 153 | 154 | Ok(slot.clone()) 155 | } 156 | None => Err(eyre::eyre!("failed to query service")), 157 | } 158 | }) 159 | .collect(); 160 | 161 | let slots = futures::future::join_all(tasks) 162 | .await 163 | .iter() 164 | .filter_map(|slot| match &slot { 165 | Ok(s) => Some(s.clone()), 166 | _ => None, 167 | }) 168 | .filter(|s| s.block_root.is_some()) 169 | .collect::>(); 170 | 171 | // Get the max epoch 172 | let max_epoch_slot = slots.iter().max_by_key(|x| x.epoch).ok_or(eyre::eyre!( 173 | "Failed to find max epoch from checkpoint slots" 174 | ))?; 175 | let max_epoch = max_epoch_slot.epoch; 176 | 177 | // Filter out all the slots that are not the max epoch. 178 | let slots = slots 179 | .into_iter() 180 | .filter(|x| x.epoch == max_epoch) 181 | .collect::>(); 182 | 183 | // Return the most commonly verified checkpoint. 184 | let checkpoints = slots 185 | .iter() 186 | .filter_map(|x| x.block_root) 187 | .collect::>(); 188 | let mut m: HashMap = HashMap::new(); 189 | for c in checkpoints { 190 | *m.entry(c).or_default() += 1; 191 | } 192 | let most_common = m.into_iter().max_by_key(|(_, v)| *v).map(|(k, _)| k); 193 | 194 | // Return the most commonly verified checkpoint for the latest epoch. 195 | most_common.ok_or_else(|| eyre::eyre!("No checkpoint found")) 196 | } 197 | 198 | /// Associated function to fetch the latest checkpoint from a specific checkpoint sync fallback 199 | /// service api url. 200 | pub async fn fetch_checkpoint_from_api(url: &str) -> eyre::Result { 201 | // Fetch the url 202 | let client = reqwest::Client::new(); 203 | let constructed_url = Self::construct_url(url); 204 | let res = client.get(constructed_url).send().await?; 205 | let raw: RawSlotResponse = res.json().await?; 206 | let slot = raw.data.slots[0].clone(); 207 | slot.block_root 208 | .ok_or_else(|| eyre::eyre!("Checkpoint not in returned slot")) 209 | } 210 | 211 | /// Constructs the checkpoint fallback service url for fetching a slot. 212 | /// 213 | /// This is an associated function and can be used like so: 214 | /// 215 | /// ```rust 216 | /// use config::CheckpointFallback; 217 | /// 218 | /// let url = CheckpointFallback::construct_url("https://sync-mainnet.beaconcha.in"); 219 | /// assert_eq!("https://sync-mainnet.beaconcha.in/checkpointz/v1/beacon/slots", url); 220 | /// ``` 221 | pub fn construct_url(endpoint: &str) -> String { 222 | format!("{endpoint}/checkpointz/v1/beacon/slots") 223 | } 224 | 225 | /// Returns a list of all checkpoint fallback endpoints. 226 | /// 227 | /// ### Warning 228 | /// 229 | /// These services are not healthchecked **nor** trustworthy and may act with malice by returning invalid checkpoints. 230 | pub fn get_all_fallback_endpoints(&self, network: &networks::Network) -> Vec { 231 | self.services[network] 232 | .iter() 233 | .map(|service| service.endpoint.clone()) 234 | .collect() 235 | } 236 | 237 | /// Returns a list of healthchecked checkpoint fallback endpoints. 238 | /// 239 | /// ### Warning 240 | /// 241 | /// These services are not trustworthy and may act with malice by returning invalid checkpoints. 242 | pub fn get_healthy_fallback_endpoints(&self, network: &networks::Network) -> Vec { 243 | self.services[network] 244 | .iter() 245 | .filter(|service| service.state) 246 | .map(|service| service.endpoint.clone()) 247 | .collect() 248 | } 249 | 250 | /// Returns a list of healthchecked checkpoint fallback services. 251 | /// 252 | /// ### Warning 253 | /// 254 | /// These services are not trustworthy and may act with malice by returning invalid checkpoints. 255 | pub fn get_healthy_fallback_services( 256 | &self, 257 | network: &networks::Network, 258 | ) -> Vec { 259 | self.services[network] 260 | .iter() 261 | .filter(|service| service.state) 262 | .cloned() 263 | .collect::>() 264 | } 265 | 266 | /// Returns the raw checkpoint fallback service objects for a given network. 267 | pub fn get_fallback_services( 268 | &self, 269 | network: &networks::Network, 270 | ) -> &Vec { 271 | self.services[network].as_ref() 272 | } 273 | } 274 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Helios (Nym mixnet fork) 2 | 3 | [![build](https://github.com/a16z/helios/actions/workflows/test.yml/badge.svg)](https://github.com/a16z/helios/actions/workflows/test.yml) [![license: MIT](https://img.shields.io/badge/license-MIT-brightgreen.svg)](https://opensource.org/licenses/MIT) [![chat](https://img.shields.io/badge/chat-telegram-blue)](https://t.me/+IntDY_gZJSRkNTJj) 4 | 5 | Helios is a fully trustless, efficient, and portable Ethereum light client written in Rust. 6 | 7 | This fork of Helios includes nascent support for the Nym mixnet, which provides strong eavesdropping protection for your Ethereum transactions. 8 | 9 | Helios converts an untrusted centralized RPC endpoint into a safe unmanipulable local RPC for its users. It syncs in seconds, requires no storage, and is lightweight enough to run on mobile devices. 10 | 11 | The entire size of Helios's binary is 5.3Mb and should be easy to compile into WebAssembly. This makes it a perfect target to embed directly inside wallets and dapps. 12 | 13 | ## Running it with Nym 14 | 15 | Make sure that your 1080 local port is free, as that port is used for socks5 traffic redirection. 16 | 17 | `cargo run --example mixnet --release` 18 | 19 | That's all you need to do - the remaining "Helios standard instructions" below refer to the non-Nym ways of running it. 20 | 21 | Note: not all Nym network requesters are up to date with the Alchemy URLs that are needed in order to proxy Helios requests, this will resolve as our community updates their systems. 22 | 23 | ## Helios standard instructions 24 | 25 | ## Installing 26 | 27 | First install `heliosup`, Helios's installer: 28 | 29 | ``` 30 | curl https://raw.githubusercontent.com/a16z/helios/master/heliosup/install | bash 31 | ``` 32 | 33 | To install Helios, run `heliosup`. 34 | 35 | ## Usage 36 | 37 | To run Helios, run the below command, replacing `$ETH_RPC_URL` with an RPC provider URL such as Alchemy or Infura: 38 | 39 | ``` 40 | helios --execution-rpc $ETH_RPC_URL 41 | ``` 42 | 43 | `$ETH_RPC_URL` must be an Ethereum provider that supports the `eth_getProof` endpoint. Infura does not currently support this. We recommend using Alchemy. 44 | 45 | Helios will now run a local RPC server at `http://127.0.0.1:8545`. 46 | 47 | Helios provides examples in the [`examples/`](./examples/) directory. To run an example, you can execute `cargo run --example ` from inside the helios repository. 48 | 49 | Helios also provides documentation of its supported RPC methods in the [rpc.md](./rpc.md) file. 50 | 51 | ### Warning 52 | 53 | Helios is still experimental software. While we hope you try it out, we do not suggest adding it as your main RPC in wallets yet. Sending high-value transactions from a wallet connected to Helios is discouraged. 54 | 55 | ### Additional Options 56 | 57 | `--consensus-rpc` or `-c` can be used to set a custom consensus layer rpc endpoint. This must be a consensus node that supports the light client beaconchain api. We recommend using Nimbus for this. If no consensus rpc is supplied, it defaults to `https://www.lightclientdata.org` which is run by us. 58 | 59 | `--checkpoint` or `-w` can be used to set a custom weak subjectivity checkpoint. This must be equal the first beacon blockhash of an epoch. Weak subjectivity checkpoints are the root of trust in the system. If this is set to a malicious value, an attacker can cause the client to sync to the wrong chain. Helios sets a default value initially, then caches the most recent finalized block it has seen for later use. 60 | 61 | `--network` or `-n` sets the network to sync to. Current valid options are `mainnet` and `goerli`, however users can add custom networks in their configuration files. 62 | 63 | `--rpc-port` or `-p` sets the port that the local RPC should run on. The default value is `8545`. 64 | 65 | `--data-dir` or `-d` sets the directory that Helios should use to store cached weak subjectivity checkpoints in. Each network only stores the latest checkpoint, which is just 32 bytes. 66 | 67 | `--fallback` or `-f` sets the checkpoint fallback url (a string). This is only used if the checkpoint provided by the `--checkpoint` flag is too outdated for Helios to use to sync. 68 | If none is provided and the `--load-external-fallback` flag is not set, Helios will error. 69 | For example, you can specify the fallback like so: `helios --fallback "https://sync-mainnet.beaconcha.in"` (or using shorthand like so: `helios -f "https://sync-mainnet.beaconcha.in"`) 70 | 71 | `--load-external-fallback` or `-l` enables weak subjectivity checkpoint fallback (no value needed). 72 | For example, say you set a checkpoint value that is too outdated and Helios cannot sync to it. 73 | If this flag is set, Helios will query all network apis in the community-maintained list 74 | at [ethpandaops/checkpoint-synz-health-checks](https://github.com/ethpandaops/checkpoint-sync-health-checks/blob/master/_data/endpoints.yaml) for their latest slots. 75 | The list of slots is filtered for healthy apis and the most frequent checkpoint occurring in the latest epoch will be returned. 76 | Note: this is a community-maintained list and thus no security guarantees are provided. Use this is a last resort if your checkpoint passed into `--checkpoint` fails. 77 | This is not recommended as malicious checkpoints can be returned from the listed apis, even if they are considered _healthy_. 78 | This can be run like so: `helios --load-external-fallback` (or `helios -l` with the shorthand). 79 | 80 | `--strict-checkpoint-age` or `-s` enables strict checkpoint age checking. If the checkpoint is over two weeks old and this flag is enabled, Helios will error. Without this flag, Helios will instead surface a warning to the user and continue. If the checkpoint is greater than two weeks old, there are theoretical attacks that can cause Helios and over light clients to sync incorrectly. These attacks are complex and expensive, so Helios disables this by default. 81 | 82 | `--help` or `-h` prints the help message. 83 | 84 | ### Configuration Files 85 | 86 | All configuration options can be set on a per-network level in `~/.helios/helios.toml`. Here is an example config file: 87 | 88 | ```toml 89 | [mainnet] 90 | consensus_rpc = "https://www.lightclientdata.org" 91 | execution_rpc = "https://eth-mainnet.g.alchemy.com/v2/XXXXX" 92 | checkpoint = "0x85e6151a246e8fdba36db27a0c7678a575346272fe978c9281e13a8b26cdfa68" 93 | 94 | [goerli] 95 | consensus_rpc = "http://testing.prater.beacon-api.nimbus.team" 96 | execution_rpc = "https://eth-goerli.g.alchemy.com/v2/XXXXX" 97 | checkpoint = "0xb5c375696913865d7c0e166d87bc7c772b6210dc9edf149f4c7ddc6da0dd4495" 98 | ``` 99 | 100 | A comprehensive breakdown of config options is available in the [config.md](./config.md) file. 101 | 102 | 103 | ### Using Helios as a Library 104 | 105 | Helios can be imported into any Rust project. Helios requires the Rust nightly toolchain to compile. 106 | 107 | ```rust 108 | use std::{str::FromStr, env}; 109 | 110 | use helios::{client::ClientBuilder, config::networks::Network, types::BlockTag}; 111 | use ethers::{types::Address, utils}; 112 | use eyre::Result; 113 | 114 | #[tokio::main] 115 | async fn main() -> Result<()> { 116 | let untrusted_rpc_url = env::var("UNTRUSTED_RPC_URL")?; 117 | 118 | let mut client = ClientBuilder::new() 119 | .network(Network::MAINNET) 120 | .consensus_rpc("https://www.lightclientdata.org") 121 | .execution_rpc(&untrusted_rpc_url) 122 | .build()?; 123 | 124 | client.start().await?; 125 | 126 | let head_block_num = client.get_block_number().await?; 127 | let addr = Address::from_str("0x00000000219ab540356cBB839Cbe05303d7705Fa")?; 128 | let block = BlockTag::Latest; 129 | let balance = client.get_balance(&addr, block).await?; 130 | 131 | println!("synced up to block: {}", head_block_num); 132 | println!("balance of deposit contract: {}", utils::format_ether(balance)); 133 | 134 | Ok(()) 135 | } 136 | ``` 137 | 138 | Below we demonstrate fetching checkpoints from the community-maintained list of checkpoint sync apis maintained by [ethPandaOps](https://github.com/ethpandaops/checkpoint-sync-health-checks/blob/master/_data/endpoints.yaml). 139 | 140 | > **Warning** 141 | > 142 | > This is a community-maintained list and thus no security guarantees are provided. Attacks on your light client can occur if malicious checkpoints are set in the list. Please use the explicit `checkpoint` flag, environment variable, or config setting with an updated, and verified checkpoint. 143 | 144 | ```rust 145 | use eyre::Result; 146 | use helios::config::{checkpoints, networks}; 147 | 148 | #[tokio::main] 149 | async fn main() -> Result<()> { 150 | // Construct the checkpoint fallback services 151 | let cf = checkpoints::CheckpointFallback::new().build().await.unwrap(); 152 | 153 | // Fetch the latest goerli checkpoint 154 | let goerli_checkpoint = cf.fetch_latest_checkpoint(&networks::Network::GOERLI).await.unwrap(); 155 | println!("Fetched latest goerli checkpoint: {}", goerli_checkpoint); 156 | 157 | // Fetch the latest mainnet checkpoint 158 | let mainnet_checkpoint = cf.fetch_latest_checkpoint(&networks::Network::MAINNET).await.unwrap(); 159 | println!("Fetched latest mainnet checkpoint: {}", mainnet_checkpoint); 160 | 161 | Ok(()) 162 | } 163 | ``` 164 | 165 | ## Architecture 166 | ```mermaid 167 | graph LR 168 | 169 | Client ----> Rpc 170 | Client ----> Node 171 | Node ----> ConsensusClient 172 | Node ----> ExecutionClient 173 | ExecutionClient ----> ExecutionRpc 174 | ConsensusClient ----> ConsensusRpc 175 | Node ----> Evm 176 | Evm ----> ExecutionClient 177 | ExecutionRpc --> UntrustedExecutionRpc 178 | ConsensusRpc --> UntrustedConsensusRpc 179 | 180 | classDef node fill:#f9f,stroke:#333,stroke-width:4px, color:black; 181 | class Node,Client node 182 | classDef execution fill:#f0f,stroke:#333,stroke-width:4px; 183 | class ExecutionClient,ExecutionRpc execution 184 | classDef consensus fill:#ff0,stroke:#333,stroke-width:4px; 185 | class ConsensusClient,ConsensusRpc consensus 186 | classDef evm fill:#0ff,stroke:#333,stroke-width:4px; 187 | class Evm evm 188 | classDef providerC fill:#ffc 189 | class UntrustedConsensusRpc providerC 190 | classDef providerE fill:#fbf 191 | class UntrustedExecutionRpc providerE 192 | classDef rpc fill:#e10 193 | class Rpc rpc 194 | 195 | 196 | subgraph "External Network" 197 | UntrustedExecutionRpc 198 | UntrustedConsensusRpc 199 | end 200 | ``` 201 | 202 | ## Benchmarks 203 | 204 | Benchmarks are defined in the [benches](./benches/) subdirectory. They are built using the [criterion](https://github.com/bheisler/criterion.rs) statistics-driven benchmarking library. 205 | 206 | To run all benchmarks, you can use `cargo bench`. To run a specific benchmark, you can use `cargo bench --bench `, where `` is one of the benchmarks defined in the [Cargo.toml](./Cargo.toml) file under a `[[bench]]` section. 207 | 208 | ## Contributing 209 | 210 | All contributions to Helios are welcome. Before opening a PR, please submit an issue detailing the bug or feature. When opening a PR, please ensure that your contribution builds on the nightly rust toolchain, has been linted with `cargo fmt`, and contains tests when applicable. 211 | 212 | ## Telegram 213 | 214 | If you are having trouble with Helios or are considering contributing, feel free to join our telegram [here](https://t.me/+IntDY_gZJSRkNTJj). 215 | 216 | ## Disclaimer 217 | 218 | _This code is being provided as is. No guarantee, representation or warranty is being made, express or implied, as to the safety or correctness of the code. It has not been audited and as such there can be no assurance it will work as intended, and users may experience delays, failures, errors, omissions or loss of transmitted information. Nothing in this repo should be construed as investment advice or legal advice for any particular facts or circumstances and is not meant to replace competent counsel. It is strongly advised for you to contact a reputable attorney in your jurisdiction for any questions or concerns with respect thereto. a16z is not liable for any use of the foregoing, and users should proceed with caution and use at their own risk. See a16z.com/disclosures for more info._ 219 | -------------------------------------------------------------------------------- /client/src/rpc.rs: -------------------------------------------------------------------------------- 1 | use ethers::{ 2 | abi::AbiEncode, 3 | types::{Address, Filter, Log, SyncingStatus, Transaction, TransactionReceipt, H256, U256}, 4 | }; 5 | use eyre::Result; 6 | use log::info; 7 | use std::{fmt::Display, net::SocketAddr, str::FromStr, sync::Arc}; 8 | use tokio::sync::RwLock; 9 | 10 | use jsonrpsee::{ 11 | core::{async_trait, server::rpc_module::Methods, Error}, 12 | http_server::{HttpServerBuilder, HttpServerHandle}, 13 | proc_macros::rpc, 14 | }; 15 | 16 | use crate::{errors::NodeError, node::Node}; 17 | 18 | use common::{ 19 | types::BlockTag, 20 | utils::{hex_str_to_bytes, u64_to_hex_string}, 21 | }; 22 | use execution::types::{CallOpts, ExecutionBlock}; 23 | 24 | pub struct Rpc { 25 | node: Arc>, 26 | handle: Option, 27 | port: u16, 28 | } 29 | 30 | impl Rpc { 31 | pub fn new(node: Arc>, port: u16) -> Self { 32 | Rpc { 33 | node, 34 | handle: None, 35 | port, 36 | } 37 | } 38 | 39 | pub async fn start(&mut self) -> Result { 40 | let rpc_inner = RpcInner { 41 | node: self.node.clone(), 42 | port: self.port, 43 | }; 44 | 45 | let (handle, addr) = start(rpc_inner).await?; 46 | self.handle = Some(handle); 47 | 48 | info!("rpc server started at {}", addr); 49 | 50 | Ok(addr) 51 | } 52 | } 53 | 54 | #[rpc(server, namespace = "eth")] 55 | trait EthRpc { 56 | #[method(name = "getBalance")] 57 | async fn get_balance(&self, address: &str, block: BlockTag) -> Result; 58 | #[method(name = "getTransactionCount")] 59 | async fn get_transaction_count(&self, address: &str, block: BlockTag) -> Result; 60 | #[method(name = "getBlockTransactionCountByHash")] 61 | async fn get_block_transaction_count_by_hash(&self, hash: &str) -> Result; 62 | #[method(name = "getBlockTransactionCountByNumber")] 63 | async fn get_block_transaction_count_by_number(&self, block: BlockTag) 64 | -> Result; 65 | #[method(name = "getCode")] 66 | async fn get_code(&self, address: &str, block: BlockTag) -> Result; 67 | #[method(name = "call")] 68 | async fn call(&self, opts: CallOpts, block: BlockTag) -> Result; 69 | #[method(name = "estimateGas")] 70 | async fn estimate_gas(&self, opts: CallOpts) -> Result; 71 | #[method(name = "chainId")] 72 | async fn chain_id(&self) -> Result; 73 | #[method(name = "gasPrice")] 74 | async fn gas_price(&self) -> Result; 75 | #[method(name = "maxPriorityFeePerGas")] 76 | async fn max_priority_fee_per_gas(&self) -> Result; 77 | #[method(name = "blockNumber")] 78 | async fn block_number(&self) -> Result; 79 | #[method(name = "getBlockByNumber")] 80 | async fn get_block_by_number( 81 | &self, 82 | block: BlockTag, 83 | full_tx: bool, 84 | ) -> Result, Error>; 85 | #[method(name = "getBlockByHash")] 86 | async fn get_block_by_hash( 87 | &self, 88 | hash: &str, 89 | full_tx: bool, 90 | ) -> Result, Error>; 91 | #[method(name = "sendRawTransaction")] 92 | async fn send_raw_transaction(&self, bytes: &str) -> Result; 93 | #[method(name = "getTransactionReceipt")] 94 | async fn get_transaction_receipt( 95 | &self, 96 | hash: &str, 97 | ) -> Result, Error>; 98 | #[method(name = "getTransactionByHash")] 99 | async fn get_transaction_by_hash(&self, hash: &str) -> Result, Error>; 100 | #[method(name = "getTransactionByBlockHashAndIndex")] 101 | async fn get_transaction_by_block_hash_and_index( 102 | &self, 103 | hash: &str, 104 | index: usize, 105 | ) -> Result, Error>; 106 | #[method(name = "getLogs")] 107 | async fn get_logs(&self, filter: Filter) -> Result, Error>; 108 | #[method(name = "getStorageAt")] 109 | async fn get_storage_at( 110 | &self, 111 | address: &str, 112 | slot: H256, 113 | block: BlockTag, 114 | ) -> Result; 115 | #[method(name = "getCoinbase")] 116 | async fn get_coinbase(&self) -> Result; 117 | #[method(name = "syncing")] 118 | async fn syncing(&self) -> Result; 119 | } 120 | 121 | #[rpc(client, server, namespace = "net")] 122 | trait NetRpc { 123 | #[method(name = "version")] 124 | async fn version(&self) -> Result; 125 | } 126 | 127 | #[derive(Clone)] 128 | struct RpcInner { 129 | node: Arc>, 130 | port: u16, 131 | } 132 | 133 | #[async_trait] 134 | impl EthRpcServer for RpcInner { 135 | async fn get_balance(&self, address: &str, block: BlockTag) -> Result { 136 | let address = convert_err(Address::from_str(address))?; 137 | let node = self.node.read().await; 138 | let balance = convert_err(node.get_balance(&address, block).await)?; 139 | 140 | Ok(format_hex(&balance)) 141 | } 142 | 143 | async fn get_transaction_count(&self, address: &str, block: BlockTag) -> Result { 144 | let address = convert_err(Address::from_str(address))?; 145 | let node = self.node.read().await; 146 | let nonce = convert_err(node.get_nonce(&address, block).await)?; 147 | 148 | Ok(format!("0x{nonce:x}")) 149 | } 150 | 151 | async fn get_block_transaction_count_by_hash(&self, hash: &str) -> Result { 152 | let hash = convert_err(hex_str_to_bytes(hash))?; 153 | let node = self.node.read().await; 154 | let transaction_count = convert_err(node.get_block_transaction_count_by_hash(&hash))?; 155 | 156 | Ok(u64_to_hex_string(transaction_count)) 157 | } 158 | 159 | async fn get_block_transaction_count_by_number( 160 | &self, 161 | block: BlockTag, 162 | ) -> Result { 163 | let node = self.node.read().await; 164 | let transaction_count = convert_err(node.get_block_transaction_count_by_number(block))?; 165 | Ok(u64_to_hex_string(transaction_count)) 166 | } 167 | 168 | async fn get_code(&self, address: &str, block: BlockTag) -> Result { 169 | let address = convert_err(Address::from_str(address))?; 170 | let node = self.node.read().await; 171 | let code = convert_err(node.get_code(&address, block).await)?; 172 | 173 | Ok(format!("0x{:}", hex::encode(code))) 174 | } 175 | 176 | async fn call(&self, opts: CallOpts, block: BlockTag) -> Result { 177 | let node = self.node.read().await; 178 | 179 | let res = node 180 | .call(&opts, block) 181 | .await 182 | .map_err(NodeError::to_json_rpsee_error)?; 183 | 184 | Ok(format!("0x{}", hex::encode(res))) 185 | } 186 | 187 | async fn estimate_gas(&self, opts: CallOpts) -> Result { 188 | let node = self.node.read().await; 189 | let gas = node 190 | .estimate_gas(&opts) 191 | .await 192 | .map_err(NodeError::to_json_rpsee_error)?; 193 | 194 | Ok(u64_to_hex_string(gas)) 195 | } 196 | 197 | async fn chain_id(&self) -> Result { 198 | let node = self.node.read().await; 199 | let id = node.chain_id(); 200 | Ok(u64_to_hex_string(id)) 201 | } 202 | 203 | async fn gas_price(&self) -> Result { 204 | let node = self.node.read().await; 205 | let gas_price = convert_err(node.get_gas_price())?; 206 | Ok(format_hex(&gas_price)) 207 | } 208 | 209 | async fn max_priority_fee_per_gas(&self) -> Result { 210 | let node = self.node.read().await; 211 | let tip = convert_err(node.get_priority_fee())?; 212 | Ok(format_hex(&tip)) 213 | } 214 | 215 | async fn block_number(&self) -> Result { 216 | let node = self.node.read().await; 217 | let num = convert_err(node.get_block_number())?; 218 | Ok(u64_to_hex_string(num)) 219 | } 220 | 221 | async fn get_block_by_number( 222 | &self, 223 | block: BlockTag, 224 | full_tx: bool, 225 | ) -> Result, Error> { 226 | let node = self.node.read().await; 227 | let block = convert_err(node.get_block_by_number(block, full_tx).await)?; 228 | Ok(block) 229 | } 230 | 231 | async fn get_block_by_hash( 232 | &self, 233 | hash: &str, 234 | full_tx: bool, 235 | ) -> Result, Error> { 236 | let hash = convert_err(hex_str_to_bytes(hash))?; 237 | let node = self.node.read().await; 238 | let block = convert_err(node.get_block_by_hash(&hash, full_tx).await)?; 239 | Ok(block) 240 | } 241 | 242 | async fn send_raw_transaction(&self, bytes: &str) -> Result { 243 | let node = self.node.read().await; 244 | let bytes = convert_err(hex_str_to_bytes(bytes))?; 245 | let tx_hash = convert_err(node.send_raw_transaction(&bytes).await)?; 246 | Ok(hex::encode(tx_hash)) 247 | } 248 | 249 | async fn get_transaction_receipt( 250 | &self, 251 | hash: &str, 252 | ) -> Result, Error> { 253 | let node = self.node.read().await; 254 | let hash = H256::from_slice(&convert_err(hex_str_to_bytes(hash))?); 255 | let receipt = convert_err(node.get_transaction_receipt(&hash).await)?; 256 | Ok(receipt) 257 | } 258 | 259 | async fn get_transaction_by_hash(&self, hash: &str) -> Result, Error> { 260 | let node = self.node.read().await; 261 | let hash = H256::from_slice(&convert_err(hex_str_to_bytes(hash))?); 262 | convert_err(node.get_transaction_by_hash(&hash).await) 263 | } 264 | 265 | async fn get_transaction_by_block_hash_and_index( 266 | &self, 267 | hash: &str, 268 | index: usize, 269 | ) -> Result, Error> { 270 | let hash = convert_err(hex_str_to_bytes(hash))?; 271 | let node = self.node.read().await; 272 | convert_err( 273 | node.get_transaction_by_block_hash_and_index(&hash, index) 274 | .await, 275 | ) 276 | } 277 | 278 | async fn get_coinbase(&self) -> Result { 279 | let node = self.node.read().await; 280 | Ok(node.get_coinbase().unwrap()) 281 | } 282 | 283 | async fn syncing(&self) -> Result { 284 | let node = self.node.read().await; 285 | convert_err(node.syncing()) 286 | } 287 | 288 | async fn get_logs(&self, filter: Filter) -> Result, Error> { 289 | let node = self.node.read().await; 290 | convert_err(node.get_logs(&filter).await) 291 | } 292 | 293 | async fn get_storage_at( 294 | &self, 295 | address: &str, 296 | slot: H256, 297 | block: BlockTag, 298 | ) -> Result { 299 | let address = convert_err(Address::from_str(address))?; 300 | let node = self.node.read().await; 301 | let storage = convert_err(node.get_storage_at(&address, slot, block).await)?; 302 | 303 | Ok(format_hex(&storage)) 304 | } 305 | } 306 | 307 | #[async_trait] 308 | impl NetRpcServer for RpcInner { 309 | async fn version(&self) -> Result { 310 | let node = self.node.read().await; 311 | Ok(node.chain_id().to_string()) 312 | } 313 | } 314 | 315 | async fn start(rpc: RpcInner) -> Result<(HttpServerHandle, SocketAddr)> { 316 | let addr = format!("127.0.0.1:{}", rpc.port); 317 | let server = HttpServerBuilder::default().build(addr).await?; 318 | 319 | let addr = server.local_addr()?; 320 | 321 | let mut methods = Methods::new(); 322 | let eth_methods: Methods = EthRpcServer::into_rpc(rpc.clone()).into(); 323 | let net_methods: Methods = NetRpcServer::into_rpc(rpc).into(); 324 | 325 | methods.merge(eth_methods)?; 326 | methods.merge(net_methods)?; 327 | 328 | let handle = server.start(methods)?; 329 | 330 | Ok((handle, addr)) 331 | } 332 | 333 | fn convert_err(res: Result) -> Result { 334 | res.map_err(|err| Error::Custom(err.to_string())) 335 | } 336 | 337 | fn format_hex(num: &U256) -> String { 338 | let stripped = num 339 | .encode_hex() 340 | .strip_prefix("0x") 341 | .unwrap() 342 | .trim_start_matches('0') 343 | .to_string(); 344 | 345 | let stripped = if stripped.is_empty() { 346 | "0".to_string() 347 | } else { 348 | stripped 349 | }; 350 | 351 | format!("0x{stripped}") 352 | } 353 | --------------------------------------------------------------------------------