Compare commits

...

67 Commits

Author SHA1 Message Date
Noah Citron 2e6b948c8f
feat(cli): add version cli flag (#216) 2023-03-15 16:04:09 -04:00
Noah Citron f5760f7a8a
chore: bump version to 0.3.0 (#215) 2023-03-15 15:40:01 -04:00
Noah Citron ba3f31111f
feat: support capella (#214)
* handle forks using superstructs

* fix ssz for superstructs

* add capella types

* fix tests

* clippy

* clippy take two
2023-03-15 00:46:00 -04:00
refcell.eth 3afa312776
feat(flamegraphs): Introduce Flamegraphs (#138)
* 🔥 flamegraphs

* 📝 flamegraph blog post :chain:

* client get_fee_history

* node get_fee_history

* errors: InvalidBaseGaseFee

* execution get_fee_history

* http rpc get_fee_history

* moc rpc get_fee_history and json file

* module add get_fee_history

* update exec

* test feehistory

* update execution with logging + better logic

* fee history config loader

* rust fmt client

* rustfmt node

* rustfmt error

* rustfmt execution

* rustfmt http and moc rpc

* rustfmt mod.rs

* fee history formating

* correct typos

* use env var

* InvalidGasUsedRatio error

* check gas used ratio

* remove logging

* Update execution/src/errors.rs

Co-authored-by: refcell.eth <abigger87@gmail.com>

* Update execution/src/execution.rs

Co-authored-by: refcell.eth <abigger87@gmail.com>

* adding block and payload errors

* using error

* handle error in test

* fix: evm panic on slot not found (#208)

* fixes, but test fails

* fix: cleanup and example (#210)

* clean up fee history

* bump time dep in chrono, thx dependabot

* add benches to pr

* sleep

* fmt 

* place benching behind a man flag

* cleanup flamegraphs

* cleanup

---------

Co-authored-by: SFYLL <santiagoflood@hotmail.fr>
Co-authored-by: SFYLL <39958632+SFYLL@users.noreply.github.com>
2023-03-13 18:12:44 -04:00
Noah Citron 5a32f30686
feat: fee history (#211)
* client get_fee_history

* node get_fee_history

* errors: InvalidBaseGaseFee

* execution get_fee_history

* http rpc get_fee_history

* moc rpc get_fee_history and json file

* module add get_fee_history

* update exec

* test feehistory

* update execution with logging + better logic

* fee history config loader

* rust fmt client

* rustfmt node

* rustfmt error

* rustfmt execution

* rustfmt http and moc rpc

* rustfmt mod.rs

* fee history formating

* correct typos

* use env var

* InvalidGasUsedRatio error

* check gas used ratio

* remove logging

* Update execution/src/errors.rs

Co-authored-by: refcell.eth <abigger87@gmail.com>

* Update execution/src/execution.rs

Co-authored-by: refcell.eth <abigger87@gmail.com>

* adding block and payload errors

* using error

* handle error in test

* fix: evm panic on slot not found (#208)

* fixes, but test fails

* fix: cleanup and example (#210)

* clean up fee history

* bump time dep in chrono, thx dependabot

* add benches to pr

* sleep

* fmt 

* place benching behind a man flag

---------

Co-authored-by: SFYLL <santiagoflood@hotmail.fr>
Co-authored-by: SFYLL <39958632+SFYLL@users.noreply.github.com>
Co-authored-by: refcell.eth <abigger87@gmail.com>
2023-03-11 01:59:29 -05:00
Noah Citron 3c471c2bef
fix: cleanup call example (#212)
* fix: evm panic on slot not found (#208)

* cleanup and example

---------

Co-authored-by: refcell.eth <abigger87@gmail.com>
2023-03-11 01:58:42 -05:00
refcell.eth a73f9c648b
fix: Optional CallOpts Recipient (#207) 2023-03-10 11:05:34 -05:00
Noah Citron 0e20b5f783
fix: remove FileDB export for wasm build (#203) 2023-03-08 16:25:22 -05:00
Noah Citron 8da632f8f2
fix: filter invalid checkpoints from fallback (#196)
* fix: filter invalid checkpoints from fallback

* use iter find
2023-02-12 14:57:24 -05:00
Noah Citron ef5a6a216f
fix: correct zero hex formatting (#195)
* fix: correct zero hex formatting

* clippy
2023-02-12 12:16:11 -05:00
Giovanni Vignone 32d09736e0
feat: add eth_syncing (#188)
* adding documentation for rpc.md

* adding rpc methods in table for rpc.md

* adjusting readme to link to rpc.md

* fixing grammar

* grammar

* adding RPC Methods according to documentation and listing column as Client Function

* adding more description space

* undoing description spacing

* adding get block transaction count by hash to node.rs and rpc.rs

* functioning getblocktransactioncountbyhash function

* removing documentation

* adding second rpc method and simplifying logic

* adjusting example and node.rs

* formatting

* fixing clippy errors

* adding to client and to rpc.md

* formatting

* integrating into client

* u64 return types, rpc.md updated to get_nonce, get_transaction_count -> get_nonce revert

* cargo fmt

* readme architecture init

* removing blockchain

* removing complexity

* updating mermaid with links from evm -> execution, renaming, and recoloring

* coloring letters black

* removing uncessary styling and adding untrustedexecutionrpc and untrustedconsensusrpc

* initial syncing work

* adding in proper computations to syncing()

* simplificaiton of logic

* cargo fmt

* updaing rpc.md and rpc

* removing unwrapping
2023-02-11 18:05:22 -05:00
Noah Citron da520290ce
fix: embedded wasm binary (#193)
* fix: embedded wasm binary

* remove public path

* remove unused comments from run.sh
2023-02-10 12:47:57 -05:00
Noah Citron 8e006d623b
feat: typescript bindings (#191)
* basic ethers provider setup

* add getCode

* add call

* add estimateGas

* add gas pricing methods

* add sendRawTransaction

* add getTransactionReceipt

* add getLogs

* add net_version

* decouple ethers from lib

* add config options

* fmt
2023-02-09 14:32:17 -05:00
Noah Citron 4066828387
fix: prevent logs for unseen blocks (#192) 2023-02-08 18:12:41 -05:00
christn 1b1a540340
feat: backfill payloads (#189)
* Loop over all missing slots since last update

* Adjust get_block_header function to allow getting headers of past blocks

* Compare parent hashes when backfilling blocks

* Backfill blocks concurrently

* Do not rehash backfilled blocks

* Revert "Adjust get_block_header function to allow getting headers of past blocks"

This reverts commit 5895118046.

* Move get_payloads to consensus module

* Continue with the next block instead of request failure to recover from skipped blocks

* clippy and rustfmt

* clippy

* Remove redundant get_block_from_rpc method
2023-02-07 17:36:29 -05:00
danilowhk a0032835f3
feat: make ConfigDB public (#190)
* Update lib.rs

* format
2023-02-03 15:35:10 -05:00
Noah Citron 4a84ecabed
fix: handle checkpoint overrides correctly (#187)
* fix: handle checkpoint overrides correctly

* fix optional deserialize

* clippy

* fmt
2023-02-01 17:10:26 -05:00
Noah Citron 1fa2dede25
fix: add data dir to basic example (#186) 2023-02-01 13:40:11 -05:00
Noah Citron 7b7dc708f8
fix: check checkpoint block root exists (#185)
* fix: check checkpoint block root is some

* remove print

* check block root earlier
2023-01-31 18:40:53 -05:00
Noah Citron 6b662f903b
fix: benches (#184) 2023-01-31 18:23:55 -05:00
Noah Citron 5b9e90436a
fix: check checkpoint file exists (#183) 2023-01-31 14:18:51 -05:00
Noah Citron 72267b4563
feat: wasm support (#182)
* basic consensus setup

* basic execution setup

* patch for wasm

* basic wasm client

* proxy cors for testing

* migrate to webpack

* use typescript

* track chain head

* rename to helios-ts

* better build instructions

* add getCode

* builds everywhere

* add wasm-pack to dependencies

* compile for both wasm and non-wasm

* fix deps

* fix deps

* remove ds store

* add blocktags

* add getNonce

* use BTreeMap to store payloads

* add getTransaction

* switch to proper ethers provider

* post merge fixes

* compile client to wasm

* fix tests

* fmt

* use milagro for bls

* handle node advance in rust

* faster bls deserialization

* clippy

* add ConfigDB

* remove ts bindings

* fix gitignore

* remove ts workspace member

* remove unused mut

* uncomment old deletions

* bump to 0.2.0
2023-01-30 21:38:46 -05:00
James Prestwich 604b325983
fix: specify precise nightly version (#179) 2023-01-24 17:30:03 -05:00
Noah Citron 2c5c318529
feat: check consensus rpc network (#177)
* feat: check consensus rpc network

* cleanup
2023-01-23 10:07:11 -05:00
Noah Citron de90eb9158
feat: check execution rpc network (#176)
* feat: check execution rpc network

* clippy
2023-01-22 11:58:55 -05:00
Noah Citron 10e39eb35a
chore: bump version to 0.1.3 (#173) 2023-01-19 19:23:06 -05:00
Noah Citron 4d6568a8a5
fix: correct example names in cargo (#172) 2023-01-18 21:45:29 -05:00
Noah Citron 4d1ca1d6ed
chore: rename examples (#171) 2023-01-18 21:23:47 -05:00
Noah Citron cb6cf75d59
feat: make checkpoint age check optional (#170)
* feat: make checkpoint age check optional

* add new flag to readme

* fix tests
2023-01-18 21:18:26 -05:00
Noah Citron 69b8108dae
fix: deserialize wrapped lc headers correctly (#169)
* fix: deserialize wrapped lc headers correctly

* clippy
2023-01-18 19:42:56 -05:00
danilowhk 819ee702e8
feat : eth_coinbase implementation #157 (#167)
* ethereum_get_cooinbase

* change return type from CoinbaseAddress to Address

* format
2023-01-12 20:30:15 -05:00
Dimitris Apostolou c7a1bad8e5
fix: typos (#164) 2023-01-10 15:47:58 -05:00
danilowhk 9d69c2b2b4
feat : add eth_getTransactionByBlockHashAndIndex (#157)
* eth_getTransactionByBlockHashAndIndex

* format

* clippy changes

* test check
2023-01-03 09:45:56 -05:00
Giovanni Vignone ec4beb38e9
docs: architectural Diagram (#161)
* adding documentation for rpc.md

* adding rpc methods in table for rpc.md

* adjusting readme to link to rpc.md

* fixing grammar

* grammar

* adding RPC Methods according to documentation and listing column as Client Function

* adding more description space

* undoing description spacing

* adding get block transaction count by hash to node.rs and rpc.rs

* functioning getblocktransactioncountbyhash function

* removing documentation

* adding second rpc method and simplifying logic

* adjusting example and node.rs

* formatting

* fixing clippy errors

* adding to client and to rpc.md

* formatting

* integrating into client

* u64 return types, rpc.md updated to get_nonce, get_transaction_count -> get_nonce revert

* cargo fmt

* readme architecture init

* removing blockchain

* removing complexity

* updating mermaid with links from evm -> execution, renaming, and recoloring

* coloring letters black

* removing uncessary styling and adding untrustedexecutionrpc and untrustedconsensusrpc
2022-12-30 16:28:46 -05:00
ControlCplusControlV ac8a145ae3
feat: reduce binary size (#160)
* reduced bin size

* Update README.md
2022-12-23 21:14:21 -05:00
Noah Citron f8275f054e
fix: handle calls to eoa (#159) 2022-12-21 19:11:43 -05:00
Mathieu bfe44809d8
chore: make execution types clonable (#156) 2022-12-18 10:27:57 -05:00
danilowhk aa838aeee1
chore: add checkpoints in export config mod (#149)
* add checkpoints in export config mod

* add checkpoints
2022-12-14 16:38:19 -05:00
Mathieu af2a12dd57
chore: export src/execution types (#148) 2022-12-14 14:37:15 -05:00
Mathieu 9f53fc9bfc
chore: export ExecutionBlock type (#146)
* export ExecutionBlock type

* fix linting error

* Format with cargo fmt
2022-12-14 11:43:43 -05:00
refcell.eth 4707a0a9d4
feat(docs): Config (#140)
* 📝 comprehensive config options

* 🔨 remove forks and chain parameters
2022-12-14 00:10:24 -05:00
Noah Citron 56f9d89525
fix(ci): move benchmark ci to separate action (#143) 2022-12-13 20:22:53 -05:00
Giovanni Vignone c26e393b7d
feat: add tx length rpc methods (#142)
* adding documentation for rpc.md

* adding rpc methods in table for rpc.md

* adjusting readme to link to rpc.md

* fixing grammar

* grammar

* adding RPC Methods according to documentation and listing column as Client Function

* adding more description space

* undoing description spacing

* adding get block transaction count by hash to node.rs and rpc.rs

* functioning getblocktransactioncountbyhash function

* removing documentation

* adding second rpc method and simplifying logic

* adjusting example and node.rs

* formatting

* fixing clippy errors

* adding to client and to rpc.md

* formatting

* integrating into client

* u64 return types, rpc.md updated to get_nonce, get_transaction_count -> get_nonce revert

* cargo fmt
2022-12-13 19:19:36 -05:00
refcell.eth 94bf458d94
feat(benches): Criterion + Iai Benchmarking [RFC] (#131)
* ⚙️ benches

* 📝 docs

* 🏗️ file_db benches and checkpoint fixes

* 🔨 fix github action env vars

*  benchmark env vars

* ⚙️ sync benchmarks

*  cargo fmt touchups
2022-12-11 14:42:52 -05:00
Giovanni Vignone f37aa2aa45
documentation: add rpc docs (#136)
* adding documentation for rpc.md

* adding rpc methods in table for rpc.md

* adjusting readme to link to rpc.md

* fixing grammar

* grammar

* adding RPC Methods according to documentation and listing column as Client Function

* adding more description space

* undoing description spacing
2022-12-11 11:45:34 -05:00
sragss d8db74ede9
fix: surface eth_getStorageAt (#124)
* feat: surface eth_getStorageAt

* add blocktag

* cargo fmt
2022-12-08 10:57:21 -05:00
refcell.eth b449c1f674
fix(readme): Resolve Dependency Type Conflicts (#129)
* 🐛 resolve dependency type conflicts

* 🔨 fix inline payload construction in tests
2022-12-04 15:28:44 -05:00
sragss e132706f0b
fix: prepend 0x to eth_getCode responses (#125) 2022-12-02 18:42:58 -05:00
refcell.eth 7e3fe40613
feat(examples): Introduces working examples (#126)
* ⚙️ examples and more cleanup

* ⚙️ examples
2022-12-02 18:41:50 -05:00
HAOYUatHZ e8642fe521
build(github CI): replace `actions/cache@v2` with `Swatinem/rust-cache@v2` (#123) 2022-12-02 18:39:41 -05:00
HAOYUatHZ e4071fea6e
build(CI): add cargo clippy (#122) 2022-12-01 20:21:20 -05:00
andreas 161e0fbfb9
feat: checkpoint fallbacks (#120)
* 🏗️ checkpoint fallback initial impl

* 🚧 checkpoint fallbacks

*  checkpoint fallbacks

* ⚙️ fix result types

* ♻️ checkpoints refactoring

* 🔨 import nits

* 🚀 graceful checkpoint fallbacks

*  parallel checkpoint fallback service fetching using async tokio tasks

* 📝 readme touchups
2022-12-01 20:18:23 -05:00
Abdel @ StarkWare 4d721e86c3
chore: export CallOpts type (#118) 2022-11-30 08:48:12 -08:00
asnared 21c73c1649
feat: Lints, Clippy, and Cleaning (#115)
*  cargo fmt smells

* ♻️ cargo cleaning
2022-11-29 17:31:25 -08:00
Noah Citron 5e29149297
fix: remove leading zeros in rpc responses (#113) 2022-11-26 16:47:20 -05:00
HAOYUatHZ f74724763c
build(github CI): add cargo cache (#109) 2022-11-22 11:45:14 -05:00
HAOYUatHZ 81c2d6ddd7
build: add rust_toolchain (#110) 2022-11-22 11:32:08 -05:00
Simon Saliba 3177ad55c1
feat: add `get_logs` RPC method (#108)
* Implemented RPC method get_logs

* Limit the max number of logs to 5

* remove unused import

Co-authored-by: Noah Citron <noah@jeff.org>
2022-11-17 12:14:13 -05:00
0xModene eaf5605d4d
feat: verify checkpoint has valid age (#105)
* check blockhash has valid timestamp

* remove warn log

* made checkpoint age req configurable

* renamed method to make more sense

* fixed broken tests

* formatting

* unit tests completed

* removed needless imports

* renaming vars
2022-11-14 15:23:51 -05:00
sragss 23bb207f1a
refactor: promote constant; switch to list chunking (#107)
* promote constant; switch to list chunking

* cargo fmt

Co-authored-by: Noah Citron <noah@jeff.org>
2022-11-11 19:45:08 -05:00
sragss b9d67e956b
feat: surface revert errors to RPC (#106)
* surface revert errors to RPC

* remove temp generic errors from evm, node

* merge resolution

* cargo fmt
2022-11-11 19:41:37 -05:00
Pablo Carranza Vélez d13df518d6
fix: explicit check for invalid divergent paths (#100)
* fix: avoid computing a wrong shared prefix length in proofs

* fix: avoid looping over the node_path twice, and fix test

* fix: properly validate proofs with divergent paths

* fix: cargo fmt

* fix: typo

* fix: tokio tests must be async
2022-11-09 13:20:19 -05:00
dadepo 2dbe057e3a
refactor: add count parameter to get_update and use 128 as the limit (#97)
* Make get_update take count. Limit count to 128 as specified in the spec

* use u8 instead of u32

* run cargo fmt to fix formating

* fix compilation in tests

* moved constants module into the consensus package
2022-11-08 16:24:55 -05:00
guacamoleoverflow b5d1dbc638
chore: fix typos (#96) 2022-11-08 04:57:40 -05:00
Noah Citron 4c72344b55
documentation: update readme (#95) 2022-11-07 17:50:25 -05:00
Noah Citron 6ff501352b
fix: install instructions (#94) 2022-11-07 16:21:35 -05:00
Noah Citron 23d0e6f658
chore: bumb version (#93) 2022-11-07 13:42:40 -05:00
82 changed files with 10122 additions and 1337 deletions

26
.github/workflows/benchmarks.yml vendored Normal file
View File

@ -0,0 +1,26 @@
name: benchmarks
on:
workflow_bench:
# push:
# branches: [ "master" ]
env:
MAINNET_RPC_URL: ${{ secrets.MAINNET_RPC_URL }}
GOERLI_RPC_URL: ${{ secrets.GOERLI_RPC_URL }}
jobs:
benches:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: nightly
override: true
components: rustfmt
- uses: Swatinem/rust-cache@v2
- uses: actions-rs/cargo@v1
with:
command: bench

View File

@ -20,6 +20,8 @@ jobs:
- name: install target
run: rustup target add aarch64-apple-darwin
- uses: Swatinem/rust-cache@v2
- name: build
run: cargo build --package cli --release --target aarch64-apple-darwin
@ -57,6 +59,8 @@ jobs:
- name: install target
run: rustup target add x86_64-apple-darwin
- uses: Swatinem/rust-cache@v2
- name: build
run: cargo build --package cli --release --target x86_64-apple-darwin
@ -100,6 +104,8 @@ jobs:
sudo apt-get install -y gcc-aarch64-linux-gnu
echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc" >> $GITHUB_ENV
- uses: Swatinem/rust-cache@v2
- name: build
run: cargo build --package cli --release --target aarch64-unknown-linux-gnu
@ -137,6 +143,8 @@ jobs:
- name: install target
run: rustup target add x86_64-unknown-linux-gnu
- uses: Swatinem/rust-cache@v2
- name: build
run: cargo build --package cli --release --target x86_64-unknown-linux-gnu

View File

@ -6,48 +6,68 @@ on:
pull_request:
branches: [ "master" ]
env:
MAINNET_RPC_URL: ${{ secrets.MAINNET_RPC_URL }}
GOERLI_RPC_URL: ${{ secrets.GOERLI_RPC_URL }}
jobs:
check:
name: check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: nightly
override: true
- uses: Swatinem/rust-cache@v2
- uses: actions-rs/cargo@v1
with:
command: check
test:
name: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: nightly
override: true
- uses: Swatinem/rust-cache@v2
- uses: actions-rs/cargo@v1
with:
command: test
args: --all
fmt:
name: fmt
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: nightly
override: true
- run: rustup component add rustfmt
components: rustfmt
- uses: Swatinem/rust-cache@v2
- uses: actions-rs/cargo@v1
with:
command: fmt
args: --all -- --check
clippy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: nightly
override: true
components: clippy
- uses: Swatinem/rust-cache@v2
- uses: actions-rs/cargo@v1
with:
command: clippy
args: --all -- -D warnings

8
.gitignore vendored
View File

@ -1 +1,7 @@
/target
.DS_Store
target
*.env
helios-ts/node_modules
helios-ts/dist
helios-ts/helios-*.tgz

1971
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,10 +1,13 @@
[package]
name = "helios"
version = "0.0.1"
version = "0.3.0"
edition = "2021"
autobenches = false
exclude = [
"benches"
]
[workspace]
members = [
"cli",
"client",
@ -12,7 +15,12 @@ members = [
"config",
"consensus",
"execution",
"helios-ts",
]
default-members = ["cli"]
[profile.bench]
debug = true
[dependencies]
client = { path = "./client" }
@ -20,7 +28,68 @@ config = { path = "./config" }
common = { path = "./common" }
consensus = { path = "./consensus" }
execution = { path = "./execution" }
serde = { version = "1.0.154", features = ["derive"] }
[patch.crates-io]
ethers = { git = "https://github.com/ncitron/ethers-rs", branch = "fix-retry" }
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
tokio = { version = "1", features = ["full"] }
eyre = "0.6.8"
dirs = "4.0.0"
ethers = { version = "1.0.2", features = [ "abigen" ] }
env_logger = "0.9.0"
log = "0.4.17"
tracing-test = "0.2.4"
criterion = { version = "0.4", features = [ "async_tokio", "plotters" ]}
plotters = "0.3.4"
tempfile = "3.4.0"
hex = "0.4.3"
[profile.release]
strip = true
opt-level = "z"
lto = true
codegen-units = 1
panic = "abort"
######################################
# Examples
######################################
[[example]]
name = "checkpoints"
path = "examples/checkpoints.rs"
[[example]]
name = "basic"
path = "examples/basic.rs"
[[example]]
name = "client"
path = "examples/client.rs"
[[example]]
name = "config"
path = "examples/config.rs"
[[example]]
name = "call"
path = "examples/call.rs"
######################################
# Benchmarks
######################################
[[bench]]
name = "file_db"
harness = false
[[bench]]
name = "get_balance"
harness = false
[[bench]]
name = "get_code"
harness = false
[[bench]]
name = "sync"
harness = false

126
README.md
View File

@ -1,42 +1,76 @@
## Helios
[![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)
Helios is a fully trustless, efficient, and portable Ethereum light client written in Rust.
Helios converts an untrusted centralized RPC endoint 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.
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.
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.
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.
## Installing
First install `heliosup`, Helios's installer:
```
curl https://github.com/a16z/helios/blob/master/heliosup/install | bash
curl https://raw.githubusercontent.com/a16z/helios/master/heliosup/install | bash
```
To install Helios, run `heliosup`.
## Usage
To run Helios, run the below command, replacing `$ETH_RPC_URL` with an RPC provider URL such as Alchemy or Infura:
```
helios --execution-rpc $ETH_RPC_URL
```
`$ETH_RPC_URL` must be an Ethereum provider that supports the `eth_getProof` endpoint. Infura does not currently support this. We recommend using Alchemy.
Helios will now run a local RPC server at `http://127.0.0.1:8545`.
Helios provides examples in the [`examples/`](./examples/) directory. To run an example, you can execute `cargo run --example <example_name>` from inside the helios repository.
Helios also provides documentation of its supported RPC methods in the [rpc.md](./rpc.md) file.
### Warning
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.
### Additional Options
`--consensus-rpc` or `-c` can be used to set a custom consensus layer rpc endpoint. This must be a consenus node that supports the light client beaconchain api. We recomment using Nimbus for this. If no consensus rpc is supplied, it defaults to `https://www.lightclientdata.org` which is run by us.
`--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.
`--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.
`--network` or `-n` sets the network to sync to. Current valid option are `mainnet` and `goerli`, however users can add custom networks in their configurationf files.
`--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.
`--rpc-port` or `-p` sets the port that the local RPC should run on. The default value is `8545`.
`--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.
`--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.
If none is provided and the `--load-external-fallback` flag is not set, Helios will error.
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"`)
`--load-external-fallback` or `-l` enables weak subjectivity checkpoint fallback (no value needed).
For example, say you set a checkpoint value that is too outdated and Helios cannot sync to it.
If this flag is set, Helios will query all network apis in the community-maintained list
at [ethpandaops/checkpoint-synz-health-checks](https://github.com/ethpandaops/checkpoint-sync-health-checks/blob/master/_data/endpoints.yaml) for their latest slots.
The list of slots is filtered for healthy apis and the most frequent checkpoint occurring in the latest epoch will be returned.
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.
This is not recommended as malicious checkpoints can be returned from the listed apis, even if they are considered _healthy_.
This can be run like so: `helios --load-external-fallback` (or `helios -l` with the shorthand).
`--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.
`--help` or `-h` prints the help message.
### Configuration Files
All configuration options can be set on a per-network level in `~/.helios/helios.toml`. Here is an example config file:
```toml
[mainnet]
consensus_rpc = "https://www.lightclientdata.org"
@ -49,7 +83,11 @@ execution_rpc = "https://eth-goerli.g.alchemy.com/v2/XXXXX"
checkpoint = "0xb5c375696913865d7c0e166d87bc7c772b6210dc9edf149f4c7ddc6da0dd4495"
```
A comprehensive breakdown of config options is available in the [config.md](./config.md) file.
### Using Helios as a Library
Helios can be imported into any Rust project. Helios requires the Rust nightly toolchain to compile.
```rust
@ -83,11 +121,87 @@ async fn main() -> Result<()> {
}
```
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).
> **Warning**
>
> 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.
```rust
use eyre::Result;
use helios::config::{checkpoints, networks};
#[tokio::main]
async fn main() -> Result<()> {
// Construct the checkpoint fallback services
let cf = checkpoints::CheckpointFallback::new().build().await.unwrap();
// Fetch the latest goerli checkpoint
let goerli_checkpoint = cf.fetch_latest_checkpoint(&networks::Network::GOERLI).await.unwrap();
println!("Fetched latest goerli checkpoint: {}", goerli_checkpoint);
// Fetch the latest mainnet checkpoint
let mainnet_checkpoint = cf.fetch_latest_checkpoint(&networks::Network::MAINNET).await.unwrap();
println!("Fetched latest mainnet checkpoint: {}", mainnet_checkpoint);
Ok(())
}
```
## Architecture
```mermaid
graph LR
Client ----> Rpc
Client ----> Node
Node ----> ConsensusClient
Node ----> ExecutionClient
ExecutionClient ----> ExecutionRpc
ConsensusClient ----> ConsensusRpc
Node ----> Evm
Evm ----> ExecutionClient
ExecutionRpc --> UntrustedExecutionRpc
ConsensusRpc --> UntrustedConsensusRpc
classDef node fill:#f9f,stroke:#333,stroke-width:4px, color:black;
class Node,Client node
classDef execution fill:#f0f,stroke:#333,stroke-width:4px;
class ExecutionClient,ExecutionRpc execution
classDef consensus fill:#ff0,stroke:#333,stroke-width:4px;
class ConsensusClient,ConsensusRpc consensus
classDef evm fill:#0ff,stroke:#333,stroke-width:4px;
class Evm evm
classDef providerC fill:#ffc
class UntrustedConsensusRpc providerC
classDef providerE fill:#fbf
class UntrustedExecutionRpc providerE
classDef rpc fill:#e10
class Rpc rpc
subgraph "External Network"
UntrustedExecutionRpc
UntrustedConsensusRpc
end
```
## Benchmarks
Benchmarks are defined in the [benches](./benches/) subdirectory. They are built using the [criterion](https://github.com/bheisler/criterion.rs) statistics-driven benchmarking library.
To run all benchmarks, you can use `cargo bench`. To run a specific benchmark, you can use `cargo bench --bench <name>`, where `<name>` is one of the benchmarks defined in the [Cargo.toml](./Cargo.toml) file under a `[[bench]]` section.
To learn more about [helios](https://github.com/a16z/helios) benchmarking and to view benchmark flamegraphs, view the [benchmark readme](./benches/README.md).
## Contributing
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.
## Telegram
If you are having trouble with Helios or are considering contributing, feel free to join our telegram [here](https://t.me/+IntDY_gZJSRkNTJj).
## Disclaimer
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.
_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._

18
benches/README.md Normal file
View File

@ -0,0 +1,18 @@
# Helios Benchmarking
Helios performance is measured using [criterion](https://github.com/bheisler/criterion.rs) for comprehensive statistics-driven benchmarking.
Benchmarks are defined in the [benches](./) subdirectory and can be run using the cargo `bench` subcommand (eg `cargo bench`). To run a specific benchmark, you can use `cargo bench --bench <name>`, where `<name>` is one of the benchmarks defined in the [Cargo.toml](./Cargo.toml) file under a `[[bench]]` section.
#### Flamegraphs
[Flamegraph](https://github.com/brendangregg/FlameGraph) is a powerful rust crate for generating profile visualizations, that is graphing the time a program spends in each function. Functions called during execution are displayed as horizontal rectangles with the width proportional to the time spent in that function. As the call stack grows (think nested function invocations), the rectangles are stacked vertically. This provides a powerful visualization for quickly understanding which parts of a codebase take up disproportionate amounts of time.
Check out Brendan Gregg's [Flame Graphs](http://www.brendangregg.com/flamegraphs.html) blog post if you're interested in learning more about flamegraphs and performance visualizations in general.
To generate a flamegraph for helios, you can use the `cargo flamegraph` subcommand. For example, to generate a flamegraph for the [`client`](./examples/client.rs) example, you can run:
```bash
cargo flamegraph --example client -o ./flamegraphs/client.svg
```

51
benches/file_db.rs Normal file
View File

@ -0,0 +1,51 @@
use client::database::Database;
use config::Config;
use criterion::{criterion_group, criterion_main, Criterion};
use helios::prelude::FileDB;
use tempfile::tempdir;
mod harness;
criterion_main!(file_db);
criterion_group! {
name = file_db;
config = Criterion::default();
targets = save_checkpoint, load_checkpoint
}
/// Benchmark saving/writing a checkpoint to the file db.
pub fn save_checkpoint(c: &mut Criterion) {
c.bench_function("save_checkpoint", |b| {
let checkpoint = vec![1, 2, 3];
b.iter(|| {
let data_dir = Some(tempdir().unwrap().into_path());
let config = Config {
data_dir,
..Default::default()
};
let db = FileDB::new(&config).unwrap();
db.save_checkpoint(checkpoint.clone()).unwrap();
})
});
}
/// Benchmark loading a checkpoint from the file db.
pub fn load_checkpoint(c: &mut Criterion) {
c.bench_function("load_checkpoint", |b| {
// First write to the db
let data_dir = Some(tempdir().unwrap().into_path());
let config = Config {
data_dir,
..Default::default()
};
let db = FileDB::new(&config).unwrap();
let written_checkpoint = vec![1; 32];
db.save_checkpoint(written_checkpoint.clone()).unwrap();
// Then read from the db
b.iter(|| {
let checkpoint = db.load_checkpoint().unwrap();
assert_eq!(checkpoint, written_checkpoint.clone());
})
});
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 27 KiB

70
benches/get_balance.rs Normal file
View File

@ -0,0 +1,70 @@
use criterion::{criterion_group, criterion_main, Criterion};
use ethers::prelude::*;
use helios::types::BlockTag;
use std::str::FromStr;
mod harness;
criterion_main!(get_balance);
criterion_group! {
name = get_balance;
config = Criterion::default().sample_size(10);
targets = bench_mainnet_get_balance, bench_goerli_get_balance
}
/// Benchmark mainnet get balance.
/// Address: 0x00000000219ab540356cbb839cbe05303d7705fa (beacon chain deposit address)
pub fn bench_mainnet_get_balance(c: &mut Criterion) {
c.bench_function("get_balance", |b| {
// Create a new multi-threaded tokio runtime.
let rt = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.unwrap();
// Construct a mainnet client using our harness and tokio runtime.
let client = std::sync::Arc::new(harness::construct_mainnet_client(&rt).unwrap());
// Get the beacon chain deposit contract address.
let addr = Address::from_str("0x00000000219ab540356cbb839cbe05303d7705fa").unwrap();
let block = BlockTag::Latest;
// Execute the benchmark asynchronously.
b.to_async(rt).iter(|| async {
let inner = std::sync::Arc::clone(&client);
inner.get_balance(&addr, block).await.unwrap()
})
});
}
/// Benchmark goerli get balance.
/// Address: 0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6 (goerli weth)
pub fn bench_goerli_get_balance(c: &mut Criterion) {
c.bench_function("get_balance", |b| {
// Create a new multi-threaded tokio runtime.
let rt = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.unwrap();
// Construct a goerli client using our harness and tokio runtime.
let gc = match harness::construct_goerli_client(&rt) {
Ok(gc) => gc,
Err(e) => {
println!("failed to construct goerli client: {}", e);
std::process::exit(1);
}
};
let client = std::sync::Arc::new(gc);
// Get the beacon chain deposit contract address.
let addr = Address::from_str("0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6").unwrap();
let block = BlockTag::Latest;
// Execute the benchmark asynchronously.
b.to_async(rt).iter(|| async {
let inner = std::sync::Arc::clone(&client);
inner.get_balance(&addr, block).await.unwrap()
})
});
}

63
benches/get_code.rs Normal file
View File

@ -0,0 +1,63 @@
use criterion::{criterion_group, criterion_main, Criterion};
use ethers::prelude::*;
use helios::types::BlockTag;
use std::str::FromStr;
mod harness;
criterion_main!(get_code);
criterion_group! {
name = get_code;
config = Criterion::default().sample_size(10);
targets = bench_mainnet_get_code, bench_goerli_get_code
}
/// Benchmark mainnet get code call.
/// Address: 0x00000000219ab540356cbb839cbe05303d7705fa (beacon chain deposit address)
pub fn bench_mainnet_get_code(c: &mut Criterion) {
c.bench_function("get_code", |b| {
// Create a new multi-threaded tokio runtime.
let rt = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.unwrap();
// Construct a mainnet client using our harness and tokio runtime.
let client = std::sync::Arc::new(harness::construct_mainnet_client(&rt).unwrap());
// Get the beacon chain deposit contract address.
let addr = Address::from_str("0x00000000219ab540356cbb839cbe05303d7705fa").unwrap();
let block = BlockTag::Latest;
// Execute the benchmark asynchronously.
b.to_async(rt).iter(|| async {
let inner = std::sync::Arc::clone(&client);
inner.get_code(&addr, block).await.unwrap()
})
});
}
/// Benchmark goerli get code call.
/// Address: 0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6 (goerli weth)
pub fn bench_goerli_get_code(c: &mut Criterion) {
c.bench_function("get_code", |b| {
// Create a new multi-threaded tokio runtime.
let rt = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.unwrap();
// Construct a goerli client using our harness and tokio runtime.
let client = std::sync::Arc::new(harness::construct_goerli_client(&rt).unwrap());
// Get the beacon chain deposit contract address.
let addr = Address::from_str("0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6").unwrap();
let block = BlockTag::Latest;
// Execute the benchmark asynchronously.
b.to_async(rt).iter(|| async {
let inner = std::sync::Arc::clone(&client);
inner.get_code(&addr, block).await.unwrap()
})
});
}

113
benches/harness.rs Normal file
View File

@ -0,0 +1,113 @@
#![allow(dead_code)]
use std::{str::FromStr, sync::Arc};
use ::client::{database::ConfigDB, Client};
use ethers::{
abi::Address,
types::{H256, U256},
};
use helios::{client, config::networks, types::BlockTag};
/// Fetches the latest mainnet checkpoint from the fallback service.
///
/// Uses the [CheckpointFallback](config::CheckpointFallback).
/// The `build` method will fetch a list of [CheckpointFallbackService](config::CheckpointFallbackService)s from a community-mainained list by ethPandaOps.
/// This list is NOT guaranteed to be secure, but is provided in good faith.
/// The raw list can be found here: https://github.com/ethpandaops/checkpoint-sync-health-checks/blob/master/_data/endpoints.yaml
pub async fn fetch_mainnet_checkpoint() -> eyre::Result<H256> {
let cf = config::CheckpointFallback::new().build().await.unwrap();
cf.fetch_latest_checkpoint(&networks::Network::MAINNET)
.await
}
/// Constructs a mainnet [Client](client::Client) for benchmark usage.
///
/// Requires a [Runtime](tokio::runtime::Runtime) to be passed in by reference.
/// The client is parameterized with a [FileDB](client::FileDB).
/// It will also use the environment variable `MAINNET_RPC_URL` to connect to a mainnet node.
/// The client will use `https://www.lightclientdata.org` as the consensus RPC.
pub fn construct_mainnet_client(
rt: &tokio::runtime::Runtime,
) -> eyre::Result<client::Client<ConfigDB>> {
rt.block_on(inner_construct_mainnet_client())
}
pub async fn inner_construct_mainnet_client() -> eyre::Result<client::Client<ConfigDB>> {
let benchmark_rpc_url = std::env::var("MAINNET_RPC_URL")?;
let mut client = client::ClientBuilder::new()
.network(networks::Network::MAINNET)
.consensus_rpc("https://www.lightclientdata.org")
.execution_rpc(&benchmark_rpc_url)
.load_external_fallback()
.build()?;
client.start().await?;
Ok(client)
}
pub async fn construct_mainnet_client_with_checkpoint(
checkpoint: &str,
) -> eyre::Result<client::Client<ConfigDB>> {
let benchmark_rpc_url = std::env::var("MAINNET_RPC_URL")?;
let mut client = client::ClientBuilder::new()
.network(networks::Network::MAINNET)
.consensus_rpc("https://www.lightclientdata.org")
.execution_rpc(&benchmark_rpc_url)
.checkpoint(checkpoint)
.build()?;
client.start().await?;
Ok(client)
}
/// Create a tokio multi-threaded runtime.
///
/// # Panics
///
/// Panics if the runtime cannot be created.
pub fn construct_runtime() -> tokio::runtime::Runtime {
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.unwrap()
}
/// Constructs a goerli client for benchmark usage.
///
/// Requires a [Runtime](tokio::runtime::Runtime) to be passed in by reference.
/// The client is parameterized with a [FileDB](client::FileDB).
/// It will also use the environment variable `GOERLI_RPC_URL` to connect to a mainnet node.
/// The client will use `http://testing.prater.beacon-api.nimbus.team` as the consensus RPC.
pub fn construct_goerli_client(
rt: &tokio::runtime::Runtime,
) -> eyre::Result<client::Client<ConfigDB>> {
rt.block_on(async {
let benchmark_rpc_url = std::env::var("GOERLI_RPC_URL")?;
let mut client = client::ClientBuilder::new()
.network(networks::Network::GOERLI)
.consensus_rpc("http://testing.prater.beacon-api.nimbus.team")
.execution_rpc(&benchmark_rpc_url)
.load_external_fallback()
.build()?;
client.start().await?;
Ok(client)
})
}
/// Gets the balance of the given address on mainnet.
pub fn get_balance(
rt: &tokio::runtime::Runtime,
client: Arc<Client<ConfigDB>>,
address: &str,
) -> eyre::Result<U256> {
rt.block_on(async {
let block = BlockTag::Latest;
let address = Address::from_str(address)?;
client.get_balance(&address, block).await
})
}
// h/t @ https://github.com/smrpn
// rev: https://github.com/smrpn/casbin-rs/commit/7a0a75d8075440ee65acdac3ee9c0de6fcbd5c48
pub fn await_future<F: std::future::Future<Output = T>, T>(future: F) -> T {
tokio::runtime::Runtime::new().unwrap().block_on(future)
}

81
benches/sync.rs Normal file
View File

@ -0,0 +1,81 @@
use criterion::{criterion_group, criterion_main, Criterion};
use ethers::types::Address;
use helios::types::BlockTag;
mod harness;
criterion_main!(sync);
criterion_group! {
name = sync;
config = Criterion::default().sample_size(10);
targets =
bench_full_sync,
bench_full_sync_with_call,
bench_full_sync_checkpoint_fallback,
bench_full_sync_with_call_checkpoint_fallback,
}
/// Benchmark full client sync.
pub fn bench_full_sync(c: &mut Criterion) {
// Externally, let's fetch the latest checkpoint from our fallback service so as not to benchmark the checkpoint fetch.
let checkpoint = harness::await_future(harness::fetch_mainnet_checkpoint()).unwrap();
let checkpoint = hex::encode(checkpoint);
// On client construction, it will sync to the latest checkpoint using our fetched checkpoint.
c.bench_function("full_sync", |b| {
b.to_async(harness::construct_runtime()).iter(|| async {
let _client = std::sync::Arc::new(
harness::construct_mainnet_client_with_checkpoint(&checkpoint)
.await
.unwrap(),
);
})
});
}
/// Benchmark full client sync.
/// Address: 0x00000000219ab540356cbb839cbe05303d7705fa (beacon chain deposit address)
pub fn bench_full_sync_with_call(c: &mut Criterion) {
// Externally, let's fetch the latest checkpoint from our fallback service so as not to benchmark the checkpoint fetch.
let checkpoint = harness::await_future(harness::fetch_mainnet_checkpoint()).unwrap();
let checkpoint = hex::encode(checkpoint);
// On client construction, it will sync to the latest checkpoint using our fetched checkpoint.
c.bench_function("full_sync_call", |b| {
b.to_async(harness::construct_runtime()).iter(|| async {
let client = std::sync::Arc::new(
harness::construct_mainnet_client_with_checkpoint(&checkpoint)
.await
.unwrap(),
);
let addr = "0x00000000219ab540356cbb839cbe05303d7705fa"
.parse::<Address>()
.unwrap();
let block = BlockTag::Latest;
client.get_balance(&addr, block).await.unwrap()
})
});
}
/// Benchmark full client sync with checkpoint fallback.
pub fn bench_full_sync_checkpoint_fallback(c: &mut Criterion) {
c.bench_function("full_sync_fallback", |b| {
let rt = harness::construct_runtime();
b.iter(|| {
let _client = std::sync::Arc::new(harness::construct_mainnet_client(&rt).unwrap());
})
});
}
/// Benchmark full client sync with a call and checkpoint fallback.
/// Address: 0x00000000219ab540356cbb839cbe05303d7705fa (beacon chain deposit address)
pub fn bench_full_sync_with_call_checkpoint_fallback(c: &mut Criterion) {
c.bench_function("full_sync_call", |b| {
let addr = "0x00000000219ab540356cbb839cbe05303d7705fa";
let rt = harness::construct_runtime();
b.iter(|| {
let client = std::sync::Arc::new(harness::construct_mainnet_client(&rt).unwrap());
harness::get_balance(&rt, client, addr).unwrap();
})
});
}

View File

@ -2,7 +2,7 @@ cargo-features = ["different-binary-name"]
[package]
name = "cli"
version = "0.1.0"
version = "0.3.0"
edition = "2021"
[[bin]]

View File

@ -1,5 +1,4 @@
use std::{
fs,
path::PathBuf,
process::exit,
str::FromStr,
@ -15,16 +14,25 @@ use eyre::Result;
use client::{database::FileDB, Client, ClientBuilder};
use config::{CliConfig, Config};
use futures::executor::block_on;
use log::info;
use log::{error, info};
#[tokio::main]
async fn main() -> Result<()> {
env_logger::Builder::from_env(Env::default().default_filter_or("info")).init();
let config = get_config();
let mut client = ClientBuilder::new().config(config).build()?;
let mut client = match ClientBuilder::new().config(config).build() {
Ok(client) => client,
Err(err) => {
error!("{}", err);
exit(1);
}
};
client.start().await?;
if let Err(err) = client.start().await {
error!("{}", err);
exit(1);
}
register_shutdown_handler(client);
std::future::pending().await
@ -72,6 +80,8 @@ fn get_config() -> Config {
}
#[derive(Parser)]
#[clap(version, about)]
/// Helios is a fast, secure, and portable light client for Ethereum
struct Cli {
#[clap(short, long, default_value = "mainnet")]
network: String,
@ -85,14 +95,20 @@ struct Cli {
consensus_rpc: Option<String>,
#[clap(short, long, env)]
data_dir: Option<String>,
#[clap(short = 'f', long, env)]
fallback: Option<String>,
#[clap(short = 'l', long, env)]
load_external_fallback: bool,
#[clap(short = 's', long, env)]
strict_checkpoint_age: bool,
}
impl Cli {
fn as_cli_config(&self) -> CliConfig {
let checkpoint = match &self.checkpoint {
Some(checkpoint) => Some(hex_str_to_bytes(&checkpoint).expect("invalid checkpoint")),
None => self.get_cached_checkpoint(),
};
let checkpoint = self
.checkpoint
.as_ref()
.map(|c| hex_str_to_bytes(c).expect("invalid checkpoint"));
CliConfig {
checkpoint,
@ -100,21 +116,9 @@ impl Cli {
consensus_rpc: self.consensus_rpc.clone(),
data_dir: self.get_data_dir(),
rpc_port: self.rpc_port,
}
}
fn get_cached_checkpoint(&self) -> Option<Vec<u8>> {
let data_dir = self.get_data_dir();
let checkpoint_file = data_dir.join("checkpoint");
if checkpoint_file.exists() {
let checkpoint_res = fs::read(checkpoint_file);
match checkpoint_res {
Ok(checkpoint) => Some(checkpoint),
Err(_) => None,
}
} else {
None
fallback: self.fallback.clone(),
load_external_fallback: self.load_external_fallback,
strict_checkpoint_age: self.strict_checkpoint_age,
}
}

View File

@ -1,28 +1,29 @@
[package]
name = "client"
version = "0.1.0"
version = "0.3.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
reqwest = { version = "0.11", features = ["json"] }
tokio = { version = "1", features = ["full"] }
eyre = "0.6.8"
serde = { version = "1.0.143", features = ["derive"] }
hex = "0.4.3"
ssz-rs = { git = "https://github.com/ralexstokes/ssz-rs", rev = "cb08f18ca919cc1b685b861d0fa9e2daabe89737" }
blst = "0.3.10"
ssz-rs = { git = "https://github.com/ralexstokes/ssz-rs", rev = "d09f55b4f8554491e3431e01af1c32347a8781cd" }
ethers = "1.0.0"
jsonrpsee = { version = "0.15.1", features = ["full"] }
revm = "2.1.0"
bytes = "1.2.1"
futures = "0.3.23"
toml = "0.5.9"
log = "0.4.17"
openssl = { version = "0.10", features = ["vendored"] }
thiserror = "1.0.37"
common = { path = "../common" }
consensus = { path = "../consensus" }
execution = { path = "../execution" }
config = { path = "../config" }
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
jsonrpsee = { version = "0.15.1", features = ["full"] }
tokio = { version = "1", features = ["full"] }
[target.'cfg(target_arch = "wasm32")'.dependencies]
gloo-timers = "0.2.6"
wasm-bindgen-futures = "0.4.33"
tokio = { version = "1", features = ["sync"] }

View File

@ -1,74 +1,58 @@
use std::path::PathBuf;
use std::sync::Arc;
use config::networks::Network;
use consensus::errors::ConsensusError;
use ethers::prelude::{Address, U256};
use ethers::types::{Transaction, TransactionReceipt, H256};
use ethers::types::{
FeeHistory, Filter, Log, SyncingStatus, Transaction, TransactionReceipt, H256,
};
use eyre::{eyre, Result};
use common::types::BlockTag;
use config::Config;
use consensus::types::Header;
use config::{CheckpointFallback, Config};
use consensus::{types::Header, ConsensusClient};
use execution::types::{CallOpts, ExecutionBlock};
use log::{info, warn};
use tokio::spawn;
use log::{error, info, warn};
use tokio::sync::RwLock;
#[cfg(not(target_arch = "wasm32"))]
use std::path::PathBuf;
#[cfg(not(target_arch = "wasm32"))]
use tokio::spawn;
#[cfg(not(target_arch = "wasm32"))]
use tokio::time::sleep;
use crate::database::{Database, FileDB};
#[cfg(target_arch = "wasm32")]
use gloo_timers::callback::Interval;
#[cfg(target_arch = "wasm32")]
use wasm_bindgen_futures::spawn_local;
use crate::database::Database;
use crate::errors::NodeError;
use crate::node::Node;
#[cfg(not(target_arch = "wasm32"))]
use crate::rpc::Rpc;
pub struct Client<DB: Database> {
node: Arc<RwLock<Node>>,
rpc: Option<Rpc>,
db: Option<DB>,
}
impl Client<FileDB> {
fn new(config: Config) -> Result<Self> {
let config = Arc::new(config);
let node = Node::new(config.clone())?;
let node = Arc::new(RwLock::new(node));
let rpc = if let Some(port) = config.rpc_port {
Some(Rpc::new(node.clone(), port))
} else {
None
};
let data_dir = config.data_dir.clone();
let db = if let Some(dir) = data_dir {
Some(FileDB::new(dir))
} else {
None
};
Ok(Client { node, rpc, db })
}
}
#[derive(Default)]
pub struct ClientBuilder {
network: Option<Network>,
consensus_rpc: Option<String>,
execution_rpc: Option<String>,
checkpoint: Option<Vec<u8>>,
#[cfg(not(target_arch = "wasm32"))]
rpc_port: Option<u16>,
#[cfg(not(target_arch = "wasm32"))]
data_dir: Option<PathBuf>,
config: Option<Config>,
fallback: Option<String>,
load_external_fallback: bool,
strict_checkpoint_age: bool,
}
impl ClientBuilder {
pub fn new() -> Self {
Self {
network: None,
consensus_rpc: None,
execution_rpc: None,
checkpoint: None,
rpc_port: None,
data_dir: None,
config: None,
}
Self::default()
}
pub fn network(mut self, network: Network) -> Self {
@ -87,16 +71,19 @@ impl ClientBuilder {
}
pub fn checkpoint(mut self, checkpoint: &str) -> Self {
let checkpoint = hex::decode(checkpoint).expect("cannot parse checkpoint");
let checkpoint = hex::decode(checkpoint.strip_prefix("0x").unwrap_or(checkpoint))
.expect("cannot parse checkpoint");
self.checkpoint = Some(checkpoint);
self
}
#[cfg(not(target_arch = "wasm32"))]
pub fn rpc_port(mut self, port: u16) -> Self {
self.rpc_port = Some(port);
self
}
#[cfg(not(target_arch = "wasm32"))]
pub fn data_dir(mut self, data_dir: PathBuf) -> Self {
self.data_dir = Some(data_dir);
self
@ -107,7 +94,22 @@ impl ClientBuilder {
self
}
pub fn build(self) -> Result<Client<FileDB>> {
pub fn fallback(mut self, fallback: &str) -> Self {
self.fallback = Some(fallback.to_string());
self
}
pub fn load_external_fallback(mut self) -> Self {
self.load_external_fallback = true;
self
}
pub fn strict_checkpoint_age(mut self) -> Self {
self.strict_checkpoint_age = true;
self
}
pub fn build<DB: Database>(self) -> Result<Client<DB>> {
let base_config = if let Some(network) = self.network {
network.to_base_config()
} else {
@ -135,13 +137,20 @@ impl ClientBuilder {
});
let checkpoint = if let Some(checkpoint) = self.checkpoint {
checkpoint
Some(checkpoint)
} else if let Some(config) = &self.config {
config.checkpoint.clone()
} else {
base_config.checkpoint
None
};
let default_checkpoint = if let Some(config) = &self.config {
config.default_checkpoint.clone()
} else {
base_config.default_checkpoint.clone()
};
#[cfg(not(target_arch = "wasm32"))]
let rpc_port = if self.rpc_port.is_some() {
self.rpc_port
} else if let Some(config) = &self.config {
@ -150,6 +159,7 @@ impl ClientBuilder {
None
};
#[cfg(not(target_arch = "wasm32"))]
let data_dir = if self.data_dir.is_some() {
self.data_dir
} else if let Some(config) = &self.config {
@ -158,31 +168,131 @@ impl ClientBuilder {
None
};
let fallback = if self.fallback.is_some() {
self.fallback
} else if let Some(config) = &self.config {
config.fallback.clone()
} else {
None
};
let load_external_fallback = if let Some(config) = &self.config {
self.load_external_fallback || config.load_external_fallback
} else {
self.load_external_fallback
};
let strict_checkpoint_age = if let Some(config) = &self.config {
self.strict_checkpoint_age || config.strict_checkpoint_age
} else {
self.strict_checkpoint_age
};
let config = Config {
consensus_rpc,
execution_rpc,
checkpoint,
default_checkpoint,
#[cfg(not(target_arch = "wasm32"))]
rpc_port,
#[cfg(target_arch = "wasm32")]
rpc_port: None,
#[cfg(not(target_arch = "wasm32"))]
data_dir,
#[cfg(target_arch = "wasm32")]
data_dir: None,
chain: base_config.chain,
forks: base_config.forks,
max_checkpoint_age: base_config.max_checkpoint_age,
fallback,
load_external_fallback,
strict_checkpoint_age,
};
Client::new(config)
}
}
pub struct Client<DB: Database> {
node: Arc<RwLock<Node>>,
#[cfg(not(target_arch = "wasm32"))]
rpc: Option<Rpc>,
db: DB,
fallback: Option<String>,
load_external_fallback: bool,
}
impl<DB: Database> Client<DB> {
fn new(mut config: Config) -> Result<Self> {
let db = DB::new(&config)?;
if config.checkpoint.is_none() {
let checkpoint = db.load_checkpoint()?;
config.checkpoint = Some(checkpoint);
}
let config = Arc::new(config);
let node = Node::new(config.clone())?;
let node = Arc::new(RwLock::new(node));
#[cfg(not(target_arch = "wasm32"))]
let rpc = config.rpc_port.map(|port| Rpc::new(node.clone(), port));
Ok(Client {
node,
#[cfg(not(target_arch = "wasm32"))]
rpc,
db,
fallback: config.fallback.clone(),
load_external_fallback: config.load_external_fallback,
})
}
pub async fn start(&mut self) -> Result<()> {
#[cfg(not(target_arch = "wasm32"))]
if let Some(rpc) = &mut self.rpc {
rpc.start().await?;
}
let res = self.node.write().await.sync().await;
if let Err(err) = res {
warn!("consensus error: {}", err);
let sync_res = self.node.write().await.sync().await;
if let Err(err) = sync_res {
match err {
NodeError::ConsensusSyncError(err) => match err.downcast_ref() {
Some(ConsensusError::CheckpointTooOld) => {
warn!(
"failed to sync consensus node with checkpoint: 0x{}",
hex::encode(
self.node
.read()
.await
.config
.checkpoint
.clone()
.unwrap_or_default()
),
);
let fallback = self.boot_from_fallback().await;
if fallback.is_err() && self.load_external_fallback {
self.boot_from_external_fallbacks().await?
} else if fallback.is_err() {
error!("Invalid checkpoint. Please update your checkpoint too a more recent block. Alternatively, set an explicit checkpoint fallback service url with the `-f` flag or use the configured external fallback services with `-l` (NOT RECOMMENDED). See https://github.com/a16z/helios#additional-options for more information.");
return Err(err);
}
}
_ => return Err(err),
},
_ => return Err(err.into()),
}
}
self.start_advance_thread();
Ok(())
}
#[cfg(not(target_arch = "wasm32"))]
fn start_advance_thread(&self) {
let node = self.node.clone();
spawn(async move {
loop {
@ -192,10 +302,93 @@ impl<DB: Database> Client<DB> {
}
let next_update = node.read().await.duration_until_next_update();
sleep(next_update).await;
}
});
}
#[cfg(target_arch = "wasm32")]
fn start_advance_thread(&self) {
let node = self.node.clone();
Interval::new(12000, move || {
let node = node.clone();
spawn_local(async move {
let res = node.write().await.advance().await;
if let Err(err) = res {
warn!("consensus error: {}", err);
}
});
})
.forget();
}
async fn boot_from_fallback(&self) -> eyre::Result<()> {
if let Some(fallback) = &self.fallback {
info!(
"attempting to load checkpoint from fallback \"{}\"",
fallback
);
let checkpoint = CheckpointFallback::fetch_checkpoint_from_api(fallback)
.await
.map_err(|_| {
eyre::eyre!("Failed to fetch checkpoint from fallback \"{}\"", fallback)
})?;
info!(
"external fallbacks responded with checkpoint 0x{:?}",
checkpoint
);
// Try to sync again with the new checkpoint by reconstructing the consensus client
// We fail fast here since the node is unrecoverable at this point
let config = self.node.read().await.config.clone();
let consensus =
ConsensusClient::new(&config.consensus_rpc, checkpoint.as_bytes(), config.clone())?;
self.node.write().await.consensus = consensus;
self.node.write().await.sync().await?;
Ok(())
} else {
Err(eyre::eyre!("no explicit fallback specified"))
}
}
async fn boot_from_external_fallbacks(&self) -> eyre::Result<()> {
info!("attempting to fetch checkpoint from external fallbacks...");
// Build the list of external checkpoint fallback services
let list = CheckpointFallback::new()
.build()
.await
.map_err(|_| eyre::eyre!("Failed to construct external checkpoint sync fallbacks"))?;
let checkpoint = if self.node.read().await.config.chain.chain_id == 5 {
list.fetch_latest_checkpoint(&Network::GOERLI)
.await
.map_err(|_| {
eyre::eyre!("Failed to fetch latest goerli checkpoint from external fallbacks")
})?
} else {
list.fetch_latest_checkpoint(&Network::MAINNET)
.await
.map_err(|_| {
eyre::eyre!("Failed to fetch latest mainnet checkpoint from external fallbacks")
})?
};
info!(
"external fallbacks responded with checkpoint {:?}",
checkpoint
);
// Try to sync again with the new checkpoint by reconstructing the consensus client
// We fail fast here since the node is unrecoverable at this point
let config = self.node.read().await.config.clone();
let consensus =
ConsensusClient::new(&config.consensus_rpc, checkpoint.as_bytes(), config.clone())?;
self.node.write().await.consensus = consensus;
self.node.write().await.sync().await?;
Ok(())
}
@ -208,18 +401,28 @@ impl<DB: Database> Client<DB> {
};
info!("saving last checkpoint hash");
let res = self.db.as_ref().map(|db| db.save_checkpoint(checkpoint));
if res.is_some() && res.unwrap().is_err() {
let res = self.db.save_checkpoint(checkpoint);
if res.is_err() {
warn!("checkpoint save failed");
}
}
pub async fn call(&self, opts: &CallOpts, block: BlockTag) -> Result<Vec<u8>> {
self.node.read().await.call(opts, block).await
self.node
.read()
.await
.call(opts, block)
.await
.map_err(|err| err.into())
}
pub async fn estimate_gas(&self, opts: &CallOpts) -> Result<u64> {
self.node.read().await.estimate_gas(opts).await
self.node
.read()
.await
.estimate_gas(opts)
.await
.map_err(|err| err.into())
}
pub async fn get_balance(&self, address: &Address, block: BlockTag) -> Result<U256> {
@ -230,15 +433,38 @@ impl<DB: Database> Client<DB> {
self.node.read().await.get_nonce(address, block).await
}
pub async fn get_block_transaction_count_by_hash(&self, hash: &Vec<u8>) -> Result<u64> {
self.node
.read()
.await
.get_block_transaction_count_by_hash(hash)
}
pub async fn get_block_transaction_count_by_number(&self, block: BlockTag) -> Result<u64> {
self.node
.read()
.await
.get_block_transaction_count_by_number(block)
}
pub async fn get_code(&self, address: &Address, block: BlockTag) -> Result<Vec<u8>> {
self.node.read().await.get_code(address, block).await
}
pub async fn get_storage_at(&self, address: &Address, slot: H256) -> Result<U256> {
self.node.read().await.get_storage_at(address, slot).await
pub async fn get_storage_at(
&self,
address: &Address,
slot: H256,
block: BlockTag,
) -> Result<U256> {
self.node
.read()
.await
.get_storage_at(address, slot, block)
.await
}
pub async fn send_raw_transaction(&self, bytes: &Vec<u8>) -> Result<H256> {
pub async fn send_raw_transaction(&self, bytes: &[u8]) -> Result<H256> {
self.node.read().await.send_raw_transaction(bytes).await
}
@ -261,6 +487,10 @@ impl<DB: Database> Client<DB> {
.await
}
pub async fn get_logs(&self, filter: &Filter) -> Result<Vec<Log>> {
self.node.read().await.get_logs(filter).await
}
pub async fn get_gas_price(&self) -> Result<U256> {
self.node.read().await.get_gas_price()
}
@ -273,6 +503,19 @@ impl<DB: Database> Client<DB> {
self.node.read().await.get_block_number()
}
pub async fn get_fee_history(
&self,
block_count: u64,
last_block: u64,
reward_percentiles: &[f64],
) -> Result<Option<FeeHistory>> {
self.node
.read()
.await
.get_fee_history(block_count, last_block, reward_percentiles)
.await
}
pub async fn get_block_by_number(
&self,
block: BlockTag,
@ -297,11 +540,31 @@ impl<DB: Database> Client<DB> {
.await
}
pub async fn get_transaction_by_block_hash_and_index(
&self,
block_hash: &Vec<u8>,
index: usize,
) -> Result<Option<Transaction>> {
self.node
.read()
.await
.get_transaction_by_block_hash_and_index(block_hash, index)
.await
}
pub async fn chain_id(&self) -> u64 {
self.node.read().await.chain_id()
}
pub async fn syncing(&self) -> Result<SyncingStatus> {
self.node.read().await.syncing()
}
pub async fn get_header(&self) -> Result<Header> {
self.node.read().await.get_header()
}
pub async fn get_coinbase(&self) -> Result<Address> {
self.node.read().await.get_coinbase()
}
}

View File

@ -1,22 +1,40 @@
use std::{fs, io::Write, path::PathBuf};
#[cfg(not(target_arch = "wasm32"))]
use std::{
fs,
io::{Read, Write},
path::PathBuf,
};
use config::Config;
use eyre::Result;
pub trait Database {
fn new(config: &Config) -> Result<Self>
where
Self: Sized;
fn save_checkpoint(&self, checkpoint: Vec<u8>) -> Result<()>;
fn load_checkpoint(&self) -> Result<Vec<u8>>;
}
#[cfg(not(target_arch = "wasm32"))]
pub struct FileDB {
data_dir: PathBuf,
default_checkpoint: Vec<u8>,
}
impl FileDB {
pub fn new(data_dir: PathBuf) -> Self {
FileDB { data_dir }
}
}
#[cfg(not(target_arch = "wasm32"))]
impl Database for FileDB {
fn new(config: &Config) -> Result<Self> {
if let Some(data_dir) = &config.data_dir {
return Ok(FileDB {
data_dir: data_dir.to_path_buf(),
default_checkpoint: config.default_checkpoint.clone(),
});
}
eyre::bail!("data dir not in config")
}
fn save_checkpoint(&self, checkpoint: Vec<u8>) -> Result<()> {
fs::create_dir_all(&self.data_dir)?;
@ -30,4 +48,42 @@ impl Database for FileDB {
Ok(())
}
fn load_checkpoint(&self) -> Result<Vec<u8>> {
let mut buf = Vec::new();
let res = fs::OpenOptions::new()
.read(true)
.open(self.data_dir.join("checkpoint"))
.map(|mut f| f.read_to_end(&mut buf));
if buf.len() == 32 && res.is_ok() {
Ok(buf)
} else {
Ok(self.default_checkpoint.clone())
}
}
}
pub struct ConfigDB {
checkpoint: Vec<u8>,
}
impl Database for ConfigDB {
fn new(config: &Config) -> Result<Self> {
Ok(Self {
checkpoint: config
.checkpoint
.clone()
.unwrap_or(config.default_checkpoint.clone()),
})
}
fn load_checkpoint(&self) -> Result<Vec<u8>> {
Ok(self.checkpoint.clone())
}
fn save_checkpoint(&self, _checkpoint: Vec<u8>) -> Result<()> {
Ok(())
}
}

63
client/src/errors.rs Normal file
View File

@ -0,0 +1,63 @@
use common::errors::BlockNotFoundError;
use execution::errors::EvmError;
use eyre::Report;
use thiserror::Error;
/// Errors that can occur during Node calls
#[derive(Debug, Error)]
pub enum NodeError {
#[error(transparent)]
ExecutionEvmError(#[from] EvmError),
#[error("execution error: {0}")]
ExecutionError(Report),
#[error("out of sync: {0} slots behind")]
OutOfSync(u64),
#[error("consensus payload error: {0}")]
ConsensusPayloadError(Report),
#[error("execution payload error: {0}")]
ExecutionPayloadError(Report),
#[error("consensus client creation error: {0}")]
ConsensusClientCreationError(Report),
#[error("execution client creation error: {0}")]
ExecutionClientCreationError(Report),
#[error("consensus advance error: {0}")]
ConsensusAdvanceError(Report),
#[error("consensus sync error: {0}")]
ConsensusSyncError(Report),
#[error(transparent)]
BlockNotFoundError(#[from] BlockNotFoundError),
}
#[cfg(not(target_arch = "wasm32"))]
impl NodeError {
pub fn to_json_rpsee_error(self) -> jsonrpsee::core::Error {
match self {
NodeError::ExecutionEvmError(evm_err) => match evm_err {
EvmError::Revert(data) => {
let mut msg = "execution reverted".to_string();
if let Some(reason) = data.as_ref().and_then(EvmError::decode_revert_reason) {
msg = format!("{msg}: {reason}")
}
jsonrpsee::core::Error::Call(jsonrpsee::types::error::CallError::Custom(
jsonrpsee::types::error::ErrorObject::owned(
3,
msg,
data.map(|data| format!("0x{}", hex::encode(data))),
),
))
}
_ => jsonrpsee::core::Error::Custom(evm_err.to_string()),
},
_ => jsonrpsee::core::Error::Custom(self.to_string()),
}
}
}

View File

@ -1,9 +1,10 @@
#![feature(map_first_last)]
mod client;
pub use crate::client::*;
pub mod database;
pub mod errors;
#[cfg(not(target_arch = "wasm32"))]
pub mod rpc;
mod node;
pub mod node;

View File

@ -3,12 +3,15 @@ use std::sync::Arc;
use std::time::Duration;
use ethers::prelude::{Address, U256};
use ethers::types::{Transaction, TransactionReceipt, H256};
use ethers::types::{
FeeHistory, Filter, Log, SyncProgress, SyncingStatus, Transaction, TransactionReceipt, H256,
};
use eyre::{eyre, Result};
use common::errors::BlockNotFoundError;
use common::types::BlockTag;
use config::Config;
use consensus::rpc::nimbus_rpc::NimbusRpc;
use consensus::types::{ExecutionPayload, Header};
use consensus::ConsensusClient;
@ -17,23 +20,29 @@ use execution::rpc::http_rpc::HttpRpc;
use execution::types::{CallOpts, ExecutionBlock};
use execution::ExecutionClient;
use crate::errors::NodeError;
pub struct Node {
consensus: ConsensusClient<NimbusRpc>,
execution: Arc<ExecutionClient<HttpRpc>>,
config: Arc<Config>,
pub consensus: ConsensusClient<NimbusRpc>,
pub execution: Arc<ExecutionClient<HttpRpc>>,
pub config: Arc<Config>,
payloads: BTreeMap<u64, ExecutionPayload>,
finalized_payloads: BTreeMap<u64, ExecutionPayload>,
history_size: usize,
current_slot: Option<u64>,
pub history_size: usize,
}
impl Node {
pub fn new(config: Arc<Config>) -> Result<Self> {
pub fn new(config: Arc<Config>) -> Result<Self, NodeError> {
let consensus_rpc = &config.consensus_rpc;
let checkpoint_hash = &config.checkpoint;
let checkpoint_hash = &config.checkpoint.as_ref().unwrap();
let execution_rpc = &config.execution_rpc;
let consensus = ConsensusClient::new(consensus_rpc, checkpoint_hash, config.clone())?;
let execution = Arc::new(ExecutionClient::new(execution_rpc)?);
let consensus = ConsensusClient::new(consensus_rpc, checkpoint_hash, config.clone())
.map_err(NodeError::ConsensusClientCreationError)?;
let execution = Arc::new(
ExecutionClient::new(execution_rpc).map_err(NodeError::ExecutionClientCreationError)?,
);
let payloads = BTreeMap::new();
let finalized_payloads = BTreeMap::new();
@ -44,17 +53,36 @@ impl Node {
config,
payloads,
finalized_payloads,
current_slot: None,
history_size: 64,
})
}
pub async fn sync(&mut self) -> Result<()> {
self.consensus.sync().await?;
pub async fn sync(&mut self) -> Result<(), NodeError> {
let chain_id = self.config.chain.chain_id;
self.execution
.check_rpc(chain_id)
.await
.map_err(NodeError::ExecutionError)?;
self.consensus
.check_rpc()
.await
.map_err(NodeError::ConsensusSyncError)?;
self.consensus
.sync()
.await
.map_err(NodeError::ConsensusSyncError)?;
self.update_payloads().await
}
pub async fn advance(&mut self) -> Result<()> {
self.consensus.advance().await?;
pub async fn advance(&mut self) -> Result<(), NodeError> {
self.consensus
.advance()
.await
.map_err(NodeError::ConsensusAdvanceError)?;
self.update_payloads().await
}
@ -65,32 +93,48 @@ impl Node {
.unwrap()
}
async fn update_payloads(&mut self) -> Result<()> {
async fn update_payloads(&mut self) -> Result<(), NodeError> {
let latest_header = self.consensus.get_header();
let latest_payload = self
.consensus
.get_execution_payload(&Some(latest_header.slot))
.await?;
.await
.map_err(NodeError::ConsensusPayloadError)?;
let finalized_header = self.consensus.get_finalized_header();
let finalized_payload = self
.consensus
.get_execution_payload(&Some(finalized_header.slot))
.await?;
.await
.map_err(NodeError::ConsensusPayloadError)?;
self.payloads
.insert(latest_payload.block_number, latest_payload);
.insert(*latest_payload.block_number(), latest_payload);
self.payloads
.insert(finalized_payload.block_number, finalized_payload.clone());
.insert(*finalized_payload.block_number(), finalized_payload.clone());
self.finalized_payloads
.insert(finalized_payload.block_number, finalized_payload);
.insert(*finalized_payload.block_number(), finalized_payload);
let start_slot = self
.current_slot
.unwrap_or(latest_header.slot - self.history_size as u64);
let backfill_payloads = self
.consensus
.get_payloads(start_slot, latest_header.slot)
.await
.map_err(NodeError::ConsensusPayloadError)?;
for payload in backfill_payloads {
self.payloads.insert(*payload.block_number(), payload);
}
self.current_slot = Some(latest_header.slot);
while self.payloads.len() > self.history_size {
self.payloads.pop_first();
}
// only save one finalized block per epoch
// finality updates only occur on epoch boundries
// finality updates only occur on epoch boundaries
while self.finalized_payloads.len() > usize::max(self.history_size / 32, 1) {
self.finalized_payloads.pop_first();
}
@ -98,37 +142,39 @@ impl Node {
Ok(())
}
pub async fn call(&self, opts: &CallOpts, block: BlockTag) -> Result<Vec<u8>> {
pub async fn call(&self, opts: &CallOpts, block: BlockTag) -> Result<Vec<u8>, NodeError> {
self.check_blocktag_age(&block)?;
let payload = self.get_payload(block)?;
let mut evm = Evm::new(
self.execution.clone(),
&payload,
payload,
&self.payloads,
self.chain_id(),
);
evm.call(opts).await
evm.call(opts).await.map_err(NodeError::ExecutionEvmError)
}
pub async fn estimate_gas(&self, opts: &CallOpts) -> Result<u64> {
pub async fn estimate_gas(&self, opts: &CallOpts) -> Result<u64, NodeError> {
self.check_head_age()?;
let payload = self.get_payload(BlockTag::Latest)?;
let mut evm = Evm::new(
self.execution.clone(),
&payload,
payload,
&self.payloads,
self.chain_id(),
);
evm.estimate_gas(opts).await
evm.estimate_gas(opts)
.await
.map_err(NodeError::ExecutionEvmError)
}
pub async fn get_balance(&self, address: &Address, block: BlockTag) -> Result<U256> {
self.check_blocktag_age(&block)?;
let payload = self.get_payload(block)?;
let account = self.execution.get_account(&address, None, payload).await?;
let account = self.execution.get_account(address, None, payload).await?;
Ok(account.balance)
}
@ -136,22 +182,41 @@ impl Node {
self.check_blocktag_age(&block)?;
let payload = self.get_payload(block)?;
let account = self.execution.get_account(&address, None, payload).await?;
let account = self.execution.get_account(address, None, payload).await?;
Ok(account.nonce)
}
pub fn get_block_transaction_count_by_hash(&self, hash: &Vec<u8>) -> Result<u64> {
let payload = self.get_payload_by_hash(hash)?;
let transaction_count = payload.1.transactions().len();
Ok(transaction_count as u64)
}
pub fn get_block_transaction_count_by_number(&self, block: BlockTag) -> Result<u64> {
let payload = self.get_payload(block)?;
let transaction_count = payload.transactions().len();
Ok(transaction_count as u64)
}
pub async fn get_code(&self, address: &Address, block: BlockTag) -> Result<Vec<u8>> {
self.check_blocktag_age(&block)?;
let payload = self.get_payload(block)?;
let account = self.execution.get_account(&address, None, payload).await?;
let account = self.execution.get_account(address, None, payload).await?;
Ok(account.code)
}
pub async fn get_storage_at(&self, address: &Address, slot: H256) -> Result<U256> {
pub async fn get_storage_at(
&self,
address: &Address,
slot: H256,
block: BlockTag,
) -> Result<U256> {
self.check_head_age()?;
let payload = self.get_payload(BlockTag::Latest)?;
let payload = self.get_payload(block)?;
let account = self
.execution
.get_account(address, Some(&[slot]), payload)
@ -164,7 +229,7 @@ impl Node {
}
}
pub async fn send_raw_transaction(&self, bytes: &Vec<u8>) -> Result<H256> {
pub async fn send_raw_transaction(&self, bytes: &[u8]) -> Result<H256> {
self.execution.send_raw_transaction(bytes).await
}
@ -183,12 +248,28 @@ impl Node {
.await
}
pub async fn get_transaction_by_block_hash_and_index(
&self,
hash: &Vec<u8>,
index: usize,
) -> Result<Option<Transaction>> {
let payload = self.get_payload_by_hash(hash)?;
self.execution
.get_transaction_by_block_hash_and_index(payload.1, index)
.await
}
pub async fn get_logs(&self, filter: &Filter) -> Result<Vec<Log>> {
self.execution.get_logs(filter, &self.payloads).await
}
// assumes tip of 1 gwei to prevent having to prove out every tx in the block
pub fn get_gas_price(&self) -> Result<U256> {
self.check_head_age()?;
let payload = self.get_payload(BlockTag::Latest)?;
let base_fee = U256::from_little_endian(&payload.base_fee_per_gas.to_bytes_le());
let base_fee = U256::from_little_endian(&payload.base_fee_per_gas().to_bytes_le());
let tip = U256::from(10_u64.pow(9));
Ok(base_fee + tip)
}
@ -203,7 +284,7 @@ impl Node {
self.check_head_age()?;
let payload = self.get_payload(BlockTag::Latest)?;
Ok(payload.block_number)
Ok(*payload.block_number())
}
pub async fn get_block_by_number(
@ -214,33 +295,32 @@ impl Node {
self.check_blocktag_age(&block)?;
match self.get_payload(block) {
Ok(payload) => self
.execution
.get_block(payload, full_tx)
.await
.map(|b| Some(b)),
Ok(payload) => self.execution.get_block(payload, full_tx).await.map(Some),
Err(_) => Ok(None),
}
}
pub async fn get_fee_history(
&self,
block_count: u64,
last_block: u64,
reward_percentiles: &[f64],
) -> Result<Option<FeeHistory>> {
self.execution
.get_fee_history(block_count, last_block, reward_percentiles, &self.payloads)
.await
}
pub async fn get_block_by_hash(
&self,
hash: &Vec<u8>,
full_tx: bool,
) -> Result<Option<ExecutionBlock>> {
let payloads = self
.payloads
.iter()
.filter(|entry| &entry.1.block_hash.to_vec() == hash)
.collect::<Vec<(&u64, &ExecutionPayload)>>();
let payload = self.get_payload_by_hash(hash);
if let Some(payload_entry) = payloads.get(0) {
self.execution
.get_block(payload_entry.1, full_tx)
.await
.map(|b| Some(b))
} else {
Ok(None)
match payload {
Ok(payload) => self.execution.get_block(payload.1, full_tx).await.map(Some),
Err(_) => Ok(None),
}
}
@ -248,16 +328,54 @@ impl Node {
self.config.chain.chain_id
}
pub fn syncing(&self) -> Result<SyncingStatus> {
if self.check_head_age().is_ok() {
Ok(SyncingStatus::IsFalse)
} else {
let latest_synced_block = self.get_block_number()?;
let oldest_payload = self.payloads.first_key_value();
let oldest_synced_block =
oldest_payload.map_or(latest_synced_block, |(key, _value)| *key);
let highest_block = self.consensus.expected_current_slot();
Ok(SyncingStatus::IsSyncing(Box::new(SyncProgress {
current_block: latest_synced_block.into(),
highest_block: highest_block.into(),
starting_block: oldest_synced_block.into(),
pulled_states: None,
known_states: None,
healed_bytecode_bytes: None,
healed_bytecodes: None,
healed_trienode_bytes: None,
healed_trienodes: None,
healing_bytecode: None,
healing_trienodes: None,
synced_account_bytes: None,
synced_accounts: None,
synced_bytecode_bytes: None,
synced_bytecodes: None,
synced_storage: None,
synced_storage_bytes: None,
})))
}
}
pub fn get_header(&self) -> Result<Header> {
self.check_head_age()?;
Ok(self.consensus.get_header().clone())
}
pub fn get_coinbase(&self) -> Result<Address> {
self.check_head_age()?;
let payload = self.get_payload(BlockTag::Latest)?;
let coinbase_address = Address::from_slice(payload.fee_recipient());
Ok(coinbase_address)
}
pub fn get_last_checkpoint(&self) -> Option<Vec<u8>> {
self.consensus.last_checkpoint.clone()
}
fn get_payload(&self, block: BlockTag) -> Result<&ExecutionPayload> {
fn get_payload(&self, block: BlockTag) -> Result<&ExecutionPayload, BlockNotFoundError> {
match block {
BlockTag::Latest => {
let payload = self.payloads.last_key_value();
@ -271,24 +389,37 @@ impl Node {
}
BlockTag::Number(num) => {
let payload = self.payloads.get(&num);
payload.ok_or(BlockNotFoundError::new(BlockTag::Number(num)).into())
payload.ok_or(BlockNotFoundError::new(BlockTag::Number(num)))
}
}
}
fn check_head_age(&self) -> Result<()> {
fn get_payload_by_hash(&self, hash: &Vec<u8>) -> Result<(&u64, &ExecutionPayload)> {
let payloads = self
.payloads
.iter()
.filter(|entry| &entry.1.block_hash().to_vec() == hash)
.collect::<Vec<(&u64, &ExecutionPayload)>>();
payloads
.get(0)
.cloned()
.ok_or(eyre!("Block not found by hash"))
}
fn check_head_age(&self) -> Result<(), NodeError> {
let synced_slot = self.consensus.get_header().slot;
let expected_slot = self.consensus.expected_current_slot();
let slot_delay = expected_slot - synced_slot;
if slot_delay > 10 {
return Err(eyre!("out of sync"));
return Err(NodeError::OutOfSync(slot_delay));
}
Ok(())
}
fn check_blocktag_age(&self, block: &BlockTag) -> Result<()> {
fn check_blocktag_age(&self, block: &BlockTag) -> Result<(), NodeError> {
match block {
BlockTag::Latest => self.check_head_age(),
BlockTag::Finalized => Ok(()),

View File

@ -1,6 +1,6 @@
use ethers::{
abi::AbiEncode,
types::{Address, Transaction, TransactionReceipt, H256},
types::{Address, Filter, Log, SyncingStatus, Transaction, TransactionReceipt, H256, U256},
};
use eyre::Result;
use log::info;
@ -13,7 +13,7 @@ use jsonrpsee::{
proc_macros::rpc,
};
use crate::node::Node;
use crate::{errors::NodeError, node::Node};
use common::{
types::BlockTag,
@ -57,6 +57,11 @@ trait EthRpc {
async fn get_balance(&self, address: &str, block: BlockTag) -> Result<String, Error>;
#[method(name = "getTransactionCount")]
async fn get_transaction_count(&self, address: &str, block: BlockTag) -> Result<String, Error>;
#[method(name = "getBlockTransactionCountByHash")]
async fn get_block_transaction_count_by_hash(&self, hash: &str) -> Result<String, Error>;
#[method(name = "getBlockTransactionCountByNumber")]
async fn get_block_transaction_count_by_number(&self, block: BlockTag)
-> Result<String, Error>;
#[method(name = "getCode")]
async fn get_code(&self, address: &str, block: BlockTag) -> Result<String, Error>;
#[method(name = "call")]
@ -92,6 +97,25 @@ trait EthRpc {
) -> Result<Option<TransactionReceipt>, Error>;
#[method(name = "getTransactionByHash")]
async fn get_transaction_by_hash(&self, hash: &str) -> Result<Option<Transaction>, Error>;
#[method(name = "getTransactionByBlockHashAndIndex")]
async fn get_transaction_by_block_hash_and_index(
&self,
hash: &str,
index: usize,
) -> Result<Option<Transaction>, Error>;
#[method(name = "getLogs")]
async fn get_logs(&self, filter: Filter) -> Result<Vec<Log>, Error>;
#[method(name = "getStorageAt")]
async fn get_storage_at(
&self,
address: &str,
slot: H256,
block: BlockTag,
) -> Result<String, Error>;
#[method(name = "getCoinbase")]
async fn get_coinbase(&self) -> Result<Address, Error>;
#[method(name = "syncing")]
async fn syncing(&self) -> Result<SyncingStatus, Error>;
}
#[rpc(client, server, namespace = "net")]
@ -113,7 +137,7 @@ impl EthRpcServer for RpcInner {
let node = self.node.read().await;
let balance = convert_err(node.get_balance(&address, block).await)?;
Ok(balance.encode_hex())
Ok(format_hex(&balance))
}
async fn get_transaction_count(&self, address: &str, block: BlockTag) -> Result<String, Error> {
@ -121,7 +145,24 @@ impl EthRpcServer for RpcInner {
let node = self.node.read().await;
let nonce = convert_err(node.get_nonce(&address, block).await)?;
Ok(nonce.encode_hex())
Ok(format!("0x{nonce:x}"))
}
async fn get_block_transaction_count_by_hash(&self, hash: &str) -> Result<String, Error> {
let hash = convert_err(hex_str_to_bytes(hash))?;
let node = self.node.read().await;
let transaction_count = convert_err(node.get_block_transaction_count_by_hash(&hash))?;
Ok(u64_to_hex_string(transaction_count))
}
async fn get_block_transaction_count_by_number(
&self,
block: BlockTag,
) -> Result<String, Error> {
let node = self.node.read().await;
let transaction_count = convert_err(node.get_block_transaction_count_by_number(block))?;
Ok(u64_to_hex_string(transaction_count))
}
async fn get_code(&self, address: &str, block: BlockTag) -> Result<String, Error> {
@ -129,19 +170,26 @@ impl EthRpcServer for RpcInner {
let node = self.node.read().await;
let code = convert_err(node.get_code(&address, block).await)?;
Ok(hex::encode(code))
Ok(format!("0x{:}", hex::encode(code)))
}
async fn call(&self, opts: CallOpts, block: BlockTag) -> Result<String, Error> {
let node = self.node.read().await;
let res = convert_err(node.call(&opts, block).await)?;
let res = node
.call(&opts, block)
.await
.map_err(NodeError::to_json_rpsee_error)?;
Ok(format!("0x{}", hex::encode(res)))
}
async fn estimate_gas(&self, opts: CallOpts) -> Result<String, Error> {
let node = self.node.read().await;
let gas = convert_err(node.estimate_gas(&opts).await)?;
let gas = node
.estimate_gas(&opts)
.await
.map_err(NodeError::to_json_rpsee_error)?;
Ok(u64_to_hex_string(gas))
}
@ -155,13 +203,13 @@ impl EthRpcServer for RpcInner {
async fn gas_price(&self) -> Result<String, Error> {
let node = self.node.read().await;
let gas_price = convert_err(node.get_gas_price())?;
Ok(gas_price.encode_hex())
Ok(format_hex(&gas_price))
}
async fn max_priority_fee_per_gas(&self) -> Result<String, Error> {
let node = self.node.read().await;
let tip = convert_err(node.get_priority_fee())?;
Ok(tip.encode_hex())
Ok(format_hex(&tip))
}
async fn block_number(&self) -> Result<String, Error> {
@ -213,6 +261,47 @@ impl EthRpcServer for RpcInner {
let hash = H256::from_slice(&convert_err(hex_str_to_bytes(hash))?);
convert_err(node.get_transaction_by_hash(&hash).await)
}
async fn get_transaction_by_block_hash_and_index(
&self,
hash: &str,
index: usize,
) -> Result<Option<Transaction>, Error> {
let hash = convert_err(hex_str_to_bytes(hash))?;
let node = self.node.read().await;
convert_err(
node.get_transaction_by_block_hash_and_index(&hash, index)
.await,
)
}
async fn get_coinbase(&self) -> Result<Address, Error> {
let node = self.node.read().await;
Ok(node.get_coinbase().unwrap())
}
async fn syncing(&self) -> Result<SyncingStatus, Error> {
let node = self.node.read().await;
convert_err(node.syncing())
}
async fn get_logs(&self, filter: Filter) -> Result<Vec<Log>, Error> {
let node = self.node.read().await;
convert_err(node.get_logs(&filter).await)
}
async fn get_storage_at(
&self,
address: &str,
slot: H256,
block: BlockTag,
) -> Result<String, Error> {
let address = convert_err(Address::from_str(address))?;
let node = self.node.read().await;
let storage = convert_err(node.get_storage_at(&address, slot, block).await)?;
Ok(format_hex(&storage))
}
}
#[async_trait]
@ -244,3 +333,20 @@ async fn start(rpc: RpcInner) -> Result<(HttpServerHandle, SocketAddr)> {
fn convert_err<T, E: Display>(res: Result<T, E>) -> Result<T, Error> {
res.map_err(|err| Error::Custom(err.to_string()))
}
fn format_hex(num: &U256) -> String {
let stripped = num
.encode_hex()
.strip_prefix("0x")
.unwrap()
.trim_start_matches('0')
.to_string();
let stripped = if stripped.is_empty() {
"0".to_string()
} else {
stripped
};
format!("0x{stripped}")
}

View File

@ -1,14 +1,12 @@
[package]
name = "common"
version = "0.1.0"
version = "0.3.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
eyre = "0.6.8"
serde = { version = "1.0.143", features = ["derive"] }
hex = "0.4.3"
ssz-rs = { git = "https://github.com/ralexstokes/ssz-rs", rev = "cb08f18ca919cc1b685b861d0fa9e2daabe89737" }
ssz-rs = { git = "https://github.com/ralexstokes/ssz-rs", rev = "d09f55b4f8554491e3431e01af1c32347a8781cd" }
ethers = "1.0.0"
thiserror = "1.0.37"

View File

@ -1,3 +1,4 @@
use ethers::types::H256;
use thiserror::Error;
use crate::types::BlockTag;
@ -14,6 +15,18 @@ impl BlockNotFoundError {
}
}
#[derive(Debug, Error)]
#[error("slot not found: {slot:?}")]
pub struct SlotNotFoundError {
slot: H256,
}
impl SlotNotFoundError {
pub fn new(slot: H256) -> Self {
Self { slot }
}
}
#[derive(Debug, Error)]
#[error("rpc error on method: {method}, message: {error}")]
pub struct RpcError<E: ToString> {

View File

@ -20,7 +20,7 @@ impl Display for BlockTag {
Self::Number(num) => num.to_string(),
};
write!(f, "{}", formatted)
write!(f, "{formatted}")
}
}

View File

@ -22,5 +22,5 @@ pub fn address_to_hex_string(address: &Address) -> String {
}
pub fn u64_to_hex_string(val: u64) -> String {
format!("0x{:x}", val)
format!("0x{val:x}")
}

74
config.md Normal file
View File

@ -0,0 +1,74 @@
## Helios Configuration
All configuration options can be set on a per-network level in `~/.helios/helios.toml`.
#### Comprehensive Example
```toml
[mainnet]
# The consensus rpc to use. This should be a trusted rpc endpoint. Defaults to "https://www.lightclientdata.org".
consensus_rpc = "https://www.lightclientdata.org"
# [REQUIRED] The execution rpc to use. This should be a trusted rpc endpoint.
execution_rpc = "https://eth-mainnet.g.alchemy.com/v2/XXXXX"
# The port to run the JSON-RPC server on. By default, Helios will use port 8545.
rpc_port = 8545
# 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.
checkpoint = "0x85e6151a246e8fdba36db27a0c7678a575346272fe978c9281e13a8b26cdfa68"
# The directory to store the checkpoint database in. If not provided, Helios will use "~/.helios/data/mainnet", where `mainnet` is the network.
# It is recommended to set this directory to a persistent location mapped to a fast storage device.
data_dir = "/home/user/.helios/mainnet"
# The maximum age of a checkpoint in seconds. If the checkpoint is older than this, Helios will attempt to fetch a new checkpoint.
max_checkpoint_age = 86400
# 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 (like provided in https://github.com/ethpandaops/checkpoint-sync-health-checks/blob/master/_data/endpoints.yaml).
fallback = "https://sync-mainnet.beaconcha.in"
# 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.
load_external_fallback = true
[goerli]
# The consensus rpc to use. This should be a trusted rpc endpoint. Defaults to Nimbus testnet.
consensus_rpc = "http://testing.prater.beacon-api.nimbus.team"
# [REQUIRED] The execution rpc to use. This should be a trusted rpc endpoint.
execution_rpc = "https://eth-goerli.g.alchemy.com/v2/XXXXX"
# The port to run the JSON-RPC server on. By default, Helios will use port 8545.
rpc_port = 8545
# 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.
checkpoint = "0xb5c375696913865d7c0e166d87bc7c772b6210dc9edf149f4c7ddc6da0dd4495"
# The directory to store the checkpoint database in. If not provided, Helios will use "~/.helios/data/goerli", where `goerli` is the network.
# It is recommended to set this directory to a persistent location mapped to a fast storage device.
data_dir = "/home/user/.helios/goerli"
# The maximum age of a checkpoint in seconds. If the checkpoint is older than this, Helios will attempt to fetch a new checkpoint.
max_checkpoint_age = 86400
# 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 (like provided in https://github.com/ethpandaops/checkpoint-sync-health-checks/blob/master/_data/endpoints.yaml).
fallback = "https://sync-goerli.beaconcha.in"
# 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.
load_external_fallback = true
```
#### Options
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.
- `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.
- `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.
- `rpc_port` - The port to run the JSON-RPC server on. By default, Helios will use port 8545.
- `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.
- `data_dir` - The directory to store the checkpoint database in. If not provided, Helios will use "~/.helios/data/<NETWORK>", where `<NETWORK>` is the network. It is recommended to set this directory to a persistent location mapped to a fast storage device.
- `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.
- `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.
- `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.

View File

@ -1,18 +1,24 @@
[package]
name = "config"
version = "0.1.0"
version = "0.3.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
eyre = "0.6.8"
serde = { version = "1.0.143", features = ["derive"] }
hex = "0.4.3"
ssz-rs = { git = "https://github.com/ralexstokes/ssz-rs", rev = "cb08f18ca919cc1b685b861d0fa9e2daabe89737" }
ssz-rs = { git = "https://github.com/ralexstokes/ssz-rs", rev = "d09f55b4f8554491e3431e01af1c32347a8781cd" }
ethers = "1.0.0"
figment = { version = "0.10.7", features = ["toml", "env"] }
thiserror = "1.0.37"
log = "0.4.17"
reqwest = "0.11.13"
serde_yaml = "0.9.14"
strum = "0.24.1"
futures = "0.3.25"
common = { path = "../common" }
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
tokio = { version = "1", features = ["full"] }

19
config/src/base.rs Normal file
View File

@ -0,0 +1,19 @@
use serde::Serialize;
use crate::types::{ChainConfig, Forks};
use crate::utils::bytes_serialize;
/// The base configuration for a network.
#[derive(Serialize, Default)]
pub struct BaseConfig {
pub rpc_port: u16,
pub consensus_rpc: Option<String>,
#[serde(
deserialize_with = "bytes_deserialize",
serialize_with = "bytes_serialize"
)]
pub default_checkpoint: Vec<u8>,
pub chain: ChainConfig,
pub forks: Forks,
pub max_checkpoint_age: u64,
}

273
config/src/checkpoints.rs Normal file
View File

@ -0,0 +1,273 @@
use std::collections::HashMap;
use ethers::types::H256;
use serde::{Deserialize, Serialize};
use crate::networks;
/// The location where the list of checkpoint services are stored.
pub const CHECKPOINT_SYNC_SERVICES_LIST: &str = "https://raw.githubusercontent.com/ethpandaops/checkpoint-sync-health-checks/master/_data/endpoints.yaml";
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RawSlotResponse {
pub data: RawSlotResponseData,
}
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RawSlotResponseData {
pub slots: Vec<Slot>,
}
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Slot {
pub slot: u64,
pub block_root: Option<H256>,
pub state_root: Option<H256>,
pub epoch: u64,
pub time: StartEndTime,
}
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct StartEndTime {
/// An ISO 8601 formatted UTC timestamp.
pub start_time: String,
/// An ISO 8601 formatted UTC timestamp.
pub end_time: String,
}
/// A health check for the checkpoint sync service.
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Health {
/// If the node is healthy.
pub result: bool,
/// An [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) UTC timestamp.
pub date: String,
}
/// A checkpoint fallback service.
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CheckpointFallbackService {
/// The endpoint for the checkpoint sync service.
pub endpoint: String,
/// The checkpoint sync service name.
pub name: String,
/// The service state.
pub state: bool,
/// If the service is verified.
pub verification: bool,
/// Contact information for the service maintainers.
pub contacts: Option<serde_yaml::Value>,
/// Service Notes
pub notes: Option<serde_yaml::Value>,
/// The service health check.
pub health: Vec<Health>,
}
/// The CheckpointFallback manages checkpoint fallback services.
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CheckpointFallback {
/// Services Map
pub services: HashMap<networks::Network, Vec<CheckpointFallbackService>>,
/// A list of supported networks to build.
/// Default: [mainnet, goerli]
pub networks: Vec<networks::Network>,
}
impl CheckpointFallback {
/// Constructs a new checkpoint fallback service.
pub fn new() -> Self {
Self {
services: Default::default(),
networks: [networks::Network::MAINNET, networks::Network::GOERLI].to_vec(),
}
}
/// Build the checkpoint fallback service from the community-maintained list by [ethPandaOps](https://github.com/ethpandaops).
///
/// The list is defined in [ethPandaOps/checkpoint-fallback-service](https://github.com/ethpandaops/checkpoint-sync-health-checks/blob/master/_data/endpoints.yaml).
pub async fn build(mut self) -> eyre::Result<Self> {
// Fetch the services
let client = reqwest::Client::new();
let res = client.get(CHECKPOINT_SYNC_SERVICES_LIST).send().await?;
let yaml = res.text().await?;
// Parse the yaml content results.
let list: serde_yaml::Value = serde_yaml::from_str(&yaml)?;
// Construct the services mapping from network <> list of services
let mut services = HashMap::new();
for network in &self.networks {
// Try to parse list of checkpoint fallback services
let service_list = list
.get(network.to_string().to_lowercase())
.ok_or_else(|| {
eyre::eyre!(format!("missing {network} fallback checkpoint services"))
})?;
let parsed: Vec<CheckpointFallbackService> =
serde_yaml::from_value(service_list.clone())?;
services.insert(*network, parsed);
}
self.services = services;
Ok(self)
}
/// Fetch the latest checkpoint from the checkpoint fallback service.
pub async fn fetch_latest_checkpoint(
&self,
network: &crate::networks::Network,
) -> eyre::Result<H256> {
let services = &self.get_healthy_fallback_services(network);
Self::fetch_latest_checkpoint_from_services(&services[..]).await
}
async fn query_service(endpoint: &str) -> Option<RawSlotResponse> {
let client = reqwest::Client::new();
let constructed_url = Self::construct_url(endpoint);
let res = client.get(&constructed_url).send().await.ok()?;
let raw: RawSlotResponse = res.json().await.ok()?;
Some(raw)
}
/// Fetch the latest checkpoint from a list of checkpoint fallback services.
pub async fn fetch_latest_checkpoint_from_services(
services: &[CheckpointFallbackService],
) -> eyre::Result<H256> {
// Iterate over all mainnet checkpoint sync services and get the latest checkpoint slot for each.
let tasks: Vec<_> = services
.iter()
.map(|service| async move {
let service = service.clone();
match Self::query_service(&service.endpoint).await {
Some(raw) => {
if raw.data.slots.is_empty() {
return Err(eyre::eyre!("no slots"));
}
let slot = raw
.data
.slots
.iter()
.find(|s| s.block_root.is_some())
.ok_or(eyre::eyre!("no valid slots"))?;
Ok(slot.clone())
}
None => Err(eyre::eyre!("failed to query service")),
}
})
.collect();
let slots = futures::future::join_all(tasks)
.await
.iter()
.filter_map(|slot| match &slot {
Ok(s) => Some(s.clone()),
_ => None,
})
.filter(|s| s.block_root.is_some())
.collect::<Vec<_>>();
// Get the max epoch
let max_epoch_slot = slots.iter().max_by_key(|x| x.epoch).ok_or(eyre::eyre!(
"Failed to find max epoch from checkpoint slots"
))?;
let max_epoch = max_epoch_slot.epoch;
// Filter out all the slots that are not the max epoch.
let slots = slots
.into_iter()
.filter(|x| x.epoch == max_epoch)
.collect::<Vec<_>>();
// Return the most commonly verified checkpoint.
let checkpoints = slots
.iter()
.filter_map(|x| x.block_root)
.collect::<Vec<_>>();
let mut m: HashMap<H256, usize> = HashMap::new();
for c in checkpoints {
*m.entry(c).or_default() += 1;
}
let most_common = m.into_iter().max_by_key(|(_, v)| *v).map(|(k, _)| k);
// Return the most commonly verified checkpoint for the latest epoch.
most_common.ok_or_else(|| eyre::eyre!("No checkpoint found"))
}
/// Associated function to fetch the latest checkpoint from a specific checkpoint sync fallback
/// service api url.
pub async fn fetch_checkpoint_from_api(url: &str) -> eyre::Result<H256> {
// Fetch the url
let client = reqwest::Client::new();
let constructed_url = Self::construct_url(url);
let res = client.get(constructed_url).send().await?;
let raw: RawSlotResponse = res.json().await?;
let slot = raw.data.slots[0].clone();
slot.block_root
.ok_or_else(|| eyre::eyre!("Checkpoint not in returned slot"))
}
/// Constructs the checkpoint fallback service url for fetching a slot.
///
/// This is an associated function and can be used like so:
///
/// ```rust
/// use config::CheckpointFallback;
///
/// let url = CheckpointFallback::construct_url("https://sync-mainnet.beaconcha.in");
/// assert_eq!("https://sync-mainnet.beaconcha.in/checkpointz/v1/beacon/slots", url);
/// ```
pub fn construct_url(endpoint: &str) -> String {
format!("{endpoint}/checkpointz/v1/beacon/slots")
}
/// Returns a list of all checkpoint fallback endpoints.
///
/// ### Warning
///
/// These services are not healthchecked **nor** trustworthy and may act with malice by returning invalid checkpoints.
pub fn get_all_fallback_endpoints(&self, network: &networks::Network) -> Vec<String> {
self.services[network]
.iter()
.map(|service| service.endpoint.clone())
.collect()
}
/// Returns a list of healthchecked checkpoint fallback endpoints.
///
/// ### Warning
///
/// These services are not trustworthy and may act with malice by returning invalid checkpoints.
pub fn get_healthy_fallback_endpoints(&self, network: &networks::Network) -> Vec<String> {
self.services[network]
.iter()
.filter(|service| service.state)
.map(|service| service.endpoint.clone())
.collect()
}
/// Returns a list of healthchecked checkpoint fallback services.
///
/// ### Warning
///
/// These services are not trustworthy and may act with malice by returning invalid checkpoints.
pub fn get_healthy_fallback_services(
&self,
network: &networks::Network,
) -> Vec<CheckpointFallbackService> {
self.services[network]
.iter()
.filter(|service| service.state)
.cloned()
.collect::<Vec<CheckpointFallbackService>>()
}
/// Returns the raw checkpoint fallback service objects for a given network.
pub fn get_fallback_services(
&self,
network: &networks::Network,
) -> &Vec<CheckpointFallbackService> {
self.services[network].as_ref()
}
}

57
config/src/cli.rs Normal file
View File

@ -0,0 +1,57 @@
use std::{collections::HashMap, path::PathBuf};
use figment::{providers::Serialized, value::Value};
use serde::{Deserialize, Serialize};
/// Cli Config
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
pub struct CliConfig {
pub execution_rpc: Option<String>,
pub consensus_rpc: Option<String>,
pub checkpoint: Option<Vec<u8>>,
pub rpc_port: Option<u16>,
pub data_dir: PathBuf,
pub fallback: Option<String>,
pub load_external_fallback: bool,
pub strict_checkpoint_age: bool,
}
impl CliConfig {
pub fn as_provider(&self, network: &str) -> Serialized<HashMap<&str, Value>> {
let mut user_dict = HashMap::new();
if let Some(rpc) = &self.execution_rpc {
user_dict.insert("execution_rpc", Value::from(rpc.clone()));
}
if let Some(rpc) = &self.consensus_rpc {
user_dict.insert("consensus_rpc", Value::from(rpc.clone()));
}
if let Some(checkpoint) = &self.checkpoint {
user_dict.insert("checkpoint", Value::from(hex::encode(checkpoint)));
}
if let Some(port) = self.rpc_port {
user_dict.insert("rpc_port", Value::from(port));
}
user_dict.insert("data_dir", Value::from(self.data_dir.to_str().unwrap()));
if let Some(fallback) = &self.fallback {
user_dict.insert("fallback", Value::from(fallback.clone()));
}
user_dict.insert(
"load_external_fallback",
Value::from(self.load_external_fallback),
);
user_dict.insert(
"strict_checkpoint_age",
Value::from(self.strict_checkpoint_age),
);
Serialized::from(user_dict, network)
}
}

97
config/src/config.rs Normal file
View File

@ -0,0 +1,97 @@
use figment::{
providers::{Format, Serialized, Toml},
Figment,
};
use serde::Deserialize;
use std::{path::PathBuf, process::exit};
use crate::base::BaseConfig;
use crate::cli::CliConfig;
use crate::networks;
use crate::types::{ChainConfig, Forks};
use crate::utils::{bytes_deserialize, bytes_opt_deserialize};
#[derive(Deserialize, Debug, Default)]
pub struct Config {
pub consensus_rpc: String,
pub execution_rpc: String,
pub rpc_port: Option<u16>,
#[serde(deserialize_with = "bytes_deserialize")]
pub default_checkpoint: Vec<u8>,
#[serde(default)]
#[serde(deserialize_with = "bytes_opt_deserialize")]
pub checkpoint: Option<Vec<u8>>,
pub data_dir: Option<PathBuf>,
pub chain: ChainConfig,
pub forks: Forks,
pub max_checkpoint_age: u64,
pub fallback: Option<String>,
pub load_external_fallback: bool,
pub strict_checkpoint_age: bool,
}
impl Config {
pub fn from_file(config_path: &PathBuf, network: &str, cli_config: &CliConfig) -> Self {
let base_config = match network {
"mainnet" => networks::mainnet(),
"goerli" => networks::goerli(),
_ => BaseConfig::default(),
};
let base_provider = Serialized::from(base_config, network);
let toml_provider = Toml::file(config_path).nested();
let cli_provider = cli_config.as_provider(network);
let config_res = Figment::new()
.merge(base_provider)
.merge(toml_provider)
.merge(cli_provider)
.select(network)
.extract();
match config_res {
Ok(config) => config,
Err(err) => {
match err.kind {
figment::error::Kind::MissingField(field) => {
let field = field.replace('_', "-");
println!("\x1b[91merror\x1b[0m: missing configuration field: {field}");
println!("\n\ttry supplying the propoper command line argument: --{field}");
println!("\talternatively, you can add the field to your helios.toml file or as an environment variable");
println!("\nfor more information, check the github README");
}
_ => println!("cannot parse configuration: {err}"),
}
exit(1);
}
}
}
pub fn fork_version(&self, slot: u64) -> Vec<u8> {
let epoch = slot / 32;
if epoch >= self.forks.capella.epoch {
self.forks.capella.fork_version.clone()
} else if epoch >= self.forks.bellatrix.epoch {
self.forks.bellatrix.fork_version.clone()
} else if epoch >= self.forks.altair.epoch {
self.forks.altair.fork_version.clone()
} else {
self.forks.genesis.fork_version.clone()
}
}
pub fn to_base_config(&self) -> BaseConfig {
BaseConfig {
rpc_port: self.rpc_port.unwrap_or(8545),
consensus_rpc: Some(self.consensus_rpc.clone()),
default_checkpoint: self.default_checkpoint.clone(),
chain: self.chain.clone(),
forks: self.forks.clone(),
max_checkpoint_age: self.max_checkpoint_age,
}
}
}

View File

@ -1,177 +1,26 @@
/// Base Config
pub mod base;
pub use base::*;
/// Core Config
pub mod config;
pub use crate::config::*;
/// Checkpoint Config
pub mod checkpoints;
pub use checkpoints::*;
/// Cli Config
pub mod cli;
pub use cli::*;
/// Network Configuration
pub mod networks;
pub use networks::*;
use std::{collections::HashMap, path::PathBuf, process::exit};
/// Generic Config Types
pub mod types;
pub use types::*;
use eyre::Result;
use figment::{
providers::{Format, Serialized, Toml},
value::Value,
Figment,
};
use networks::BaseConfig;
use serde::{Deserialize, Serialize};
use common::utils::hex_str_to_bytes;
#[derive(Serialize, Deserialize, Debug, Default)]
pub struct Config {
pub consensus_rpc: String,
pub execution_rpc: String,
pub rpc_port: Option<u16>,
#[serde(
deserialize_with = "bytes_deserialize",
serialize_with = "bytes_serialize"
)]
pub checkpoint: Vec<u8>,
pub data_dir: Option<PathBuf>,
pub chain: ChainConfig,
pub forks: Forks,
}
impl Config {
pub fn from_file(config_path: &PathBuf, network: &str, cli_config: &CliConfig) -> Self {
let base_config = match network {
"mainnet" => networks::mainnet(),
"goerli" => networks::goerli(),
_ => BaseConfig::default(),
};
let base_provider = Serialized::from(base_config, network);
let toml_provider = Toml::file(config_path).nested();
let cli_provider = cli_config.as_provider(network);
let config_res = Figment::new()
.merge(base_provider)
.merge(toml_provider)
.merge(cli_provider)
.select(network)
.extract();
match config_res {
Ok(config) => config,
Err(err) => {
match err.kind {
figment::error::Kind::MissingField(field) => {
let field = field.replace("_", "-");
println!(
"\x1b[91merror\x1b[0m: missing configuration field: {}",
field
);
println!(
"\n\ttry supplying the propoper command line argument: --{}",
field
);
println!("\talternatively, you can add the field to your helios.toml file or as an environment variable");
println!("\nfor more information, check the github README");
}
_ => println!("cannot parse configuration: {}", err),
}
exit(1);
}
}
}
pub fn fork_version(&self, slot: u64) -> Vec<u8> {
let epoch = slot / 32;
if epoch >= self.forks.bellatrix.epoch {
self.forks.bellatrix.fork_version.clone()
} else if epoch >= self.forks.altair.epoch {
self.forks.altair.fork_version.clone()
} else {
self.forks.genesis.fork_version.clone()
}
}
pub fn to_base_config(&self) -> BaseConfig {
BaseConfig {
rpc_port: self.rpc_port.unwrap_or(8545),
consensus_rpc: Some(self.consensus_rpc.clone()),
checkpoint: self.checkpoint.clone(),
chain: self.chain.clone(),
forks: self.forks.clone(),
}
}
}
#[derive(Serialize)]
pub struct CliConfig {
pub execution_rpc: Option<String>,
pub consensus_rpc: Option<String>,
pub checkpoint: Option<Vec<u8>>,
pub rpc_port: Option<u16>,
pub data_dir: PathBuf,
}
impl CliConfig {
fn as_provider(&self, network: &str) -> Serialized<HashMap<&str, Value>> {
let mut user_dict = HashMap::new();
if let Some(rpc) = &self.execution_rpc {
user_dict.insert("execution_rpc", Value::from(rpc.clone()));
}
if let Some(rpc) = &self.consensus_rpc {
user_dict.insert("consensus_rpc", Value::from(rpc.clone()));
}
if let Some(checkpoint) = &self.checkpoint {
user_dict.insert("checkpoint", Value::from(hex::encode(checkpoint)));
}
if let Some(port) = self.rpc_port {
user_dict.insert("rpc_port", Value::from(port));
}
user_dict.insert("data_dir", Value::from(self.data_dir.to_str().unwrap()));
Serialized::from(user_dict, network)
}
}
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct ChainConfig {
pub chain_id: u64,
pub genesis_time: u64,
#[serde(
deserialize_with = "bytes_deserialize",
serialize_with = "bytes_serialize"
)]
pub genesis_root: Vec<u8>,
}
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct Forks {
pub genesis: Fork,
pub altair: Fork,
pub bellatrix: Fork,
}
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct Fork {
pub epoch: u64,
#[serde(
deserialize_with = "bytes_deserialize",
serialize_with = "bytes_serialize"
)]
pub fork_version: Vec<u8>,
}
fn bytes_deserialize<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
where
D: serde::Deserializer<'de>,
{
let bytes: String = serde::Deserialize::deserialize(deserializer)?;
Ok(hex_str_to_bytes(&bytes).unwrap())
}
fn bytes_serialize<S>(bytes: &Vec<u8>, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let bytes_string = hex::encode(bytes);
serializer.serialize_str(&bytes_string)
}
/// Generic Utilities
pub mod utils;

View File

@ -1,8 +1,24 @@
use serde::Serialize;
use crate::{bytes_serialize, ChainConfig, Fork, Forks};
use common::utils::hex_str_to_bytes;
use serde::{Deserialize, Serialize};
use strum::{Display, EnumIter};
use crate::base::BaseConfig;
use crate::types::{ChainConfig, Fork, Forks};
#[derive(
Debug,
Clone,
Copy,
Serialize,
Deserialize,
EnumIter,
Display,
Hash,
Eq,
PartialEq,
PartialOrd,
Ord,
)]
pub enum Network {
MAINNET,
GOERLI,
@ -17,23 +33,10 @@ impl Network {
}
}
#[derive(Serialize, Default)]
pub struct BaseConfig {
pub rpc_port: u16,
pub consensus_rpc: Option<String>,
#[serde(
deserialize_with = "bytes_deserialize",
serialize_with = "bytes_serialize"
)]
pub checkpoint: Vec<u8>,
pub chain: ChainConfig,
pub forks: Forks,
}
pub fn mainnet() -> BaseConfig {
BaseConfig {
checkpoint: hex_str_to_bytes(
"0x428ce0b5f5bbed1fc2b3feb5d4152ae0fe98a80b1bfa8de36681868e81e9222a",
default_checkpoint: hex_str_to_bytes(
"0x766647f3c4e1fc91c0db9a9374032ae038778411fbff222974e11f2e3ce7dadf",
)
.unwrap(),
rpc_port: 8545,
@ -59,13 +62,18 @@ pub fn mainnet() -> BaseConfig {
epoch: 144896,
fork_version: hex_str_to_bytes("0x02000000").unwrap(),
},
capella: Fork {
epoch: u64::MAX, // TODO: set epoch when known
fork_version: hex_str_to_bytes("0x03000000").unwrap(),
},
},
max_checkpoint_age: 1_209_600, // 14 days
}
}
pub fn goerli() -> BaseConfig {
BaseConfig {
checkpoint: hex_str_to_bytes(
default_checkpoint: hex_str_to_bytes(
"0xd4344682866dbede543395ecf5adf9443a27f423a4b00f270458e7932686ced1",
)
.unwrap(),
@ -92,6 +100,11 @@ pub fn goerli() -> BaseConfig {
epoch: 112260,
fork_version: hex_str_to_bytes("0x02001020").unwrap(),
},
capella: Fork {
epoch: 162304,
fork_version: hex_str_to_bytes("0x03001020").unwrap(),
},
},
max_checkpoint_age: 1_209_600, // 14 days
}
}

32
config/src/types.rs Normal file
View File

@ -0,0 +1,32 @@
use serde::{Deserialize, Serialize};
use crate::utils::{bytes_deserialize, bytes_serialize};
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct ChainConfig {
pub chain_id: u64,
pub genesis_time: u64,
#[serde(
deserialize_with = "bytes_deserialize",
serialize_with = "bytes_serialize"
)]
pub genesis_root: Vec<u8>,
}
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct Forks {
pub genesis: Fork,
pub altair: Fork,
pub bellatrix: Fork,
pub capella: Fork,
}
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct Fork {
pub epoch: u64,
#[serde(
deserialize_with = "bytes_deserialize",
serialize_with = "bytes_serialize"
)]
pub fork_version: Vec<u8>,
}

29
config/src/utils.rs Normal file
View File

@ -0,0 +1,29 @@
use common::utils::hex_str_to_bytes;
pub fn bytes_deserialize<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
where
D: serde::Deserializer<'de>,
{
let bytes: String = serde::Deserialize::deserialize(deserializer)?;
Ok(hex_str_to_bytes(&bytes).unwrap())
}
pub fn bytes_serialize<S>(bytes: &Vec<u8>, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let bytes_string = hex::encode(bytes);
serializer.serialize_str(&bytes_string)
}
pub fn bytes_opt_deserialize<'de, D>(deserializer: D) -> Result<Option<Vec<u8>>, D::Error>
where
D: serde::Deserializer<'de>,
{
let bytes_opt: Option<String> = serde::Deserialize::deserialize(deserializer)?;
if let Some(bytes) = bytes_opt {
Ok(Some(hex_str_to_bytes(&bytes).unwrap()))
} else {
Ok(None)
}
}

View File

@ -0,0 +1,68 @@
use config::networks;
use ethers::types::H256;
#[tokio::test]
async fn test_checkpoint_fallback() {
let cf = config::checkpoints::CheckpointFallback::new();
assert_eq!(cf.services.get(&networks::Network::MAINNET), None);
assert_eq!(cf.services.get(&networks::Network::GOERLI), None);
assert_eq!(
cf.networks,
[networks::Network::MAINNET, networks::Network::GOERLI].to_vec()
);
}
#[tokio::test]
async fn test_construct_checkpoints() {
let cf = config::checkpoints::CheckpointFallback::new()
.build()
.await
.unwrap();
assert!(cf.services[&networks::Network::MAINNET].len() > 1);
assert!(cf.services[&networks::Network::GOERLI].len() > 1);
}
#[tokio::test]
async fn test_fetch_latest_checkpoints() {
let cf = config::checkpoints::CheckpointFallback::new()
.build()
.await
.unwrap();
let checkpoint = cf
.fetch_latest_checkpoint(&networks::Network::GOERLI)
.await
.unwrap();
assert!(checkpoint != H256::zero());
let checkpoint = cf
.fetch_latest_checkpoint(&networks::Network::MAINNET)
.await
.unwrap();
assert!(checkpoint != H256::zero());
}
#[tokio::test]
async fn test_get_all_fallback_endpoints() {
let cf = config::checkpoints::CheckpointFallback::new()
.build()
.await
.unwrap();
let urls = cf.get_all_fallback_endpoints(&networks::Network::MAINNET);
assert!(!urls.is_empty());
let urls = cf.get_all_fallback_endpoints(&networks::Network::GOERLI);
assert!(!urls.is_empty());
}
#[tokio::test]
async fn test_get_healthy_fallback_endpoints() {
let cf = config::checkpoints::CheckpointFallback::new()
.build()
.await
.unwrap();
let urls = cf.get_healthy_fallback_endpoints(&networks::Network::MAINNET);
assert!(!urls.is_empty());
let urls = cf.get_healthy_fallback_endpoints(&networks::Network::GOERLI);
assert!(!urls.is_empty());
}

View File

@ -1,29 +1,33 @@
[package]
name = "consensus"
version = "0.1.0"
version = "0.3.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
tokio = { version = "1", features = ["full"] }
eyre = "0.6.8"
futures = "0.3.23"
serde = { version = "1.0.143", features = ["derive"] }
serde_json = "1.0.85"
hex = "0.4.3"
ssz-rs = { git = "https://github.com/ralexstokes/ssz-rs", rev = "cb08f18ca919cc1b685b861d0fa9e2daabe89737" }
blst = "0.3.10"
ssz-rs = { git = "https://github.com/ralexstokes/ssz-rs", rev = "d09f55b4f8554491e3431e01af1c32347a8781cd" }
milagro_bls = { git = "https://github.com/Snowfork/milagro_bls" }
ethers = "1.0.0"
bytes = "1.2.1"
toml = "0.5.9"
async-trait = "0.1.57"
log = "0.4.17"
chrono = "0.4.22"
chrono = "0.4.23"
thiserror = "1.0.37"
openssl = { version = "0.10", features = ["vendored"] }
reqwest = { version = "0.11.12", features = ["json"] }
reqwest-middleware = "0.1.6"
reqwest-retry = "0.1.5"
reqwest = { version = "0.11.13", features = ["json"] }
superstruct = "0.7.0"
common = { path = "../common" }
config = { path = "../config" }
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
openssl = { version = "0.10", features = ["vendored"] }
tokio = { version = "1", features = ["full"] }
[target.'cfg(target_arch = "wasm32")'.dependencies]
wasm-timer = "0.2.5"

View File

@ -1,27 +1,40 @@
use std::cmp;
use std::sync::Arc;
use std::time::UNIX_EPOCH;
use blst::min_pk::PublicKey;
use chrono::Duration;
use eyre::eyre;
use eyre::Result;
use futures::future::join_all;
use log::warn;
use log::{debug, info};
use milagro_bls::PublicKey;
use ssz_rs::prelude::*;
use common::types::*;
use common::utils::*;
use config::Config;
use crate::constants::MAX_REQUEST_LIGHT_CLIENT_UPDATES;
use crate::errors::ConsensusError;
use super::rpc::ConsensusRpc;
use super::types::*;
use super::utils::*;
#[cfg(not(target_arch = "wasm32"))]
use std::time::SystemTime;
#[cfg(not(target_arch = "wasm32"))]
use std::time::UNIX_EPOCH;
#[cfg(target_arch = "wasm32")]
use wasm_timer::SystemTime;
#[cfg(target_arch = "wasm32")]
use wasm_timer::UNIX_EPOCH;
// https://github.com/ethereum/consensus-specs/blob/dev/specs/altair/light-client/sync-protocol.md
// does not implement force updates
#[derive(Debug)]
pub struct ConsensusClient<R: ConsensusRpc> {
rpc: R,
store: LightClientStore,
@ -43,7 +56,7 @@ struct LightClientStore {
impl<R: ConsensusRpc> ConsensusClient<R> {
pub fn new(
rpc: &str,
checkpoint_block_root: &Vec<u8>,
checkpoint_block_root: &[u8],
config: Arc<Config>,
) -> Result<ConsensusClient<R>> {
let rpc = R::new(rpc);
@ -53,13 +66,23 @@ impl<R: ConsensusRpc> ConsensusClient<R> {
store: LightClientStore::default(),
last_checkpoint: None,
config,
initial_checkpoint: checkpoint_block_root.clone(),
initial_checkpoint: checkpoint_block_root.to_vec(),
})
}
pub async fn check_rpc(&self) -> Result<()> {
let chain_id = self.rpc.chain_id().await?;
if chain_id != self.config.chain.chain_id {
Err(ConsensusError::IncorrectRpcNetwork.into())
} else {
Ok(())
}
}
pub async fn get_execution_payload(&self, slot: &Option<u64>) -> Result<ExecutionPayload> {
let slot = slot.unwrap_or(self.store.optimistic_header.slot);
let mut block = self.rpc.get_block(slot).await?.clone();
let mut block = self.rpc.get_block(slot).await?;
let block_hash = block.hash_tree_root()?;
let latest_slot = self.store.optimistic_header.slot;
@ -80,10 +103,50 @@ impl<R: ConsensusRpc> ConsensusClient<R> {
)
.into())
} else {
Ok(block.body.execution_payload)
Ok(block.body.execution_payload().clone())
}
}
pub async fn get_payloads(
&self,
start_slot: u64,
end_slot: u64,
) -> Result<Vec<ExecutionPayload>> {
let payloads_fut = (start_slot..end_slot)
.rev()
.map(|slot| self.rpc.get_block(slot));
let mut prev_parent_hash: Bytes32 = self
.rpc
.get_block(end_slot)
.await?
.body
.execution_payload()
.parent_hash()
.clone();
let mut payloads: Vec<ExecutionPayload> = Vec::new();
for result in join_all(payloads_fut).await {
if result.is_err() {
continue;
}
let payload = result.unwrap().body.execution_payload().clone();
if payload.block_hash() != &prev_parent_hash {
warn!(
"error while backfilling blocks: {}",
ConsensusError::InvalidHeaderHash(
format!("{prev_parent_hash:02X?}"),
format!("{:02X?}", payload.parent_hash()),
)
);
break;
}
prev_parent_hash = payload.parent_hash().clone();
payloads.push(payload);
}
Ok(payloads)
}
pub fn get_header(&self) -> &Header {
&self.store.optimistic_header
}
@ -96,10 +159,13 @@ impl<R: ConsensusRpc> ConsensusClient<R> {
self.bootstrap().await?;
let current_period = calc_sync_period(self.store.finalized_header.slot);
let updates = self.rpc.get_updates(current_period).await?;
let updates = self
.rpc
.get_updates(current_period, MAX_REQUEST_LIGHT_CLIENT_UPDATES)
.await?;
for mut update in updates {
self.verify_update(&mut update)?;
for update in updates {
self.verify_update(&update)?;
self.apply_update(&update);
}
@ -111,6 +177,11 @@ impl<R: ConsensusRpc> ConsensusClient<R> {
self.verify_optimistic_update(&optimistic_update)?;
self.apply_optimistic_update(&optimistic_update);
info!(
"consensus client in sync with checkpoint: 0x{}",
hex::encode(&self.initial_checkpoint)
);
Ok(())
}
@ -126,15 +197,15 @@ impl<R: ConsensusRpc> ConsensusClient<R> {
if self.store.next_sync_committee.is_none() {
debug!("checking for sync committee update");
let current_period = calc_sync_period(self.store.finalized_header.slot);
let mut updates = self.rpc.get_updates(current_period).await?;
let mut updates = self.rpc.get_updates(current_period, 1).await?;
if updates.len() == 1 {
let mut update = updates.get_mut(0).unwrap();
let res = self.verify_update(&mut update);
let update = updates.get_mut(0).unwrap();
let res = self.verify_update(update);
if res.is_ok() {
info!("updating sync committee");
self.apply_update(&update);
self.apply_update(update);
}
}
}
@ -149,6 +220,16 @@ impl<R: ConsensusRpc> ConsensusClient<R> {
.await
.map_err(|_| eyre!("could not fetch bootstrap"))?;
let is_valid = self.is_valid_checkpoint(bootstrap.header.slot);
if !is_valid {
if self.config.strict_checkpoint_age {
return Err(ConsensusError::CheckpointTooOld.into());
} else {
warn!("checkpoint too old, consider using a more recent block");
}
}
let committee_valid = is_current_committee_proof_valid(
&bootstrap.header,
&mut bootstrap.current_sync_committee,
@ -418,13 +499,13 @@ impl<R: ConsensusRpc> ConsensusClient<R> {
fn verify_sync_committee_signture(
&self,
pks: &Vec<PublicKey>,
pks: &[PublicKey],
attested_header: &Header,
signature: &SignatureBytes,
signature_slot: u64,
) -> bool {
let res: Result<bool> = (move || {
let pks: Vec<&PublicKey> = pks.iter().map(|pk| pk).collect();
let pks: Vec<&PublicKey> = pks.iter().collect();
let header_root =
bytes_to_bytes32(attested_header.clone().hash_tree_root()?.as_bytes());
let signing_root = self.compute_committee_sign_root(header_root, signature_slot)?;
@ -450,18 +531,13 @@ impl<R: ConsensusRpc> ConsensusClient<R> {
fn age(&self, slot: u64) -> Duration {
let expected_time = self.slot_timestamp(slot);
let now = std::time::SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap();
let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap();
let delay = now - std::time::Duration::from_secs(expected_time);
chrono::Duration::from_std(delay).unwrap()
}
pub fn expected_current_slot(&self) -> u64 {
let now = std::time::SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap();
let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap();
let genesis_time = self.config.chain.genesis_time;
let since_genesis = now - std::time::Duration::from_secs(genesis_time);
@ -479,7 +555,7 @@ impl<R: ConsensusRpc> ConsensusClient<R> {
let next_slot = current_slot + 1;
let next_slot_timestamp = self.slot_timestamp(next_slot);
let now = std::time::SystemTime::now()
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
@ -489,6 +565,17 @@ impl<R: ConsensusRpc> ConsensusClient<R> {
Duration::seconds(next_update as i64)
}
// Determines blockhash_slot age and returns true if it is less than 14 days old
fn is_valid_checkpoint(&self, blockhash_slot: u64) -> bool {
let current_slot = self.expected_current_slot();
let current_slot_timestamp = self.slot_timestamp(current_slot);
let blockhash_slot_timestamp = self.slot_timestamp(blockhash_slot);
let slot_age = current_slot_timestamp - blockhash_slot_timestamp;
slot_age < self.config.max_checkpoint_age
}
}
fn get_participating_keys(
@ -499,7 +586,7 @@ fn get_participating_keys(
bitfield.iter().enumerate().for_each(|(i, bit)| {
if bit == true {
let pk = &committee.pubkeys[i];
let pk = PublicKey::from_bytes(&pk).unwrap();
let pk = PublicKey::from_bytes_unchecked(pk).unwrap();
pks.push(pk);
}
});
@ -521,7 +608,7 @@ fn get_bits(bitfield: &Bitvector<512>) -> u64 {
fn is_finality_proof_valid(
attested_header: &Header,
finality_header: &mut Header,
finality_branch: &Vec<Bytes32>,
finality_branch: &[Bytes32],
) -> bool {
is_proof_valid(attested_header, finality_header, finality_branch, 6, 41)
}
@ -529,7 +616,7 @@ fn is_finality_proof_valid(
fn is_next_committee_proof_valid(
attested_header: &Header,
next_committee: &mut SyncCommittee,
next_committee_branch: &Vec<Bytes32>,
next_committee_branch: &[Bytes32],
) -> bool {
is_proof_valid(
attested_header,
@ -543,7 +630,7 @@ fn is_next_committee_proof_valid(
fn is_current_committee_proof_valid(
attested_header: &Header,
current_committee: &mut SyncCommittee,
current_committee_branch: &Vec<Bytes32>,
current_committee_branch: &[Bytes32],
) -> bool {
is_proof_valid(
attested_header,
@ -558,6 +645,7 @@ fn is_current_committee_proof_valid(
mod tests {
use std::sync::Arc;
use crate::constants::MAX_REQUEST_LIGHT_CLIENT_UPDATES;
use ssz_rs::Vector;
use crate::{
@ -569,13 +657,14 @@ mod tests {
};
use config::{networks, Config};
async fn get_client() -> ConsensusClient<MockRpc> {
async fn get_client(strict_checkpoint_age: bool) -> ConsensusClient<MockRpc> {
let base_config = networks::goerli();
let config = Config {
consensus_rpc: String::new(),
execution_rpc: String::new(),
chain: base_config.chain,
forks: base_config.forks,
strict_checkpoint_age,
..Default::default()
};
@ -585,30 +674,37 @@ mod tests {
let mut client = ConsensusClient::new("testdata/", &checkpoint, Arc::new(config)).unwrap();
client.bootstrap().await.unwrap();
client
}
#[tokio::test]
async fn test_verify_update() {
let client = get_client().await;
let client = get_client(false).await;
let period = calc_sync_period(client.store.finalized_header.slot);
let updates = client.rpc.get_updates(period).await.unwrap();
let updates = client
.rpc
.get_updates(period, MAX_REQUEST_LIGHT_CLIENT_UPDATES)
.await
.unwrap();
let mut update = updates[0].clone();
client.verify_update(&mut update).unwrap();
let update = updates[0].clone();
client.verify_update(&update).unwrap();
}
#[tokio::test]
async fn test_verify_update_invalid_committee() {
let client = get_client().await;
let client = get_client(false).await;
let period = calc_sync_period(client.store.finalized_header.slot);
let updates = client.rpc.get_updates(period).await.unwrap();
let updates = client
.rpc
.get_updates(period, MAX_REQUEST_LIGHT_CLIENT_UPDATES)
.await
.unwrap();
let mut update = updates[0].clone();
update.next_sync_committee.pubkeys[0] = Vector::default();
let err = client.verify_update(&mut update).err().unwrap();
let err = client.verify_update(&update).err().unwrap();
assert_eq!(
err.to_string(),
ConsensusError::InvalidNextSyncCommitteeProof.to_string()
@ -617,14 +713,18 @@ mod tests {
#[tokio::test]
async fn test_verify_update_invalid_finality() {
let client = get_client().await;
let client = get_client(false).await;
let period = calc_sync_period(client.store.finalized_header.slot);
let updates = client.rpc.get_updates(period).await.unwrap();
let updates = client
.rpc
.get_updates(period, MAX_REQUEST_LIGHT_CLIENT_UPDATES)
.await
.unwrap();
let mut update = updates[0].clone();
update.finalized_header = Header::default();
let err = client.verify_update(&mut update).err().unwrap();
let err = client.verify_update(&update).err().unwrap();
assert_eq!(
err.to_string(),
ConsensusError::InvalidFinalityProof.to_string()
@ -633,14 +733,18 @@ mod tests {
#[tokio::test]
async fn test_verify_update_invalid_sig() {
let client = get_client().await;
let client = get_client(false).await;
let period = calc_sync_period(client.store.finalized_header.slot);
let updates = client.rpc.get_updates(period).await.unwrap();
let updates = client
.rpc
.get_updates(period, MAX_REQUEST_LIGHT_CLIENT_UPDATES)
.await
.unwrap();
let mut update = updates[0].clone();
update.sync_aggregate.sync_committee_signature = Vector::default();
let err = client.verify_update(&mut update).err().unwrap();
let err = client.verify_update(&update).err().unwrap();
assert_eq!(
err.to_string(),
ConsensusError::InvalidSignature.to_string()
@ -649,7 +753,7 @@ mod tests {
#[tokio::test]
async fn test_verify_finality() {
let mut client = get_client().await;
let mut client = get_client(false).await;
client.sync().await.unwrap();
let update = client.rpc.get_finality_update().await.unwrap();
@ -659,7 +763,7 @@ mod tests {
#[tokio::test]
async fn test_verify_finality_invalid_finality() {
let mut client = get_client().await;
let mut client = get_client(false).await;
client.sync().await.unwrap();
let mut update = client.rpc.get_finality_update().await.unwrap();
@ -674,7 +778,7 @@ mod tests {
#[tokio::test]
async fn test_verify_finality_invalid_sig() {
let mut client = get_client().await;
let mut client = get_client(false).await;
client.sync().await.unwrap();
let mut update = client.rpc.get_finality_update().await.unwrap();
@ -689,7 +793,7 @@ mod tests {
#[tokio::test]
async fn test_verify_optimistic() {
let mut client = get_client().await;
let mut client = get_client(false).await;
client.sync().await.unwrap();
let update = client.rpc.get_optimistic_update().await.unwrap();
@ -698,7 +802,7 @@ mod tests {
#[tokio::test]
async fn test_verify_optimistic_invalid_sig() {
let mut client = get_client().await;
let mut client = get_client(false).await;
client.sync().await.unwrap();
let mut update = client.rpc.get_optimistic_update().await.unwrap();
@ -710,4 +814,10 @@ mod tests {
ConsensusError::InvalidSignature.to_string()
);
}
#[tokio::test]
#[should_panic]
async fn test_verify_checkpoint_age_invalid() {
get_client(true).await;
}
}

View File

@ -0,0 +1,4 @@
// Consensus constants
// https://github.com/ethereum/consensus-specs/blob/dev/specs/altair/light-client/p2p-interface.md#configuration
pub const MAX_REQUEST_LIGHT_CLIENT_UPDATES: u8 = 128;

View File

@ -22,4 +22,8 @@ pub enum ConsensusError {
InvalidHeaderHash(String, String),
#[error("payload not found for slot: {0}")]
PayloadNotFound(u64),
#[error("checkpoint is too old")]
CheckpointTooOld,
#[error("consensus rpc is for the incorrect network")]
IncorrectRpcNetwork,
}

View File

@ -5,4 +5,5 @@ pub mod types;
mod consensus;
pub use crate::consensus::*;
mod constants;
mod utils;

View File

@ -1,16 +1,15 @@
use std::{fs::read_to_string, path::PathBuf};
use async_trait::async_trait;
use eyre::Result;
use super::ConsensusRpc;
use crate::types::{BeaconBlock, Bootstrap, FinalityUpdate, OptimisticUpdate, Update};
use async_trait::async_trait;
use eyre::Result;
pub struct MockRpc {
testdata: PathBuf,
}
#[async_trait]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
impl ConsensusRpc for MockRpc {
fn new(path: &str) -> Self {
MockRpc {
@ -18,12 +17,12 @@ impl ConsensusRpc for MockRpc {
}
}
async fn get_bootstrap(&self, _block_root: &Vec<u8>) -> Result<Bootstrap> {
async fn get_bootstrap(&self, _block_root: &'_ [u8]) -> Result<Bootstrap> {
let bootstrap = read_to_string(self.testdata.join("bootstrap.json"))?;
Ok(serde_json::from_str(&bootstrap)?)
}
async fn get_updates(&self, _period: u64) -> Result<Vec<Update>> {
async fn get_updates(&self, _period: u64, _count: u8) -> Result<Vec<Update>> {
let updates = read_to_string(self.testdata.join("updates.json"))?;
Ok(serde_json::from_str(&updates)?)
}
@ -42,4 +41,8 @@ impl ConsensusRpc for MockRpc {
let block = read_to_string(self.testdata.join("blocks.json"))?;
Ok(serde_json::from_str(&block)?)
}
async fn chain_id(&self) -> Result<u64> {
eyre::bail!("not implemented")
}
}

View File

@ -7,12 +7,14 @@ use eyre::Result;
use crate::types::{BeaconBlock, Bootstrap, FinalityUpdate, OptimisticUpdate, Update};
// implements https://github.com/ethereum/beacon-APIs/tree/master/apis/beacon/light_client
#[async_trait]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
pub trait ConsensusRpc {
fn new(path: &str) -> Self;
async fn get_bootstrap(&self, block_root: &Vec<u8>) -> Result<Bootstrap>;
async fn get_updates(&self, period: u64) -> Result<Vec<Update>>;
async fn get_bootstrap(&self, block_root: &'_ [u8]) -> Result<Bootstrap>;
async fn get_updates(&self, period: u64, count: u8) -> Result<Vec<Update>>;
async fn get_finality_update(&self) -> Result<FinalityUpdate>;
async fn get_optimistic_update(&self) -> Result<OptimisticUpdate>;
async fn get_block(&self, slot: u64) -> Result<BeaconBlock>;
async fn chain_id(&self) -> Result<u64>;
}

View File

@ -1,43 +1,35 @@
use async_trait::async_trait;
use common::errors::RpcError;
use eyre::Result;
use reqwest_middleware::{ClientBuilder, ClientWithMiddleware};
use reqwest_retry::{policies::ExponentialBackoff, RetryTransientMiddleware};
use std::cmp;
use super::ConsensusRpc;
use crate::constants::MAX_REQUEST_LIGHT_CLIENT_UPDATES;
use crate::types::*;
use common::errors::RpcError;
#[derive(Debug)]
pub struct NimbusRpc {
rpc: String,
client: ClientWithMiddleware,
}
#[async_trait]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
impl ConsensusRpc for NimbusRpc {
fn new(rpc: &str) -> Self {
let retry_policy = ExponentialBackoff::builder()
.backoff_exponent(1)
.build_with_max_retries(3);
let client = ClientBuilder::new(reqwest::Client::new())
.with(RetryTransientMiddleware::new_with_policy(retry_policy))
.build();
NimbusRpc {
rpc: rpc.to_string(),
client,
}
}
async fn get_bootstrap(&self, block_root: &Vec<u8>) -> Result<Bootstrap> {
async fn get_bootstrap(&self, block_root: &'_ [u8]) -> Result<Bootstrap> {
let root_hex = hex::encode(block_root);
let req = format!(
"{}/eth/v1/beacon/light_client/bootstrap/0x{}",
self.rpc, root_hex
);
let res = self
.client
let client = reqwest::Client::new();
let res = client
.get(req)
.send()
.await
@ -49,14 +41,15 @@ impl ConsensusRpc for NimbusRpc {
Ok(res.data)
}
async fn get_updates(&self, period: u64) -> Result<Vec<Update>> {
async fn get_updates(&self, period: u64, count: u8) -> Result<Vec<Update>> {
let count = cmp::min(count, MAX_REQUEST_LIGHT_CLIENT_UPDATES);
let req = format!(
"{}/eth/v1/beacon/light_client/updates?start_period={}&count=1000",
self.rpc, period
"{}/eth/v1/beacon/light_client/updates?start_period={}&count={}",
self.rpc, period, count
);
let res = self
.client
let client = reqwest::Client::new();
let res = client
.get(req)
.send()
.await
@ -70,10 +63,7 @@ impl ConsensusRpc for NimbusRpc {
async fn get_finality_update(&self) -> Result<FinalityUpdate> {
let req = format!("{}/eth/v1/beacon/light_client/finality_update", self.rpc);
let res = self
.client
.get(req)
.send()
let res = reqwest::get(req)
.await
.map_err(|e| RpcError::new("finality_update", e))?
.json::<FinalityUpdateResponse>()
@ -85,10 +75,7 @@ impl ConsensusRpc for NimbusRpc {
async fn get_optimistic_update(&self) -> Result<OptimisticUpdate> {
let req = format!("{}/eth/v1/beacon/light_client/optimistic_update", self.rpc);
let res = self
.client
.get(req)
.send()
let res = reqwest::get(req)
.await
.map_err(|e| RpcError::new("optimistic_update", e))?
.json::<OptimisticUpdateResponse>()
@ -100,10 +87,7 @@ impl ConsensusRpc for NimbusRpc {
async fn get_block(&self, slot: u64) -> Result<BeaconBlock> {
let req = format!("{}/eth/v2/beacon/blocks/{}", self.rpc, slot);
let res = self
.client
.get(req)
.send()
let res = reqwest::get(req)
.await
.map_err(|e| RpcError::new("blocks", e))?
.json::<BeaconBlockResponse>()
@ -112,6 +96,18 @@ impl ConsensusRpc for NimbusRpc {
Ok(res.data.message)
}
async fn chain_id(&self) -> Result<u64> {
let req = format!("{}/eth/v1/config/spec", self.rpc);
let res = reqwest::get(req)
.await
.map_err(|e| RpcError::new("spec", e))?
.json::<SpecResponse>()
.await
.map_err(|e| RpcError::new("spec", e))?;
Ok(res.data.chain_id)
}
}
#[derive(serde::Deserialize, Debug)]
@ -145,3 +141,14 @@ struct OptimisticUpdateResponse {
struct BootstrapResponse {
data: Bootstrap,
}
#[derive(serde::Deserialize, Debug)]
struct SpecResponse {
data: Spec,
}
#[derive(serde::Deserialize, Debug)]
struct Spec {
#[serde(rename = "DEPOSIT_NETWORK_ID", deserialize_with = "u64_deserialize")]
chain_id: u64,
}

View File

@ -4,6 +4,7 @@ use ssz_rs::prelude::*;
use common::types::Bytes32;
use common::utils::hex_str_to_bytes;
use superstruct::superstruct;
pub type BLSPubKey = Vector<u8, 48>;
pub type SignatureBytes = Vector<u8, 96>;
@ -24,7 +25,15 @@ pub struct BeaconBlock {
pub body: BeaconBlockBody,
}
#[derive(serde::Deserialize, Debug, Default, SimpleSerialize, Clone)]
#[superstruct(
variants(Bellatrix, Capella),
variant_attributes(
derive(serde::Deserialize, Clone, Debug, SimpleSerialize, Default),
serde(deny_unknown_fields)
)
)]
#[derive(serde::Deserialize, Debug, Clone)]
#[serde(untagged)]
pub struct BeaconBlockBody {
#[serde(deserialize_with = "signature_deserialize")]
randao_reveal: SignatureBytes,
@ -38,9 +47,79 @@ pub struct BeaconBlockBody {
voluntary_exits: List<SignedVoluntaryExit, 16>,
sync_aggregate: SyncAggregate,
pub execution_payload: ExecutionPayload,
#[superstruct(only(Capella))]
bls_to_execution_changes: List<SignedBlsToExecutionChange, 16>,
}
#[derive(serde::Deserialize, Debug, Default, SimpleSerialize, Clone)]
impl ssz_rs::Merkleized for BeaconBlockBody {
fn hash_tree_root(&mut self) -> Result<Node, MerkleizationError> {
match self {
BeaconBlockBody::Bellatrix(body) => body.hash_tree_root(),
BeaconBlockBody::Capella(body) => body.hash_tree_root(),
}
}
}
impl ssz_rs::Sized for BeaconBlockBody {
fn is_variable_size() -> bool {
true
}
fn size_hint() -> usize {
0
}
}
impl ssz_rs::Serialize for BeaconBlockBody {
fn serialize(&self, buffer: &mut Vec<u8>) -> Result<usize, SerializeError> {
match self {
BeaconBlockBody::Bellatrix(body) => body.serialize(buffer),
BeaconBlockBody::Capella(body) => body.serialize(buffer),
}
}
}
impl ssz_rs::Deserialize for BeaconBlockBody {
fn deserialize(_encoding: &[u8]) -> Result<Self, DeserializeError>
where
Self: Sized,
{
panic!("not implemented");
}
}
#[derive(Default, Clone, Debug, SimpleSerialize, serde::Deserialize)]
pub struct SignedBlsToExecutionChange {
message: BlsToExecutionChange,
#[serde(deserialize_with = "signature_deserialize")]
signature: SignatureBytes,
}
#[derive(Default, Clone, Debug, SimpleSerialize, serde::Deserialize)]
pub struct BlsToExecutionChange {
#[serde(deserialize_with = "u64_deserialize")]
validator_index: u64,
#[serde(deserialize_with = "pubkey_deserialize")]
from_bls_pubkey: BLSPubKey,
#[serde(deserialize_with = "address_deserialize")]
to_execution_address: Address,
}
impl Default for BeaconBlockBody {
fn default() -> Self {
BeaconBlockBody::Bellatrix(BeaconBlockBodyBellatrix::default())
}
}
#[superstruct(
variants(Bellatrix, Capella),
variant_attributes(
derive(serde::Deserialize, Debug, Default, SimpleSerialize, Clone),
serde(deny_unknown_fields)
)
)]
#[derive(serde::Deserialize, Debug, Clone)]
#[serde(untagged)]
pub struct ExecutionPayload {
#[serde(deserialize_with = "bytes32_deserialize")]
pub parent_hash: Bytes32,
@ -70,10 +149,67 @@ pub struct ExecutionPayload {
pub block_hash: Bytes32,
#[serde(deserialize_with = "transactions_deserialize")]
pub transactions: List<Transaction, 1048576>,
#[superstruct(only(Capella))]
withdrawals: List<Withdrawal, 16>,
}
#[derive(Default, Clone, Debug, SimpleSerialize, serde::Deserialize)]
pub struct Withdrawal {
#[serde(deserialize_with = "u64_deserialize")]
index: u64,
#[serde(deserialize_with = "u64_deserialize")]
validator_index: u64,
#[serde(deserialize_with = "address_deserialize")]
address: Address,
#[serde(deserialize_with = "u64_deserialize")]
amount: u64,
}
impl ssz_rs::Merkleized for ExecutionPayload {
fn hash_tree_root(&mut self) -> Result<Node, MerkleizationError> {
match self {
ExecutionPayload::Bellatrix(payload) => payload.hash_tree_root(),
ExecutionPayload::Capella(payload) => payload.hash_tree_root(),
}
}
}
impl ssz_rs::Sized for ExecutionPayload {
fn is_variable_size() -> bool {
true
}
fn size_hint() -> usize {
0
}
}
impl ssz_rs::Serialize for ExecutionPayload {
fn serialize(&self, buffer: &mut Vec<u8>) -> Result<usize, SerializeError> {
match self {
ExecutionPayload::Bellatrix(payload) => payload.serialize(buffer),
ExecutionPayload::Capella(payload) => payload.serialize(buffer),
}
}
}
impl ssz_rs::Deserialize for ExecutionPayload {
fn deserialize(_encoding: &[u8]) -> Result<Self, DeserializeError>
where
Self: Sized,
{
panic!("not implemented");
}
}
impl Default for ExecutionPayload {
fn default() -> Self {
ExecutionPayload::Bellatrix(ExecutionPayloadBellatrix::default())
}
}
#[derive(serde::Deserialize, Debug, Default, SimpleSerialize, Clone)]
struct ProposerSlashing {
pub struct ProposerSlashing {
signed_header_1: SignedBeaconBlockHeader,
signed_header_2: SignedBeaconBlockHeader,
}
@ -100,7 +236,7 @@ struct BeaconBlockHeader {
}
#[derive(serde::Deserialize, Debug, Default, SimpleSerialize, Clone)]
struct AttesterSlashing {
pub struct AttesterSlashing {
attestation_1: IndexedAttestation,
attestation_2: IndexedAttestation,
}
@ -115,7 +251,7 @@ struct IndexedAttestation {
}
#[derive(serde::Deserialize, Debug, Default, SimpleSerialize, Clone)]
struct Attestation {
pub struct Attestation {
aggregation_bits: Bitlist<2048>,
data: AttestationData,
#[serde(deserialize_with = "signature_deserialize")]
@ -143,7 +279,7 @@ struct Checkpoint {
}
#[derive(serde::Deserialize, Debug, Default, SimpleSerialize, Clone)]
struct SignedVoluntaryExit {
pub struct SignedVoluntaryExit {
message: VoluntaryExit,
#[serde(deserialize_with = "signature_deserialize")]
signature: SignatureBytes,
@ -158,7 +294,7 @@ struct VoluntaryExit {
}
#[derive(serde::Deserialize, Debug, Default, SimpleSerialize, Clone)]
struct Deposit {
pub struct Deposit {
#[serde(deserialize_with = "bytes_vector_deserialize")]
proof: Vector<Bytes32, 33>,
data: DepositData,
@ -188,6 +324,7 @@ pub struct Eth1Data {
#[derive(serde::Deserialize, Debug)]
pub struct Bootstrap {
#[serde(deserialize_with = "header_deserialize")]
pub header: Header,
pub current_sync_committee: SyncCommittee,
#[serde(deserialize_with = "branch_deserialize")]
@ -196,10 +333,12 @@ pub struct Bootstrap {
#[derive(serde::Deserialize, Debug, Clone)]
pub struct Update {
#[serde(deserialize_with = "header_deserialize")]
pub attested_header: Header,
pub next_sync_committee: SyncCommittee,
#[serde(deserialize_with = "branch_deserialize")]
pub next_sync_committee_branch: Vec<Bytes32>,
#[serde(deserialize_with = "header_deserialize")]
pub finalized_header: Header,
#[serde(deserialize_with = "branch_deserialize")]
pub finality_branch: Vec<Bytes32>,
@ -210,7 +349,9 @@ pub struct Update {
#[derive(serde::Deserialize, Debug)]
pub struct FinalityUpdate {
#[serde(deserialize_with = "header_deserialize")]
pub attested_header: Header,
#[serde(deserialize_with = "header_deserialize")]
pub finalized_header: Header,
#[serde(deserialize_with = "branch_deserialize")]
pub finality_branch: Vec<Bytes32>,
@ -221,6 +362,7 @@ pub struct FinalityUpdate {
#[derive(serde::Deserialize, Debug)]
pub struct OptimisticUpdate {
#[serde(deserialize_with = "header_deserialize")]
pub attested_header: Header,
pub sync_aggregate: SyncAggregate,
#[serde(deserialize_with = "u64_deserialize")]
@ -271,7 +413,7 @@ impl From<&Update> for GenericUpdate {
Self {
attested_header: update.attested_header.clone(),
sync_aggregate: update.sync_aggregate.clone(),
signature_slot: update.signature_slot.clone(),
signature_slot: update.signature_slot,
next_sync_committee: Some(update.next_sync_committee.clone()),
next_sync_committee_branch: Some(update.next_sync_committee_branch.clone()),
finalized_header: Some(update.finalized_header.clone()),
@ -285,7 +427,7 @@ impl From<&FinalityUpdate> for GenericUpdate {
Self {
attested_header: update.attested_header.clone(),
sync_aggregate: update.sync_aggregate.clone(),
signature_slot: update.signature_slot.clone(),
signature_slot: update.signature_slot,
next_sync_committee: None,
next_sync_committee_branch: None,
finalized_header: Some(update.finalized_header.clone()),
@ -299,7 +441,7 @@ impl From<&OptimisticUpdate> for GenericUpdate {
Self {
attested_header: update.attested_header.clone(),
sync_aggregate: update.sync_aggregate.clone(),
signature_slot: update.signature_slot.clone(),
signature_slot: update.signature_slot,
next_sync_committee: None,
next_sync_committee_branch: None,
finalized_header: None,
@ -322,14 +464,13 @@ where
D: serde::Deserializer<'de>,
{
let keys: Vec<String> = serde::Deserialize::deserialize(deserializer)?;
Ok(keys
.iter()
keys.iter()
.map(|key| {
let key_bytes = hex_str_to_bytes(key)?;
Ok(Vector::from_iter(key_bytes))
})
.collect::<Result<Vector<BLSPubKey, 512>>>()
.map_err(D::Error::custom)?)
.map_err(D::Error::custom)
}
fn bytes_vector_deserialize<'de, D>(deserializer: D) -> Result<Vector<Bytes32, 33>, D::Error>
@ -337,14 +478,14 @@ where
D: serde::Deserializer<'de>,
{
let elems: Vec<String> = serde::Deserialize::deserialize(deserializer)?;
Ok(elems
elems
.iter()
.map(|elem| {
let elem_bytes = hex_str_to_bytes(elem)?;
Ok(Vector::from_iter(elem_bytes))
})
.collect::<Result<Vector<Bytes32, 33>>>()
.map_err(D::Error::custom)?)
.map_err(D::Error::custom)
}
fn signature_deserialize<'de, D>(deserializer: D) -> Result<SignatureBytes, D::Error>
@ -361,17 +502,17 @@ where
D: serde::Deserializer<'de>,
{
let branch: Vec<String> = serde::Deserialize::deserialize(deserializer)?;
Ok(branch
branch
.iter()
.map(|elem| {
let elem_bytes = hex_str_to_bytes(elem)?;
Ok(Vector::from_iter(elem_bytes))
})
.collect::<Result<_>>()
.map_err(D::Error::custom)?)
.map_err(D::Error::custom)
}
fn u64_deserialize<'de, D>(deserializer: D) -> Result<u64, D::Error>
pub fn u64_deserialize<'de, D>(deserializer: D) -> Result<u64, D::Error>
where
D: serde::Deserializer<'de>,
{
@ -454,3 +595,27 @@ where
Ok(attesting_indices)
}
fn header_deserialize<'de, D>(deserializer: D) -> Result<Header, D::Error>
where
D: serde::Deserializer<'de>,
{
let header: LightClientHeader = serde::Deserialize::deserialize(deserializer)?;
Ok(match header {
LightClientHeader::Unwrapped(header) => header,
LightClientHeader::Wrapped(header) => header.beacon,
})
}
#[derive(serde::Deserialize)]
#[serde(untagged)]
enum LightClientHeader {
Unwrapped(Header),
Wrapped(Beacon),
}
#[derive(serde::Deserialize)]
struct Beacon {
beacon: Header,
}

View File

@ -1,9 +1,6 @@
use blst::{
min_pk::{PublicKey, Signature},
BLST_ERROR,
};
use common::{types::Bytes32, utils::bytes32_to_node};
use eyre::Result;
use milagro_bls::{AggregateSignature, PublicKey};
use ssz_rs::prelude::*;
use crate::types::{Header, SignatureBytes};
@ -14,10 +11,9 @@ pub fn calc_sync_period(slot: u64) -> u64 {
}
pub fn is_aggregate_valid(sig_bytes: &SignatureBytes, msg: &[u8], pks: &[&PublicKey]) -> bool {
let dst: &[u8] = b"BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_POP_";
let sig_res = Signature::from_bytes(&sig_bytes);
let sig_res = AggregateSignature::from_bytes(sig_bytes);
match sig_res {
Ok(sig) => sig.fast_aggregate_verify(true, msg, dst, &pks) == BLST_ERROR::BLST_SUCCESS,
Ok(sig) => sig.fast_aggregate_verify(msg, pks),
Err(_) => false,
}
}
@ -25,7 +21,7 @@ pub fn is_aggregate_valid(sig_bytes: &SignatureBytes, msg: &[u8], pks: &[&Public
pub fn is_proof_valid<L: Merkleized>(
attested_header: &Header,
leaf_object: &mut L,
branch: &Vec<Bytes32>,
branch: &[Bytes32],
depth: usize,
index: usize,
) -> bool {
@ -81,7 +77,6 @@ fn compute_fork_data_root(
current_version: Vector<u8, 4>,
genesis_validator_root: Bytes32,
) -> Result<Node> {
let current_version = current_version.try_into()?;
let mut fork_data = ForkData {
current_version,
genesis_validator_root,
@ -92,6 +87,6 @@ fn compute_fork_data_root(
pub fn branch_to_nodes(branch: Vec<Bytes32>) -> Result<Vec<Node>> {
branch
.iter()
.map(|elem| bytes32_to_node(elem))
.map(bytes32_to_node)
.collect::<Result<Vec<Node>>>()
}

View File

@ -10,6 +10,7 @@ async fn setup() -> ConsensusClient<MockRpc> {
execution_rpc: String::new(),
chain: base_config.chain,
forks: base_config.forks,
max_checkpoint_age: 123123123,
..Default::default()
};
@ -37,5 +38,5 @@ async fn test_get_payload() {
client.sync().await.unwrap();
let payload = client.get_execution_payload(&None).await.unwrap();
assert_eq!(payload.block_number, 7530932);
assert_eq!(*payload.block_number(), 7530932);
}

45
examples/basic.rs Normal file
View File

@ -0,0 +1,45 @@
use std::{path::PathBuf, str::FromStr};
use env_logger::Env;
use ethers::{types::Address, utils};
use eyre::Result;
use helios::{config::networks::Network, prelude::*};
#[tokio::main]
async fn main() -> Result<()> {
env_logger::Builder::from_env(Env::default().default_filter_or("info")).init();
let untrusted_rpc_url = "https://eth-mainnet.g.alchemy.com/v2/<YOUR_API_KEY>";
log::info!("Using untrusted RPC URL [REDACTED]");
let consensus_rpc = "https://www.lightclientdata.org";
log::info!("Using consensus RPC URL: {}", consensus_rpc);
let mut client: Client<FileDB> = ClientBuilder::new()
.network(Network::MAINNET)
.consensus_rpc(consensus_rpc)
.execution_rpc(untrusted_rpc_url)
.load_external_fallback()
.data_dir(PathBuf::from("/tmp/helios"))
.build()?;
log::info!(
"Built client on network \"{}\" with external checkpoint fallbacks",
Network::MAINNET
);
client.start().await?;
let head_block_num = client.get_block_number().await?;
let addr = Address::from_str("0x00000000219ab540356cBB839Cbe05303d7705Fa")?;
let block = BlockTag::Latest;
let balance = client.get_balance(&addr, block).await?;
log::info!("synced up to block: {}", head_block_num);
log::info!(
"balance of deposit contract: {}",
utils::format_ether(balance)
);
Ok(())
}

93
examples/call.rs Normal file
View File

@ -0,0 +1,93 @@
#![allow(deprecated)]
use env_logger::Env;
use ethers::prelude::*;
use std::{path::PathBuf, sync::Arc};
use helios::{
client::{Client, ClientBuilder, FileDB},
config::networks::Network,
types::{BlockTag, CallOpts},
};
// Generate the type-safe contract bindings with an ABI
abigen!(
Renderer,
r#"[
function renderBroker(uint256) external view returns (string memory)
function renderBroker(uint256, uint256) external view returns (string memory)
]"#,
event_derives(serde::Deserialize, serde::Serialize)
);
#[tokio::main]
async fn main() -> eyre::Result<()> {
env_logger::Builder::from_env(Env::default().default_filter_or("debug")).init();
// Load the rpc url using the `MAINNET_RPC_URL` environment variable
let eth_rpc_url = std::env::var("MAINNET_RPC_URL")?;
let consensus_rpc = "https://www.lightclientdata.org";
log::info!("Consensus RPC URL: {}", consensus_rpc);
// Construct the client
let data_dir = PathBuf::from("/tmp/helios");
let mut client: Client<FileDB> = ClientBuilder::new()
.network(Network::MAINNET)
.data_dir(data_dir)
.consensus_rpc(consensus_rpc)
.execution_rpc(&eth_rpc_url)
.load_external_fallback()
.build()?;
log::info!(
"[\"{}\"] Client built with external checkpoint fallbacks",
Network::MAINNET
);
// Start the client
client.start().await?;
// Call the erroneous account method
// The expected asset is: https://0x8bb9a8baeec177ae55ac410c429cbbbbb9198cac.w3eth.io/renderBroker/5
// Retrieved by calling `renderBroker(5)` on the contract: https://etherscan.io/address/0x8bb9a8baeec177ae55ac410c429cbbbbb9198cac#code
let account = "0x8bb9a8baeec177ae55ac410c429cbbbbb9198cac";
let method = "renderBroker(uint256)";
let method2 = "renderBroker(uint256, uint256)";
let argument = U256::from(5);
let address = account.parse::<Address>()?;
let block = BlockTag::Latest;
let provider = Provider::<Http>::try_from(eth_rpc_url)?;
let render = Renderer::new(address, Arc::new(provider.clone()));
log::debug!("Context: call @ {account}::{method} <{argument}>");
// Call using abigen
let result = render.render_broker_0(argument).call().await?;
log::info!(
"[ABIGEN] {account}::{method} -> Response Length: {:?}",
result.len()
);
let render = Renderer::new(address, Arc::new(provider.clone()));
let result = render
.render_broker_1(argument, U256::from(10))
.call()
.await?;
log::info!(
"[ABIGEN] {account}::{method2} -> Response Length: {:?}",
result.len()
);
// Call on helios client
let encoded_call = render.render_broker_0(argument).calldata().unwrap();
let call_opts = CallOpts {
from: Some("0xBE0eB53F46cd790Cd13851d5EFf43D12404d33E8".parse::<Address>()?),
to: Some(address),
gas: Some(U256::from(U64::MAX.as_u64())),
gas_price: None,
value: None,
data: Some(encoded_call.to_vec()),
};
log::debug!("Calling helios client on block: {block:?}");
let result = client.call(&call_opts, block).await?;
log::info!("[HELIOS] {account}::{method} ->{:?}", result.len());
Ok(())
}

42
examples/checkpoints.rs Normal file
View File

@ -0,0 +1,42 @@
use eyre::Result;
// From helios::config
use config::{checkpoints, networks};
#[tokio::main]
async fn main() -> Result<()> {
// Construct the checkpoint fallback services.
// The `build` method will fetch a list of [CheckpointFallbackService]s from a community-mainained list by ethPandaOps.
// This list is NOT guaranteed to be secure, but is provided in good faith.
// The raw list can be found here: https://github.com/ethpandaops/checkpoint-sync-health-checks/blob/master/_data/endpoints.yaml
let cf = checkpoints::CheckpointFallback::new()
.build()
.await
.unwrap();
// Fetch the latest goerli checkpoint
let goerli_checkpoint = cf
.fetch_latest_checkpoint(&networks::Network::GOERLI)
.await
.unwrap();
println!("Fetched latest goerli checkpoint: {goerli_checkpoint}");
// Fetch the latest mainnet checkpoint
let mainnet_checkpoint = cf
.fetch_latest_checkpoint(&networks::Network::MAINNET)
.await
.unwrap();
println!("Fetched latest mainnet checkpoint: {mainnet_checkpoint}");
// Let's get a list of all the fallback service endpoints for mainnet
let endpoints = cf.get_all_fallback_endpoints(&networks::Network::MAINNET);
println!("Fetched all mainnet fallback endpoints: {endpoints:?}");
// Since we built the checkpoint fallback services, we can also just get the raw checkpoint fallback services.
// The `get_fallback_services` method returns a reference to the internal list of CheckpointFallbackService objects
// for the given network.
let services = cf.get_fallback_services(&networks::Network::MAINNET);
println!("Fetched all mainnet fallback services: {services:?}");
Ok(())
}

42
examples/client.rs Normal file
View File

@ -0,0 +1,42 @@
use std::path::PathBuf;
use eyre::Result;
use helios::prelude::*;
#[tokio::main]
async fn main() -> Result<()> {
// Create a new Helios Client Builder
let mut builder = ClientBuilder::new();
// Set the network to mainnet
builder = builder.network(networks::Network::MAINNET);
// Set the consensus rpc url
builder = builder.consensus_rpc("https://www.lightclientdata.org");
// Set the execution rpc url
builder = builder.execution_rpc("https://eth-mainnet.g.alchemy.com/v2/XXXXX");
// Set the checkpoint to the last known checkpoint
builder =
builder.checkpoint("85e6151a246e8fdba36db27a0c7678a575346272fe978c9281e13a8b26cdfa68");
// Set the rpc port
builder = builder.rpc_port(8545);
// Set the data dir
builder = builder.data_dir(PathBuf::from("/tmp/helios"));
// Set the fallback service
builder = builder.fallback("https://sync-mainnet.beaconcha.in");
// Enable lazy checkpoints
builder = builder.load_external_fallback();
// Build the client
let _client: Client<FileDB> = builder.build().unwrap();
println!("Constructed client!");
Ok(())
}

15
examples/config.rs Normal file
View File

@ -0,0 +1,15 @@
use config::CliConfig;
use dirs::home_dir;
use eyre::Result;
use helios::prelude::*;
#[tokio::main]
async fn main() -> Result<()> {
// Load the config from the global config file
let config_path = home_dir().unwrap().join(".helios/helios.toml");
let config = Config::from_file(&config_path, "mainnet", &CliConfig::default());
println!("Constructed config: {config:#?}");
Ok(())
}

View File

@ -1,21 +1,17 @@
[package]
name = "execution"
version = "0.1.0"
version = "0.3.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
reqwest = { version = "0.11", features = ["json"] }
tokio = { version = "1", features = ["full"] }
eyre = "0.6.8"
serde = { version = "1.0.143", features = ["derive"] }
serde_json = "1.0.85"
hex = "0.4.3"
ssz-rs = { git = "https://github.com/ralexstokes/ssz-rs", rev = "cb08f18ca919cc1b685b861d0fa9e2daabe89737" }
blst = "0.3.10"
ssz-rs = { git = "https://github.com/ralexstokes/ssz-rs", rev = "d09f55b4f8554491e3431e01af1c32347a8781cd" }
revm = { version = "2.3", default-features = false, features = ["std", "k256", "with-serde"] }
ethers = "1.0.0"
revm = "2.1.0"
bytes = "1.2.1"
futures = "0.3.23"
toml = "0.5.9"
@ -23,7 +19,10 @@ triehash-ethereum = { git = "https://github.com/openethereum/parity-ethereum", r
async-trait = "0.1.57"
log = "0.4.17"
thiserror = "1.0.37"
openssl = { version = "0.10", features = ["vendored"] }
common = { path = "../common" }
consensus = { path = "../consensus" }
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
openssl = { version = "0.10", features = ["vendored"] }
tokio = { version = "1", features = ["full"] }

View File

@ -0,0 +1 @@
pub const PARALLEL_QUERY_BATCH_SIZE: usize = 20;

View File

@ -1,4 +1,9 @@
use ethers::types::{Address, H256};
use bytes::Bytes;
use ethers::{
abi::AbiDecode,
types::{Address, H256, U256},
};
use eyre::Report;
use thiserror::Error;
#[derive(Debug, Error)]
@ -13,4 +18,50 @@ pub enum ExecutionError {
ReceiptRootMismatch(String),
#[error("missing transaction for tx: {0}")]
MissingTransaction(String),
#[error("could not prove receipt for tx: {0}")]
NoReceiptForTransaction(String),
#[error("missing log for transaction: {0}, index: {1}")]
MissingLog(String, U256),
#[error("too many logs to prove: {0}, current limit is: {1}")]
TooManyLogsToProve(usize, usize),
#[error("execution rpc is for the incorect network")]
IncorrectRpcNetwork(),
#[error("Invalid base gas fee helios {0} vs rpc endpoint {1} at block {2}")]
InvalidBaseGaseFee(U256, U256, u64),
#[error("Invalid gas used ratio of helios {0} vs rpc endpoint {1} at block {2}")]
InvalidGasUsedRatio(f64, f64, u64),
#[error("Block {0} not found")]
BlockNotFoundError(u64),
#[error("Helios Execution Payload is empty")]
EmptyExecutionPayload(),
#[error("User query for block {0} but helios oldest block is {1}")]
InvalidBlockRange(u64, u64),
}
/// Errors that can occur during evm.rs calls
#[derive(Debug, Error)]
pub enum EvmError {
#[error("execution reverted: {0:?}")]
Revert(Option<Bytes>),
#[error("evm error: {0:?}")]
Generic(String),
#[error("evm execution failed: {0:?}")]
Revm(revm::Return),
#[error("rpc error: {0:?}")]
RpcError(Report),
}
impl EvmError {
pub fn decode_revert_reason(data: impl AsRef<[u8]>) -> Option<String> {
let data = data.as_ref();
// skip function selector
if data.len() < 4 {
return None;
}
String::decode(&data[4..]).ok()
}
}

View File

@ -1,5 +1,4 @@
use std::{
cmp,
collections::{BTreeMap, HashMap},
str::FromStr,
sync::Arc,
@ -7,21 +6,25 @@ use std::{
};
use bytes::Bytes;
use common::{errors::BlockNotFoundError, types::BlockTag};
use common::{
errors::{BlockNotFoundError, SlotNotFoundError},
types::BlockTag,
};
use ethers::{
abi::ethereum_types::BigEndianHash,
prelude::{Address, H160, H256, U256},
types::transaction::eip2930::AccessListItem,
types::{Address, H160, H256, U256},
};
use eyre::{Report, Result};
use futures::future::join_all;
use futures::{executor::block_on, future::join_all};
use log::trace;
use revm::{AccountInfo, Bytecode, Database, Env, TransactOut, TransactTo, EVM};
use tokio::runtime::Runtime;
use consensus::types::ExecutionPayload;
use crate::{
constants::PARALLEL_QUERY_BATCH_SIZE,
errors::EvmError,
rpc::ExecutionRpc,
types::{Account, CallOpts},
};
@ -47,7 +50,7 @@ impl<'a, R: ExecutionRpc> Evm<'a, R> {
Evm { evm, chain_id }
}
pub async fn call(&mut self, opts: &CallOpts) -> Result<Vec<u8>> {
pub async fn call(&mut self, opts: &CallOpts) -> Result<Vec<u8>, EvmError> {
let account_map = self.batch_fetch_accounts(opts).await?;
self.evm.db.as_mut().unwrap().set_accounts(account_map);
@ -55,32 +58,26 @@ impl<'a, R: ExecutionRpc> Evm<'a, R> {
let tx = self.evm.transact().0;
match tx.exit_reason {
revm::Return::Revert => Err(eyre::eyre!("execution reverted")),
revm::Return::OutOfGas => Err(eyre::eyre!("execution reverted: out of gas")),
revm::Return::OutOfFund => Err(eyre::eyre!("not enough funds")),
revm::Return::CallTooDeep => Err(eyre::eyre!("execution reverted: call too deep")),
revm::Return::InvalidJump => {
Err(eyre::eyre!("execution reverted: invalid jump destination"))
}
revm::Return::InvalidOpcode => Err(eyre::eyre!("execution reverted: invalid opcode")),
revm::Return::LackOfFundForGasLimit => Err(eyre::eyre!("not enough funds")),
revm::Return::GasPriceLessThenBasefee => Err(eyre::eyre!("gas price too low")),
revm::Return::Return => {
revm::Return::Revert => match tx.out {
TransactOut::Call(bytes) => Err(EvmError::Revert(Some(bytes))),
_ => Err(EvmError::Revert(None)),
},
revm::Return::Return | revm::Return::Stop => {
if let Some(err) = &self.evm.db.as_ref().unwrap().error {
return Err(eyre::eyre!(err.clone()));
return Err(EvmError::Generic(err.clone()));
}
match tx.out {
TransactOut::None => Err(eyre::eyre!("Invalid Call")),
TransactOut::Create(..) => Err(eyre::eyre!("Invalid Call")),
TransactOut::None => Err(EvmError::Generic("Invalid Call".to_string())),
TransactOut::Create(..) => Err(EvmError::Generic("Invalid Call".to_string())),
TransactOut::Call(bytes) => Ok(bytes.to_vec()),
}
}
_ => Err(eyre::eyre!("call failed")),
_ => Err(EvmError::Revm(tx.exit_reason)),
}
}
pub async fn estimate_gas(&mut self, opts: &CallOpts) -> Result<u64> {
pub async fn estimate_gas(&mut self, opts: &CallOpts) -> Result<u64, EvmError> {
let account_map = self.batch_fetch_accounts(opts).await?;
self.evm.db.as_mut().unwrap().set_accounts(account_map);
@ -89,35 +86,32 @@ impl<'a, R: ExecutionRpc> Evm<'a, R> {
let gas = tx.gas_used;
match tx.exit_reason {
revm::Return::Revert => Err(eyre::eyre!("execution reverted")),
revm::Return::OutOfGas => Err(eyre::eyre!("execution reverted: out of gas")),
revm::Return::OutOfFund => Err(eyre::eyre!("not enough funds")),
revm::Return::CallTooDeep => Err(eyre::eyre!("execution reverted: call too deep")),
revm::Return::InvalidJump => {
Err(eyre::eyre!("execution reverted: invalid jump destination"))
}
revm::Return::InvalidOpcode => Err(eyre::eyre!("execution reverted: invalid opcode")),
revm::Return::LackOfFundForGasLimit => Err(eyre::eyre!("not enough funds")),
revm::Return::GasPriceLessThenBasefee => Err(eyre::eyre!("gas price too low")),
revm::Return::Return => {
revm::Return::Revert => match tx.out {
TransactOut::Call(bytes) => Err(EvmError::Revert(Some(bytes))),
_ => Err(EvmError::Revert(None)),
},
revm::Return::Return | revm::Return::Stop => {
if let Some(err) = &self.evm.db.as_ref().unwrap().error {
return Err(eyre::eyre!(err.clone()));
return Err(EvmError::Generic(err.clone()));
}
// overestimate to avoid out of gas reverts
let gas_scaled = (1.10 * gas as f64) as u64;
Ok(gas_scaled)
}
_ => Err(eyre::eyre!("call failed")),
_ => Err(EvmError::Revm(tx.exit_reason)),
}
}
async fn batch_fetch_accounts(&self, opts: &CallOpts) -> Result<HashMap<Address, Account>> {
async fn batch_fetch_accounts(
&self,
opts: &CallOpts,
) -> Result<HashMap<Address, Account>, EvmError> {
let db = self.evm.db.as_ref().unwrap();
let rpc = db.execution.rpc.clone();
let payload = db.current_payload.clone();
let execution = db.execution.clone();
let block = db.current_payload.block_number;
let block = *db.current_payload.block_number();
let opts_moved = CallOpts {
from: opts.from,
@ -128,8 +122,11 @@ impl<'a, R: ExecutionRpc> Evm<'a, R> {
gas_price: opts.gas_price,
};
let block_moved = block.clone();
let mut list = rpc.create_access_list(&opts_moved, block_moved).await?.0;
let mut list = rpc
.create_access_list(&opts_moved, block)
.await
.map_err(EvmError::RpcError)?
.0;
let from_access_entry = AccessListItem {
address: opts_moved.from.unwrap_or_default(),
@ -137,12 +134,12 @@ impl<'a, R: ExecutionRpc> Evm<'a, R> {
};
let to_access_entry = AccessListItem {
address: opts_moved.to,
address: opts_moved.to.unwrap_or_default(),
storage_keys: Vec::default(),
};
let producer_account = AccessListItem {
address: Address::from_slice(&payload.fee_recipient),
address: Address::from_slice(payload.fee_recipient()),
storage_keys: Vec::default(),
};
@ -150,33 +147,26 @@ impl<'a, R: ExecutionRpc> Evm<'a, R> {
list.push(to_access_entry);
list.push(producer_account);
let mut accounts = Vec::new();
let batch_size = 20;
for i in (0..list.len()).step_by(batch_size) {
let end = cmp::min(i + batch_size, list.len());
let chunk = &list[i..end];
let mut account_map = HashMap::new();
for chunk in list.chunks(PARALLEL_QUERY_BATCH_SIZE) {
let account_chunk_futs = chunk.iter().map(|account| {
let addr_fut = futures::future::ready(account.address);
let account_fut = execution.get_account(
&account.address,
Some(account.storage_keys.as_slice()),
&payload,
);
async move { (addr_fut.await, account_fut.await) }
async move { (account.address, account_fut.await) }
});
let mut account_chunk = join_all(account_chunk_futs).await;
accounts.append(&mut account_chunk);
}
let account_chunk = join_all(account_chunk_futs).await;
let mut account_map = HashMap::new();
accounts.iter().for_each(|account| {
let addr = account.0;
if let Ok(account) = &account.1 {
account_map.insert(addr, account.clone());
}
});
account_chunk
.into_iter()
.filter(|i| i.1.is_ok())
.for_each(|(key, value)| {
account_map.insert(key, value.ok().unwrap());
});
}
Ok(account_map)
}
@ -185,17 +175,17 @@ impl<'a, R: ExecutionRpc> Evm<'a, R> {
let mut env = Env::default();
let payload = &self.evm.db.as_ref().unwrap().current_payload;
env.tx.transact_to = TransactTo::Call(opts.to);
env.tx.transact_to = TransactTo::Call(opts.to.unwrap_or_default());
env.tx.caller = opts.from.unwrap_or(Address::zero());
env.tx.value = opts.value.unwrap_or(U256::from(0));
env.tx.data = Bytes::from(opts.data.clone().unwrap_or(vec![]));
env.tx.data = Bytes::from(opts.data.clone().unwrap_or_default());
env.tx.gas_limit = opts.gas.map(|v| v.as_u64()).unwrap_or(u64::MAX);
env.tx.gas_price = opts.gas_price.unwrap_or(U256::zero());
env.block.number = U256::from(payload.block_number);
env.block.coinbase = Address::from_slice(&payload.fee_recipient);
env.block.timestamp = U256::from(payload.timestamp);
env.block.difficulty = U256::from_little_endian(&payload.prev_randao);
env.block.number = U256::from(*payload.block_number());
env.block.coinbase = Address::from_slice(payload.fee_recipient());
env.block.timestamp = U256::from(*payload.timestamp());
env.block.difficulty = U256::from_little_endian(payload.prev_randao());
env.cfg.chain_id = self.chain_id.into();
@ -232,14 +222,12 @@ impl<'a, R: ExecutionRpc> ProofDB<'a, R> {
fn get_account(&mut self, address: Address, slots: &[H256]) -> Result<Account> {
let execution = self.execution.clone();
let addr = address.clone();
let payload = self.current_payload.clone();
let slots = slots.to_owned();
let handle = thread::spawn(move || {
let account_fut = execution.get_account(&addr, Some(&slots), &payload);
let runtime = Runtime::new()?;
runtime.block_on(account_fut)
let account_fut = execution.get_account(&address, Some(&slots), &payload);
block_on(account_fut)
});
handle.join().unwrap()
@ -255,7 +243,7 @@ impl<'a, R: ExecutionRpc> Database for ProofDB<'a, R> {
}
trace!(
"fetch basic evm state for addess=0x{}",
"fetch basic evm state for address=0x{}",
hex::encode(address.as_bytes())
);
@ -278,34 +266,28 @@ impl<'a, R: ExecutionRpc> Database for ProofDB<'a, R> {
.payloads
.get(&number)
.ok_or(BlockNotFoundError::new(BlockTag::Number(number)))?;
Ok(H256::from_slice(&payload.block_hash))
Ok(H256::from_slice(payload.block_hash()))
}
fn storage(&mut self, address: H160, slot: U256) -> Result<U256, Report> {
trace!(
"fetch evm state for address=0x{}, slot={}",
hex::encode(address.as_bytes()),
slot
);
trace!("fetch evm state for address={:?}, slot={}", address, slot);
let slot = H256::from_uint(&slot);
Ok(match self.accounts.get(&address) {
Some(account) => match account.slots.get(&slot) {
Some(slot) => slot.clone(),
None => self
Some(slot) => *slot,
None => *self
.get_account(address, &[slot])?
.slots
.get(&slot)
.unwrap()
.clone(),
.ok_or(SlotNotFoundError::new(slot))?,
},
None => self
None => *self
.get_account(address, &[slot])?
.slots
.get(&slot)
.unwrap()
.clone(),
.ok_or(SlotNotFoundError::new(slot))?,
})
}
@ -318,3 +300,61 @@ fn is_precompile(address: &Address) -> bool {
address.le(&Address::from_str("0x0000000000000000000000000000000000000009").unwrap())
&& address.gt(&Address::zero())
}
#[cfg(test)]
mod tests {
use common::utils::hex_str_to_bytes;
use consensus::types::ExecutionPayloadBellatrix;
use ssz_rs::Vector;
use crate::rpc::mock_rpc::MockRpc;
use super::*;
fn get_client() -> ExecutionClient<MockRpc> {
ExecutionClient::new("testdata/").unwrap()
}
#[test]
fn test_proof_db() {
// Construct proofdb params
let execution = get_client();
let address = Address::from_str("14f9D4aF749609c1438528C0Cce1cC3f6D411c47").unwrap();
let payload = ExecutionPayload::Bellatrix(ExecutionPayloadBellatrix {
state_root: Vector::from_iter(
hex_str_to_bytes(
"0xaa02f5db2ee75e3da400d10f3c30e894b6016ce8a2501680380a907b6674ce0d",
)
.unwrap(),
),
..ExecutionPayloadBellatrix::default()
});
let mut payloads = BTreeMap::new();
payloads.insert(7530933, payload.clone());
// Construct the proof database with the given client and payloads
let mut proof_db = ProofDB::new(Arc::new(execution), &payload, &payloads);
// Set the proof db accounts
let slot = U256::from(1337);
let mut accounts = HashMap::new();
let account = Account {
balance: U256::from(100),
code: hex_str_to_bytes("0x").unwrap(),
..Default::default()
};
accounts.insert(address, account);
proof_db.set_accounts(accounts);
// Get the account from the proof database
let storage_proof = proof_db.storage(address, slot);
// Check that the storage proof correctly returns a slot not found error
let expected_err: eyre::Report = SlotNotFoundError::new(H256::from_uint(&slot)).into();
assert_eq!(
expected_err.to_string(),
storage_proof.unwrap_err().to_string()
);
}
}

View File

@ -3,9 +3,9 @@ use std::str::FromStr;
use ethers::abi::AbiEncode;
use ethers::prelude::{Address, U256};
use ethers::types::{Transaction, TransactionReceipt, H256};
use ethers::types::{FeeHistory, Filter, Log, Transaction, TransactionReceipt, H256};
use ethers::utils::keccak256;
use ethers::utils::rlp::{encode, RlpStream};
use ethers::utils::rlp::{encode, Encodable, RlpStream};
use eyre::Result;
use common::utils::hex_str_to_bytes;
@ -21,6 +21,10 @@ use super::proof::{encode_account, verify_proof};
use super::rpc::ExecutionRpc;
use super::types::{Account, ExecutionBlock};
// We currently limit the max number of logs to fetch,
// to avoid blocking the client for too long.
const MAX_SUPPORTED_LOGS_NUMBER: usize = 5;
#[derive(Clone)]
pub struct ExecutionClient<R: ExecutionRpc> {
pub rpc: R,
@ -28,10 +32,18 @@ pub struct ExecutionClient<R: ExecutionRpc> {
impl<R: ExecutionRpc> ExecutionClient<R> {
pub fn new(rpc: &str) -> Result<Self> {
let rpc = ExecutionRpc::new(rpc)?;
let rpc: R = ExecutionRpc::new(rpc)?;
Ok(ExecutionClient { rpc })
}
pub async fn check_rpc(&self, chain_id: u64) -> Result<()> {
if self.rpc.chain_id().await? != chain_id {
Err(ExecutionError::IncorrectRpcNetwork().into())
} else {
Ok(())
}
}
pub async fn get_account(
&self,
address: &Address,
@ -42,7 +54,7 @@ impl<R: ExecutionRpc> ExecutionClient<R> {
let proof = self
.rpc
.get_proof(&address, slots, payload.block_number)
.get_proof(address, slots, *payload.block_number())
.await?;
let account_path = keccak256(address.as_bytes()).to_vec();
@ -50,7 +62,7 @@ impl<R: ExecutionRpc> ExecutionClient<R> {
let is_valid = verify_proof(
&proof.account_proof,
&payload.state_root,
payload.state_root(),
&account_path,
&account_encoded,
);
@ -69,7 +81,7 @@ impl<R: ExecutionRpc> ExecutionClient<R> {
let is_valid = verify_proof(
&storage_proof.proof,
&proof.storage_hash.as_bytes().to_vec(),
proof.storage_hash.as_bytes(),
&key_hash.to_vec(),
&value,
);
@ -86,7 +98,7 @@ impl<R: ExecutionRpc> ExecutionClient<R> {
let code = if proof.code_hash == KECCAK_EMPTY {
Vec::new()
} else {
let code = self.rpc.get_code(address, payload.block_number).await?;
let code = self.rpc.get_code(address, *payload.block_number()).await?;
let code_hash = keccak256(&code).into();
if proof.code_hash != code_hash {
@ -111,7 +123,7 @@ impl<R: ExecutionRpc> ExecutionClient<R> {
})
}
pub async fn send_raw_transaction(&self, bytes: &Vec<u8>) -> Result<H256> {
pub async fn send_raw_transaction(&self, bytes: &[u8]) -> Result<H256> {
self.rpc.send_raw_transaction(bytes).await
}
@ -124,15 +136,15 @@ impl<R: ExecutionRpc> ExecutionClient<R> {
let empty_uncle_hash = "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347";
let tx_hashes = payload
.transactions
.transactions()
.iter()
.map(|tx| H256::from_slice(&keccak256(tx.to_vec())))
.map(|tx| H256::from_slice(&keccak256(tx)))
.collect::<Vec<H256>>();
let txs = if full_tx {
let txs_fut = tx_hashes.iter().map(|hash| async move {
let mut payloads = BTreeMap::new();
payloads.insert(payload.block_number, payload.clone());
payloads.insert(*payload.block_number(), payload.clone());
let tx = self
.get_transaction(hash, &payloads)
.await?
@ -151,22 +163,22 @@ impl<R: ExecutionRpc> ExecutionClient<R> {
};
Ok(ExecutionBlock {
number: payload.block_number,
base_fee_per_gas: U256::from_little_endian(&payload.base_fee_per_gas.to_bytes_le()),
number: *payload.block_number(),
base_fee_per_gas: U256::from_little_endian(&payload.base_fee_per_gas().to_bytes_le()),
difficulty: U256::from(0),
extra_data: payload.extra_data.to_vec(),
gas_limit: payload.gas_limit,
gas_used: payload.gas_used,
hash: H256::from_slice(&payload.block_hash),
logs_bloom: payload.logs_bloom.to_vec(),
miner: Address::from_slice(&payload.fee_recipient),
parent_hash: H256::from_slice(&payload.parent_hash),
receipts_root: H256::from_slice(&payload.receipts_root),
state_root: H256::from_slice(&payload.state_root),
timestamp: payload.timestamp,
extra_data: payload.extra_data().to_vec(),
gas_limit: *payload.gas_limit(),
gas_used: *payload.gas_used(),
hash: H256::from_slice(payload.block_hash()),
logs_bloom: payload.logs_bloom().to_vec(),
miner: Address::from_slice(payload.fee_recipient()),
parent_hash: H256::from_slice(payload.parent_hash()),
receipts_root: H256::from_slice(payload.receipts_root()),
state_root: H256::from_slice(payload.state_root()),
timestamp: *payload.timestamp(),
total_difficulty: 0,
transactions: txs,
mix_hash: H256::from_slice(&payload.prev_randao),
mix_hash: H256::from_slice(payload.prev_randao()),
nonce: empty_nonce,
sha3_uncles: H256::from_str(empty_uncle_hash)?,
size: 0,
@ -175,6 +187,21 @@ impl<R: ExecutionRpc> ExecutionClient<R> {
})
}
pub async fn get_transaction_by_block_hash_and_index(
&self,
payload: &ExecutionPayload,
index: usize,
) -> Result<Option<Transaction>> {
let tx = payload.transactions()[index].clone();
let tx_hash = H256::from_slice(&keccak256(tx));
let mut payloads = BTreeMap::new();
payloads.insert(*payload.block_number(), payload.clone());
let tx_option = self.get_transaction(&tx_hash, &payloads).await?;
let tx = tx_option.ok_or(eyre::eyre!("not reachable"))?;
Ok(Some(tx))
}
pub async fn get_transaction_receipt(
&self,
tx_hash: &H256,
@ -195,7 +222,7 @@ impl<R: ExecutionRpc> ExecutionClient<R> {
let payload = payload.unwrap();
let tx_hashes = payload
.transactions
.transactions()
.iter()
.map(|tx| H256::from_slice(&keccak256(tx)))
.collect::<Vec<H256>>();
@ -208,14 +235,11 @@ impl<R: ExecutionRpc> ExecutionClient<R> {
let receipts = join_all(receipts_fut).await;
let receipts = receipts.into_iter().collect::<Result<Vec<_>>>()?;
let receipts_encoded: Vec<Vec<u8>> = receipts
.iter()
.map(|receipt| encode_receipt(&receipt))
.collect();
let receipts_encoded: Vec<Vec<u8>> = receipts.iter().map(encode_receipt).collect();
let expected_receipt_root = ordered_trie_root(receipts_encoded);
let expected_receipt_root = H256::from_slice(&expected_receipt_root.to_fixed_bytes());
let payload_receipt_root = H256::from_slice(&payload.receipts_root);
let payload_receipt_root = H256::from_slice(payload.receipts_root());
if expected_receipt_root != payload_receipt_root || !receipts.contains(&receipt) {
return Err(ExecutionError::ReceiptRootMismatch(tx_hash.to_string()).into());
@ -251,7 +275,7 @@ impl<R: ExecutionRpc> ExecutionClient<R> {
let tx_encoded = tx.rlp().to_vec();
let txs_encoded = payload
.transactions
.transactions()
.iter()
.map(|tx| tx.to_vec())
.collect::<Vec<_>>();
@ -259,9 +283,182 @@ impl<R: ExecutionRpc> ExecutionClient<R> {
if !txs_encoded.contains(&tx_encoded) {
return Err(ExecutionError::MissingTransaction(hash.to_string()).into());
}
Ok(Some(tx))
}
pub async fn get_logs(
&self,
filter: &Filter,
payloads: &BTreeMap<u64, ExecutionPayload>,
) -> Result<Vec<Log>> {
let filter = filter.clone();
// avoid fetching logs for a block helios hasn't seen yet
let filter = if filter.get_to_block().is_none() && filter.get_block_hash().is_none() {
let block = *payloads.last_key_value().unwrap().0;
let filter = filter.to_block(block);
if filter.get_from_block().is_none() {
filter.from_block(block)
} else {
filter
}
} else {
filter
};
let logs = self.rpc.get_logs(&filter).await?;
if logs.len() > MAX_SUPPORTED_LOGS_NUMBER {
return Err(
ExecutionError::TooManyLogsToProve(logs.len(), MAX_SUPPORTED_LOGS_NUMBER).into(),
);
}
for (_pos, log) in logs.iter().enumerate() {
// For every log
// Get the hash of the tx that generated it
let tx_hash = log
.transaction_hash
.ok_or(eyre::eyre!("tx hash not found in log"))?;
// Get its proven receipt
let receipt = self
.get_transaction_receipt(&tx_hash, payloads)
.await?
.ok_or(ExecutionError::NoReceiptForTransaction(tx_hash.to_string()))?;
// Check if the receipt contains the desired log
// Encoding logs for comparison
let receipt_logs_encoded = receipt
.logs
.iter()
.map(|log| log.rlp_bytes())
.collect::<Vec<_>>();
let log_encoded = log.rlp_bytes();
if !receipt_logs_encoded.contains(&log_encoded) {
return Err(ExecutionError::MissingLog(
tx_hash.to_string(),
log.log_index.unwrap(),
)
.into());
}
}
Ok(logs)
}
pub async fn get_fee_history(
&self,
block_count: u64,
last_block: u64,
_reward_percentiles: &[f64],
payloads: &BTreeMap<u64, ExecutionPayload>,
) -> Result<Option<FeeHistory>> {
// Extract the latest and oldest block numbers from the payloads
let helios_latest_block_number = *payloads
.last_key_value()
.ok_or(ExecutionError::EmptyExecutionPayload())?
.0;
let helios_oldest_block_number = *payloads
.first_key_value()
.ok_or(ExecutionError::EmptyExecutionPayload())?
.0;
// Case where all requested blocks are earlier than Helios' latest block number
// So helios can't prove anything in this range
if last_block < helios_oldest_block_number {
return Err(
ExecutionError::InvalidBlockRange(last_block, helios_latest_block_number).into(),
);
}
// If the requested block is more recent than helios' latest block
// we can only return up to helios' latest block
let mut request_latest_block = last_block;
if request_latest_block > helios_latest_block_number {
request_latest_block = helios_latest_block_number;
}
// Requested oldest block is further out than what helios' synced
let mut request_oldest_block = request_latest_block - block_count;
if request_oldest_block < helios_oldest_block_number {
request_oldest_block = helios_oldest_block_number;
}
// Construct a fee history
let mut fee_history = FeeHistory {
oldest_block: U256::from(request_oldest_block),
base_fee_per_gas: vec![],
gas_used_ratio: vec![],
reward: payloads.iter().map(|_| vec![]).collect::<Vec<Vec<U256>>>(),
};
for block_id in request_oldest_block..=request_latest_block {
let execution_payload = payloads
.get(&block_id)
.ok_or(ExecutionError::EmptyExecutionPayload())?;
let converted_base_fee_per_gas = ethers::types::U256::from_little_endian(
&execution_payload.base_fee_per_gas().to_bytes_le(),
);
fee_history
.base_fee_per_gas
.push(converted_base_fee_per_gas);
let gas_used_ratio_helios = ((*execution_payload.gas_used() as f64
/ *execution_payload.gas_limit() as f64)
* 10.0_f64.powi(12))
.round()
/ 10.0_f64.powi(12);
fee_history.gas_used_ratio.push(gas_used_ratio_helios);
}
Ok(Some(fee_history))
}
}
/// Verifies a fee history against an rpc.
pub async fn verify_fee_history(
rpc: &impl ExecutionRpc,
calculated_fee_history: &FeeHistory,
block_count: u64,
request_latest_block: u64,
reward_percentiles: &[f64],
) -> Result<()> {
let fee_history = rpc
.get_fee_history(block_count, request_latest_block, reward_percentiles)
.await?;
for (_pos, _base_fee_per_gas) in fee_history.base_fee_per_gas.iter().enumerate() {
// Break at last iteration
// Otherwise, this would add an additional block
if _pos == block_count as usize {
continue;
}
// Check base fee per gas
let block_to_check = (fee_history.oldest_block + _pos as u64).as_u64();
let fee_to_check = calculated_fee_history.base_fee_per_gas[_pos];
let gas_ratio_to_check = calculated_fee_history.gas_used_ratio[_pos];
if *_base_fee_per_gas != fee_to_check {
return Err(ExecutionError::InvalidBaseGaseFee(
fee_to_check,
*_base_fee_per_gas,
block_to_check,
)
.into());
}
// Check gas used ratio
let rpc_gas_used_rounded =
(fee_history.gas_used_ratio[_pos] * 10.0_f64.powi(12)).round() / 10.0_f64.powi(12);
if gas_ratio_to_check != rpc_gas_used_rounded {
return Err(ExecutionError::InvalidGasUsedRatio(
gas_ratio_to_check,
rpc_gas_used_rounded,
block_to_check,
)
.into());
}
}
Ok(())
}
fn encode_receipt(receipt: &TransactionReceipt) -> Vec<u8> {

View File

@ -1,3 +1,4 @@
pub mod constants;
pub mod errors;
pub mod evm;
pub mod rpc;

View File

@ -2,8 +2,8 @@ use ethers::types::{Bytes, EIP1186ProofResponse};
use ethers::utils::keccak256;
use ethers::utils::rlp::{decode_list, RlpStream};
pub fn verify_proof(proof: &Vec<Bytes>, root: &Vec<u8>, path: &Vec<u8>, value: &Vec<u8>) -> bool {
let mut expected_hash = root.clone();
pub fn verify_proof(proof: &Vec<Bytes>, root: &[u8], path: &Vec<u8>, value: &Vec<u8>) -> bool {
let mut expected_hash = root.to_vec();
let mut path_offset = 0;
for (i, node) in proof.iter().enumerate() {
@ -16,14 +16,14 @@ pub fn verify_proof(proof: &Vec<Bytes>, root: &Vec<u8>, path: &Vec<u8>, value: &
if node_list.len() == 17 {
if i == proof.len() - 1 {
// exclusion proof
let nibble = get_nibble(&path, path_offset);
let nibble = get_nibble(path, path_offset);
let node = &node_list[nibble as usize];
if node.len() == 0 && is_empty_value(value) {
if node.is_empty() && is_empty_value(value) {
return true;
}
} else {
let nibble = get_nibble(&path, path_offset);
let nibble = get_nibble(path, path_offset);
expected_hash = node_list[nibble as usize].clone();
path_offset += 1;
@ -31,12 +31,8 @@ pub fn verify_proof(proof: &Vec<Bytes>, root: &Vec<u8>, path: &Vec<u8>, value: &
} else if node_list.len() == 2 {
if i == proof.len() - 1 {
// exclusion proof
if !paths_match(
&node_list[0],
skip_length(&node_list[0]),
&path,
path_offset,
) && is_empty_value(value)
if !paths_match(&node_list[0], skip_length(&node_list[0]), path, path_offset)
&& is_empty_value(value)
{
return true;
}
@ -46,13 +42,18 @@ pub fn verify_proof(proof: &Vec<Bytes>, root: &Vec<u8>, path: &Vec<u8>, value: &
return paths_match(
&node_list[0],
skip_length(&node_list[0]),
&path,
path,
path_offset,
);
}
} else {
let node_path = &node_list[0];
let prefix_length = shared_prefix_length(path, path_offset, node_path);
if prefix_length < node_path.len() * 2 - skip_length(node_path) {
// The proof shows a divergent path, but we're not
// at the end of the proof, so something's wrong.
return false;
}
path_offset += prefix_length;
expected_hash = node_list[1].clone();
}
@ -89,7 +90,7 @@ fn get_rest_path(p: &Vec<u8>, s: usize) -> String {
let mut ret = String::new();
for i in s..p.len() * 2 {
let n = get_nibble(p, i);
ret += &format!("{:01x}", n);
ret += &format!("{n:01x}");
}
ret
}
@ -112,29 +113,21 @@ fn is_empty_value(value: &Vec<u8>) -> bool {
fn shared_prefix_length(path: &Vec<u8>, path_offset: usize, node_path: &Vec<u8>) -> usize {
let skip_length = skip_length(node_path);
let mut node_decoded = vec![];
for i in skip_length..node_path.len() * 2 {
let decoded_nibble_offset = i - skip_length;
if decoded_nibble_offset % 2 == 0 {
let shifted = get_nibble(node_path, i) << 4;
node_decoded.push(shifted);
} else {
let byte = &node_decoded.get(decoded_nibble_offset / 2).unwrap().clone();
let right = get_nibble(node_path, i);
node_decoded.pop();
node_decoded.push(byte | right);
}
}
let len = node_decoded.len() * 2;
let len = std::cmp::min(
node_path.len() * 2 - skip_length,
path.len() * 2 - path_offset,
);
let mut prefix_len = 0;
for i in 0..len {
let path_nibble = get_nibble(path, i + path_offset);
let node_path_nibble = get_nibble(&node_decoded, i);
let node_path_nibble = get_nibble(node_path, i + skip_length);
if path_nibble == node_path_nibble {
prefix_len += 1;
} else {
break;
}
}
@ -142,7 +135,7 @@ fn shared_prefix_length(path: &Vec<u8>, path_offset: usize, node_path: &Vec<u8>)
}
fn skip_length(node: &Vec<u8>) -> usize {
if node.len() == 0 {
if node.is_empty() {
return 0;
}
@ -156,7 +149,7 @@ fn skip_length(node: &Vec<u8>) -> usize {
}
}
fn get_nibble(path: &Vec<u8>, offset: usize) -> u8 {
fn get_nibble(path: &[u8], offset: usize) -> u8 {
let byte = path[offset / 2];
if offset % 2 == 0 {
byte >> 4
@ -174,3 +167,28 @@ pub fn encode_account(proof: &EIP1186ProofResponse) -> Vec<u8> {
let encoded = stream.out();
encoded.to_vec()
}
#[cfg(test)]
mod tests {
use crate::proof::shared_prefix_length;
#[tokio::test]
async fn test_shared_prefix_length() {
// We compare the path starting from the 6th nibble i.e. the 6 in 0x6f
let path: Vec<u8> = vec![0x12, 0x13, 0x14, 0x6f, 0x6c, 0x64, 0x21];
let path_offset = 6;
// Our node path matches only the first 5 nibbles of the path
let node_path: Vec<u8> = vec![0x6f, 0x6c, 0x63, 0x21];
let shared_len = shared_prefix_length(&path, path_offset, &node_path);
assert_eq!(shared_len, 5);
// Now we compare the path starting from the 5th nibble i.e. the 4 in 0x14
let path: Vec<u8> = vec![0x12, 0x13, 0x14, 0x6f, 0x6c, 0x64, 0x21];
let path_offset = 5;
// Our node path matches only the first 7 nibbles of the path
// Note the first nibble is 1, so we skip 1 nibble
let node_path: Vec<u8> = vec![0x14, 0x6f, 0x6c, 0x64, 0x11];
let shared_len = shared_prefix_length(&path, path_offset, &node_path);
assert_eq!(shared_len, 7);
}
}

View File

@ -1,18 +1,18 @@
use std::str::FromStr;
use async_trait::async_trait;
use common::errors::RpcError;
use ethers::prelude::{Address, Http};
use ethers::providers::{HttpRateLimitRetryPolicy, Middleware, Provider, RetryClient};
use ethers::types::transaction::eip2718::TypedTransaction;
use ethers::types::transaction::eip2930::AccessList;
use ethers::types::{
BlockId, Bytes, EIP1186ProofResponse, Eip1559TransactionRequest, Transaction,
TransactionReceipt, H256, U256,
BlockId, BlockNumber, Bytes, EIP1186ProofResponse, Eip1559TransactionRequest, FeeHistory,
Filter, Log, Transaction, TransactionReceipt, H256, U256,
};
use eyre::Result;
use crate::types::CallOpts;
use common::errors::RpcError;
use super::ExecutionRpc;
@ -27,13 +27,16 @@ impl Clone for HttpRpc {
}
}
#[async_trait]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
impl ExecutionRpc for HttpRpc {
fn new(rpc: &str) -> Result<Self> {
let http = Http::from_str(rpc)?;
let mut client = RetryClient::new(http, Box::new(HttpRateLimitRetryPolicy), 100, 50);
client.set_compute_units(300);
let provider = Provider::new(client);
Ok(HttpRpc {
url: rpc.to_string(),
provider,
@ -60,7 +63,7 @@ impl ExecutionRpc for HttpRpc {
let block = Some(BlockId::from(block));
let mut raw_tx = Eip1559TransactionRequest::new();
raw_tx.to = Some(opts.to.into());
raw_tx.to = Some(opts.to.unwrap_or_default().into());
raw_tx.from = opts.from;
raw_tx.value = opts.value;
raw_tx.gas = Some(opts.gas.unwrap_or(U256::from(100_000_000)));
@ -92,8 +95,8 @@ impl ExecutionRpc for HttpRpc {
Ok(code.to_vec())
}
async fn send_raw_transaction(&self, bytes: &Vec<u8>) -> Result<H256> {
let bytes = Bytes::from(bytes.as_slice().to_owned());
async fn send_raw_transaction(&self, bytes: &[u8]) -> Result<H256> {
let bytes = Bytes::from(bytes.to_owned());
let tx = self
.provider
.send_raw_transaction(bytes)
@ -120,4 +123,35 @@ impl ExecutionRpc for HttpRpc {
.await
.map_err(|e| RpcError::new("get_transaction", e))?)
}
async fn get_logs(&self, filter: &Filter) -> Result<Vec<Log>> {
Ok(self
.provider
.get_logs(filter)
.await
.map_err(|e| RpcError::new("get_logs", e))?)
}
async fn chain_id(&self) -> Result<u64> {
Ok(self
.provider
.get_chainid()
.await
.map_err(|e| RpcError::new("chain_id", e))?
.as_u64())
}
async fn get_fee_history(
&self,
block_count: u64,
last_block: u64,
reward_percentiles: &[f64],
) -> Result<FeeHistory> {
let block = BlockNumber::from(last_block);
Ok(self
.provider
.fee_history(block_count, block, reward_percentiles)
.await
.map_err(|e| RpcError::new("fee_history", e))?)
}
}

View File

@ -3,8 +3,8 @@ use std::{fs::read_to_string, path::PathBuf};
use async_trait::async_trait;
use common::utils::hex_str_to_bytes;
use ethers::types::{
transaction::eip2930::AccessList, Address, EIP1186ProofResponse, Transaction,
TransactionReceipt, H256,
transaction::eip2930::AccessList, Address, EIP1186ProofResponse, FeeHistory, Filter, Log,
Transaction, TransactionReceipt, H256,
};
use eyre::{eyre, Result};
@ -17,7 +17,8 @@ pub struct MockRpc {
path: PathBuf,
}
#[async_trait]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
impl ExecutionRpc for MockRpc {
fn new(rpc: &str) -> Result<Self> {
let path = PathBuf::from(rpc);
@ -43,7 +44,7 @@ impl ExecutionRpc for MockRpc {
hex_str_to_bytes(&code[0..code.len() - 1])
}
async fn send_raw_transaction(&self, _bytes: &Vec<u8>) -> Result<H256> {
async fn send_raw_transaction(&self, _bytes: &[u8]) -> Result<H256> {
Err(eyre!("not implemented"))
}
@ -56,4 +57,23 @@ impl ExecutionRpc for MockRpc {
let tx = read_to_string(self.path.join("transaction.json"))?;
Ok(serde_json::from_str(&tx)?)
}
async fn get_logs(&self, _filter: &Filter) -> Result<Vec<Log>> {
let logs = read_to_string(self.path.join("logs.json"))?;
Ok(serde_json::from_str(&logs)?)
}
async fn chain_id(&self) -> Result<u64> {
Err(eyre!("not implemented"))
}
async fn get_fee_history(
&self,
_block_count: u64,
_last_block: u64,
_reward_percentiles: &[f64],
) -> Result<FeeHistory> {
let fee_history = read_to_string(self.path.join("fee_history.json"))?;
Ok(serde_json::from_str(&fee_history)?)
}
}

View File

@ -1,7 +1,7 @@
use async_trait::async_trait;
use ethers::types::{
transaction::eip2930::AccessList, Address, EIP1186ProofResponse, Transaction,
TransactionReceipt, H256,
transaction::eip2930::AccessList, Address, EIP1186ProofResponse, FeeHistory, Filter, Log,
Transaction, TransactionReceipt, H256,
};
use eyre::Result;
@ -10,7 +10,8 @@ use crate::types::CallOpts;
pub mod http_rpc;
pub mod mock_rpc;
#[async_trait]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
pub trait ExecutionRpc: Send + Clone + Sync + 'static {
fn new(rpc: &str) -> Result<Self>
where
@ -25,7 +26,15 @@ pub trait ExecutionRpc: Send + Clone + Sync + 'static {
async fn create_access_list(&self, opts: &CallOpts, block: u64) -> Result<AccessList>;
async fn get_code(&self, address: &Address, block: u64) -> Result<Vec<u8>>;
async fn send_raw_transaction(&self, bytes: &Vec<u8>) -> Result<H256>;
async fn send_raw_transaction(&self, bytes: &[u8]) -> Result<H256>;
async fn get_transaction_receipt(&self, tx_hash: &H256) -> Result<Option<TransactionReceipt>>;
async fn get_transaction(&self, tx_hash: &H256) -> Result<Option<Transaction>>;
async fn get_logs(&self, filter: &Filter) -> Result<Vec<Log>>;
async fn chain_id(&self) -> Result<u64>;
async fn get_fee_history(
&self,
block_count: u64,
last_block: u64,
reward_percentiles: &[f64],
) -> Result<FeeHistory>;
}

View File

@ -19,7 +19,7 @@ pub struct Account {
pub slots: HashMap<H256, U256>,
}
#[derive(Deserialize, Serialize, Debug)]
#[derive(Deserialize, Serialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct ExecutionBlock {
#[serde(serialize_with = "serialize_u64_string")]
@ -54,17 +54,17 @@ pub struct ExecutionBlock {
pub uncles: Vec<H256>,
}
#[derive(Deserialize, Serialize, Debug)]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum Transactions {
Hashes(Vec<H256>),
Full(Vec<Transaction>),
}
#[derive(Deserialize, Serialize)]
#[derive(Deserialize, Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct CallOpts {
pub from: Option<Address>,
pub to: Address,
pub to: Option<Address>,
pub gas: Option<U256>,
pub gas_price: Option<U256>,
pub value: Option<U256>,
@ -78,7 +78,7 @@ impl fmt::Debug for CallOpts {
.field("from", &self.from)
.field("to", &self.to)
.field("value", &self.value)
.field("data", &hex::encode(&self.data.clone().unwrap_or_default()))
.field("data", &hex::encode(self.data.clone().unwrap_or_default()))
.finish()
}
}
@ -90,7 +90,7 @@ where
let bytes: Option<String> = serde::Deserialize::deserialize(deserializer)?;
match bytes {
Some(bytes) => {
let bytes = hex::decode(bytes.strip_prefix("0x").unwrap()).unwrap();
let bytes = hex::decode(bytes.strip_prefix("0x").unwrap_or("")).unwrap_or_default();
Ok(Some(bytes.to_vec()))
}
None => Ok(None),

35
execution/testdata/fee_history.json vendored Normal file
View File

@ -0,0 +1,35 @@
{
"id": "1",
"jsonrpc": "2.0",
"result": {
"oldestBlock": 10762137,
"reward": [
[
"0x4a817c7ee",
"0x4a817c7ee"
], [
"0x773593f0",
"0x773593f5"
], [
"0x0",
"0x0"
], [
"0x773593f5",
"0x773bae75"
]
],
"baseFeePerGas": [
"0x12",
"0x10",
"0x10",
"0xe",
"0xd"
],
"gasUsedRatio": [
0.026089875,
0.406803,
0,
0.0866665
]
}
}

17
execution/testdata/logs.json vendored Normal file
View File

@ -0,0 +1,17 @@
[
{
"transactionHash": "0x2dac1b27ab58b493f902dda8b63979a112398d747f1761c0891777c0983e591f",
"address": "0x326c977e6efc84e512bb9c30f76e30c160ed06fb",
"blockHash": "0x6663f197e991f5a0bb235f33ec554b9bd48c37b4f5002d7ac2abdfa99f86ac14",
"blockNumber": "0x72e9b5",
"data": "0x000000000000000000000000000000000000000000000001158e460913d00000",
"logIndex": "0x0",
"removed": false,
"topics": [
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
"0x0000000000000000000000004281ecf07378ee595c564a59048801330f3084ee",
"0x0000000000000000000000007daccf9b3c1ae2fa5c55f1c978aeef700bc83be0"
],
"transactionIndex": "0x0"
}
]

View File

@ -1,11 +1,11 @@
use std::collections::BTreeMap;
use std::str::FromStr;
use ethers::types::{Address, H256, U256};
use ethers::types::{Address, Filter, H256, U256};
use ssz_rs::{List, Vector};
use common::utils::hex_str_to_bytes;
use consensus::types::ExecutionPayload;
use consensus::types::{ExecutionPayload, ExecutionPayloadBellatrix};
use execution::rpc::mock_rpc::MockRpc;
use execution::ExecutionClient;
@ -18,11 +18,13 @@ async fn test_get_account() {
let execution = get_client();
let address = Address::from_str("14f9D4aF749609c1438528C0Cce1cC3f6D411c47").unwrap();
let mut payload = ExecutionPayload::default();
payload.state_root = Vector::from_iter(
hex_str_to_bytes("0xaa02f5db2ee75e3da400d10f3c30e894b6016ce8a2501680380a907b6674ce0d")
.unwrap(),
);
let payload = ExecutionPayload::Bellatrix(ExecutionPayloadBellatrix {
state_root: Vector::from_iter(
hex_str_to_bytes("0xaa02f5db2ee75e3da400d10f3c30e894b6016ce8a2501680380a907b6674ce0d")
.unwrap(),
),
..ExecutionPayloadBellatrix::default()
});
let account = execution
.get_account(&address, None, &payload)
@ -53,7 +55,7 @@ async fn test_get_tx() {
H256::from_str("2dac1b27ab58b493f902dda8b63979a112398d747f1761c0891777c0983e591f").unwrap();
let mut payload = ExecutionPayload::default();
payload.transactions.push(List::from_iter(hex_str_to_bytes("0x02f8b20583623355849502f900849502f91082ea6094326c977e6efc84e512bb9c30f76e30c160ed06fb80b844a9059cbb0000000000000000000000007daccf9b3c1ae2fa5c55f1c978aeef700bc83be0000000000000000000000000000000000000000000000001158e460913d00000c080a0e1445466b058b6f883c0222f1b1f3e2ad9bee7b5f688813d86e3fa8f93aa868ca0786d6e7f3aefa8fe73857c65c32e4884d8ba38d0ecfb947fbffb82e8ee80c167").unwrap()));
payload.transactions_mut().push(List::from_iter(hex_str_to_bytes("0x02f8b20583623355849502f900849502f91082ea6094326c977e6efc84e512bb9c30f76e30c160ed06fb80b844a9059cbb0000000000000000000000007daccf9b3c1ae2fa5c55f1c978aeef700bc83be0000000000000000000000000000000000000000000000001158e460913d00000c080a0e1445466b058b6f883c0222f1b1f3e2ad9bee7b5f688813d86e3fa8f93aa868ca0786d6e7f3aefa8fe73857c65c32e4884d8ba38d0ecfb947fbffb82e8ee80c167").unwrap()));
let mut payloads = BTreeMap::new();
payloads.insert(7530933, payload);
@ -99,19 +101,48 @@ async fn test_get_tx_not_included() {
assert!(tx_opt.is_none());
}
#[tokio::test]
async fn test_get_logs() {
let execution = get_client();
let mut payload = ExecutionPayload::Bellatrix(ExecutionPayloadBellatrix {
receipts_root: Vector::from_iter(
hex_str_to_bytes("dd82a78eccb333854f0c99e5632906e092d8a49c27a21c25cae12b82ec2a113f")
.unwrap(),
),
..ExecutionPayloadBellatrix::default()
});
payload.transactions_mut().push(List::from_iter(hex_str_to_bytes("0x02f8b20583623355849502f900849502f91082ea6094326c977e6efc84e512bb9c30f76e30c160ed06fb80b844a9059cbb0000000000000000000000007daccf9b3c1ae2fa5c55f1c978aeef700bc83be0000000000000000000000000000000000000000000000001158e460913d00000c080a0e1445466b058b6f883c0222f1b1f3e2ad9bee7b5f688813d86e3fa8f93aa868ca0786d6e7f3aefa8fe73857c65c32e4884d8ba38d0ecfb947fbffb82e8ee80c167").unwrap()));
let mut payloads = BTreeMap::new();
payloads.insert(7530933, payload);
let filter = Filter::new();
let logs = execution.get_logs(&filter, &payloads).await.unwrap();
let tx_hash =
H256::from_str("2dac1b27ab58b493f902dda8b63979a112398d747f1761c0891777c0983e591f").unwrap();
assert!(!logs.is_empty());
assert!(logs[0].transaction_hash.is_some());
assert!(logs[0].transaction_hash.unwrap() == tx_hash);
}
#[tokio::test]
async fn test_get_receipt() {
let execution = get_client();
let tx_hash =
H256::from_str("2dac1b27ab58b493f902dda8b63979a112398d747f1761c0891777c0983e591f").unwrap();
let mut payload = ExecutionPayload::default();
payload.receipts_root = Vector::from_iter(
hex_str_to_bytes("dd82a78eccb333854f0c99e5632906e092d8a49c27a21c25cae12b82ec2a113f")
.unwrap(),
);
let mut payload = ExecutionPayload::Bellatrix(ExecutionPayloadBellatrix {
receipts_root: Vector::from_iter(
hex_str_to_bytes("dd82a78eccb333854f0c99e5632906e092d8a49c27a21c25cae12b82ec2a113f")
.unwrap(),
),
..ExecutionPayloadBellatrix::default()
});
payload.transactions.push(List::from_iter(hex_str_to_bytes("0x02f8b20583623355849502f900849502f91082ea6094326c977e6efc84e512bb9c30f76e30c160ed06fb80b844a9059cbb0000000000000000000000007daccf9b3c1ae2fa5c55f1c978aeef700bc83be0000000000000000000000000000000000000000000000001158e460913d00000c080a0e1445466b058b6f883c0222f1b1f3e2ad9bee7b5f688813d86e3fa8f93aa868ca0786d6e7f3aefa8fe73857c65c32e4884d8ba38d0ecfb947fbffb82e8ee80c167").unwrap()));
payload.transactions_mut().push(List::from_iter(hex_str_to_bytes("0x02f8b20583623355849502f900849502f91082ea6094326c977e6efc84e512bb9c30f76e30c160ed06fb80b844a9059cbb0000000000000000000000007daccf9b3c1ae2fa5c55f1c978aeef700bc83be0000000000000000000000000000000000000000000000001158e460913d00000c080a0e1445466b058b6f883c0222f1b1f3e2ad9bee7b5f688813d86e3fa8f93aa868ca0786d6e7f3aefa8fe73857c65c32e4884d8ba38d0ecfb947fbffb82e8ee80c167").unwrap()));
let mut payloads = BTreeMap::new();
payloads.insert(7530933, payload);
@ -132,7 +163,7 @@ async fn test_get_receipt_bad_proof() {
H256::from_str("2dac1b27ab58b493f902dda8b63979a112398d747f1761c0891777c0983e591f").unwrap();
let mut payload = ExecutionPayload::default();
payload.transactions.push(List::from_iter(hex_str_to_bytes("0x02f8b20583623355849502f900849502f91082ea6094326c977e6efc84e512bb9c30f76e30c160ed06fb80b844a9059cbb0000000000000000000000007daccf9b3c1ae2fa5c55f1c978aeef700bc83be0000000000000000000000000000000000000000000000001158e460913d00000c080a0e1445466b058b6f883c0222f1b1f3e2ad9bee7b5f688813d86e3fa8f93aa868ca0786d6e7f3aefa8fe73857c65c32e4884d8ba38d0ecfb947fbffb82e8ee80c167").unwrap()));
payload.transactions_mut().push(List::from_iter(hex_str_to_bytes("0x02f8b20583623355849502f900849502f91082ea6094326c977e6efc84e512bb9c30f76e30c160ed06fb80b844a9059cbb0000000000000000000000007daccf9b3c1ae2fa5c55f1c978aeef700bc83be0000000000000000000000000000000000000000000000001158e460913d00000c080a0e1445466b058b6f883c0222f1b1f3e2ad9bee7b5f688813d86e3fa8f93aa868ca0786d6e7f3aefa8fe73857c65c32e4884d8ba38d0ecfb947fbffb82e8ee80c167").unwrap()));
let mut payloads = BTreeMap::new();
payloads.insert(7530933, payload);
@ -160,10 +191,32 @@ async fn test_get_receipt_not_included() {
#[tokio::test]
async fn test_get_block() {
let execution = get_client();
let mut payload = ExecutionPayload::default();
payload.block_number = 12345;
let payload = ExecutionPayload::Bellatrix(ExecutionPayloadBellatrix {
block_number: 12345,
..ExecutionPayloadBellatrix::default()
});
let block = execution.get_block(&payload, false).await.unwrap();
assert_eq!(block.number, 12345);
}
#[tokio::test]
async fn test_get_tx_by_block_hash_and_index() {
let execution = get_client();
let tx_hash =
H256::from_str("2dac1b27ab58b493f902dda8b63979a112398d747f1761c0891777c0983e591f").unwrap();
let mut payload = ExecutionPayload::Bellatrix(ExecutionPayloadBellatrix {
block_number: 7530933,
..ExecutionPayloadBellatrix::default()
});
payload.transactions_mut().push(List::from_iter(hex_str_to_bytes("0x02f8b20583623355849502f900849502f91082ea6094326c977e6efc84e512bb9c30f76e30c160ed06fb80b844a9059cbb0000000000000000000000007daccf9b3c1ae2fa5c55f1c978aeef700bc83be0000000000000000000000000000000000000000000000001158e460913d00000c080a0e1445466b058b6f883c0222f1b1f3e2ad9bee7b5f688813d86e3fa8f93aa868ca0786d6e7f3aefa8fe73857c65c32e4884d8ba38d0ecfb947fbffb82e8ee80c167").unwrap()));
let tx = execution
.get_transaction_by_block_hash_and_index(&payload, 0)
.await
.unwrap()
.unwrap();
assert_eq!(tx.hash(), tx_hash);
}

32
helios-ts/Cargo.toml Normal file
View File

@ -0,0 +1,32 @@
[package]
name = "helios-ts"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
crate-type = ["cdylib"]
[dependencies]
wasm-bindgen = "0.2.84"
wasm-bindgen-futures = "0.4.33"
serde-wasm-bindgen = "0.4.5"
console_error_panic_hook = "0.1.7"
ethers = "1.0.0"
hex = "0.4.3"
serde = { version = "1.0.143", features = ["derive"] }
serde_json = "1.0.85"
client = { path = "../client" }
common = { path = "../common" }
consensus = { path = "../consensus" }
execution = { path = "../execution" }
config = { path = "../config" }
[dependencies.web-sys]
version = "0.3"
features = [
"console",
]

24
helios-ts/index.html Normal file
View File

@ -0,0 +1,24 @@
<!DOCTYPE html>
<html lang="en-US">
<head>
<meta charset="utf-8" />
<title>hello-wasm example</title>
</head>
<body>
<script src="./dist/lib.js"></script>
<script src="https://cdn.ethers.io/lib/ethers-5.2.umd.min.js"></script>
<script>
const config = {
executionRpc:"http://localhost:9001/proxy",
consensusRpc: "http://localhost:9002/proxy",
checkpoint: "0x372342db81e3a42527e08dc19e33cd4f91f440f45b9ddb0a9865d407eceb08e4",
};
helios.createHeliosProvider(config).then(heliosProvider => {
heliosProvider.sync().then(() => {
window.provider = new ethers.providers.Web3Provider(heliosProvider);
});
});
</script>
</body>
</html>

111
helios-ts/lib.ts Normal file
View File

@ -0,0 +1,111 @@
import init, { Client } from "./pkg/index";
export async function createHeliosProvider(config: Config): Promise<HeliosProvider> {
const wasmData = require("./pkg/index_bg.wasm");
await init(wasmData);
return new HeliosProvider(config);
}
/// An EIP-1193 compliant Ethereum provider. Treat this the same as you
/// would window.ethereum when constructing an ethers or web3 provider.
export class HeliosProvider {
#client;
#chainId;
/// Do not use this constructor. Instead use the createHeliosProvider function.
constructor(config: Config) {
const executionRpc = config.executionRpc;
const consensusRpc = config.consensusRpc;
const checkpoint = config.checkpoint;
const network = config.network ?? Network.MAINNET;
this.#client = new Client(executionRpc, consensusRpc, network, checkpoint);
this.#chainId = this.#client.chain_id();
}
async sync() {
await this.#client.sync();
}
async request(req: Request): Promise<any> {
switch(req.method) {
case "eth_getBalance": {
return this.#client.get_balance(req.params[0], req.params[1]);
};
case "eth_chainId": {
return this.#chainId;
};
case "eth_blockNumber": {
return this.#client.get_block_number();
};
case "eth_getTransactionByHash": {
let tx = await this.#client.get_transaction_by_hash(req.params[0]);
return mapToObj(tx);
};
case "eth_getTransactionCount": {
return this.#client.get_transaction_count(req.params[0], req.params[1]);
};
case "eth_getBlockTransactionCountByHash": {
return this.#client.get_block_transaction_count_by_hash(req.params[0]);
};
case "eth_getBlockTransactionCountByNumber": {
return this.#client.get_block_transaction_count_by_number(req.params[0]);
};
case "eth_getCode": {
return this.#client.get_code(req.params[0], req.params[1]);
};
case "eth_call": {
return this.#client.call(req.params[0], req.params[1]);
};
case "eth_estimateGas": {
return this.#client.estimate_gas(req.params[0]);
};
case "eth_gasPrice": {
return this.#client.gas_price();
};
case "eth_maxPriorityFeePerGas": {
return this.#client.max_priority_fee_per_gas();
};
case "eth_sendRawTransaction": {
return this.#client.send_raw_transaction(req.params[0]);
};
case "eth_getTransactionReceipt": {
return this.#client.get_transaction_receipt(req.params[0]);
};
case "eth_getLogs": {
return this.#client.get_logs(req.params[0]);
};
case "net_version": {
return this.#chainId;
};
}
}
}
export type Config = {
executionRpc: string,
consensusRpc?: string,
checkpoint?: string,
network?: Network,
}
export enum Network {
MAINNET = "mainnet",
GOERLI = "goerli",
}
type Request = {
method: string,
params: any[],
}
function mapToObj(map: Map<any, any> | undefined): Object | undefined {
if(!map) return undefined;
return Array.from(map).reduce((obj: any, [key, value]) => {
obj[key] = value;
return obj;
}, {});
}

4117
helios-ts/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

22
helios-ts/package.json Normal file
View File

@ -0,0 +1,22 @@
{
"name": "helios",
"version": "0.1.0",
"main": "./dist/lib.js",
"types": "./dist/lib.d.ts",
"scripts": {
"build": "webpack"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@wasm-tool/wasm-pack-plugin": "^1.6.0",
"ts-loader": "^9.4.1",
"typescript": "^4.9.3",
"webpack": "^5.75.0",
"webpack-cli": "^5.0.0"
},
"dependencies": {
"ethers": "^5.7.2"
}
}

7
helios-ts/run.sh Executable file
View File

@ -0,0 +1,7 @@
set -e
(&>/dev/null lcp --proxyUrl https://eth-mainnet.g.alchemy.com/v2/23IavJytUwkTtBMpzt_TZKwgwAarocdT --port 9001 &)
(&>/dev/null lcp --proxyUrl https://www.lightclientdata.org --port 9002 &)
npm run build
simple-http-server

185
helios-ts/src/lib.rs Normal file
View File

@ -0,0 +1,185 @@
extern crate console_error_panic_hook;
extern crate web_sys;
use std::str::FromStr;
use common::types::BlockTag;
use ethers::types::{Address, Filter, H256};
use execution::types::CallOpts;
use wasm_bindgen::prelude::*;
use client::database::ConfigDB;
use config::{networks, Config};
#[allow(unused_macros)]
macro_rules! log {
( $( $t:tt )* ) => {
web_sys::console::log_1(&format!( $( $t )* ).into());
}
}
#[wasm_bindgen]
pub struct Client {
inner: client::Client<ConfigDB>,
chain_id: u64,
}
#[wasm_bindgen]
impl Client {
#[wasm_bindgen(constructor)]
pub fn new(
execution_rpc: String,
consensus_rpc: Option<String>,
network: String,
checkpoint: Option<String>,
) -> Self {
console_error_panic_hook::set_once();
let base = match network.as_str() {
"mainnet" => networks::mainnet(),
"goerli" => networks::goerli(),
_ => panic!("invalid network"),
};
let chain_id = base.chain.chain_id;
let checkpoint = Some(
checkpoint
.as_ref()
.map(|c| c.strip_prefix("0x").unwrap_or(c.as_str()))
.map(|c| hex::decode(c).unwrap())
.unwrap_or(base.default_checkpoint),
);
let consensus_rpc = consensus_rpc.unwrap_or(base.consensus_rpc.unwrap());
let config = Config {
execution_rpc,
consensus_rpc,
checkpoint,
chain: base.chain,
forks: base.forks,
..Default::default()
};
let inner: client::Client<ConfigDB> =
client::ClientBuilder::new().config(config).build().unwrap();
Self { inner, chain_id }
}
#[wasm_bindgen]
pub async fn sync(&mut self) {
self.inner.start().await.unwrap()
}
#[wasm_bindgen]
pub fn chain_id(&self) -> u32 {
self.chain_id as u32
}
#[wasm_bindgen]
pub async fn get_block_number(&self) -> u32 {
self.inner.get_block_number().await.unwrap() as u32
}
#[wasm_bindgen]
pub async fn get_balance(&self, addr: JsValue, block: JsValue) -> String {
let addr: Address = serde_wasm_bindgen::from_value(addr).unwrap();
let block: BlockTag = serde_wasm_bindgen::from_value(block).unwrap();
self.inner
.get_balance(&addr, block)
.await
.unwrap()
.to_string()
}
#[wasm_bindgen]
pub async fn get_transaction_by_hash(&self, hash: String) -> JsValue {
let hash = H256::from_str(&hash).unwrap();
let tx = self.inner.get_transaction_by_hash(&hash).await.unwrap();
serde_wasm_bindgen::to_value(&tx).unwrap()
}
#[wasm_bindgen]
pub async fn get_transaction_count(&self, addr: JsValue, block: JsValue) -> u32 {
let addr: Address = serde_wasm_bindgen::from_value(addr).unwrap();
let block: BlockTag = serde_wasm_bindgen::from_value(block).unwrap();
self.inner.get_nonce(&addr, block).await.unwrap() as u32
}
#[wasm_bindgen]
pub async fn get_block_transaction_count_by_hash(&self, hash: JsValue) -> u32 {
let hash: H256 = serde_wasm_bindgen::from_value(hash).unwrap();
self.inner
.get_block_transaction_count_by_hash(&hash.as_bytes().to_vec())
.await
.unwrap() as u32
}
#[wasm_bindgen]
pub async fn get_block_transaction_count_by_number(&self, block: JsValue) -> u32 {
let block: BlockTag = serde_wasm_bindgen::from_value(block).unwrap();
self.inner
.get_block_transaction_count_by_number(block)
.await
.unwrap() as u32
}
#[wasm_bindgen]
pub async fn get_code(&self, addr: JsValue, block: JsValue) -> String {
let addr: Address = serde_wasm_bindgen::from_value(addr).unwrap();
let block: BlockTag = serde_wasm_bindgen::from_value(block).unwrap();
let code = self.inner.get_code(&addr, block).await.unwrap();
format!("0x{}", hex::encode(code))
}
#[wasm_bindgen]
pub async fn call(&self, opts: JsValue, block: JsValue) -> String {
let opts: CallOpts = serde_wasm_bindgen::from_value(opts).unwrap();
let block: BlockTag = serde_wasm_bindgen::from_value(block).unwrap();
let res = self.inner.call(&opts, block).await.unwrap();
format!("0x{}", hex::encode(res))
}
#[wasm_bindgen]
pub async fn estimate_gas(&self, opts: JsValue) -> u32 {
let opts: CallOpts = serde_wasm_bindgen::from_value(opts).unwrap();
self.inner.estimate_gas(&opts).await.unwrap() as u32
}
#[wasm_bindgen]
pub async fn gas_price(&self) -> JsValue {
let price = self.inner.get_gas_price().await.unwrap();
serde_wasm_bindgen::to_value(&price).unwrap()
}
#[wasm_bindgen]
pub async fn max_priority_fee_per_gas(&self) -> JsValue {
let price = self.inner.get_priority_fee().await.unwrap();
serde_wasm_bindgen::to_value(&price).unwrap()
}
#[wasm_bindgen]
pub async fn send_raw_transaction(&self, tx: String) -> JsValue {
let tx = hex::decode(tx).unwrap();
let hash = self.inner.send_raw_transaction(&tx).await.unwrap();
serde_wasm_bindgen::to_value(&hash).unwrap()
}
#[wasm_bindgen]
pub async fn get_transaction_receipt(&self, tx: JsValue) -> JsValue {
let tx: H256 = serde_wasm_bindgen::from_value(tx).unwrap();
let receipt = self.inner.get_transaction_receipt(&tx).await.unwrap();
serde_wasm_bindgen::to_value(&receipt).unwrap()
}
#[wasm_bindgen]
pub async fn get_logs(&self, filter: JsValue) -> JsValue {
let filter: Filter = serde_wasm_bindgen::from_value(filter).unwrap();
let logs = self.inner.get_logs(&filter).await.unwrap();
serde_wasm_bindgen::to_value(&logs).unwrap()
}
}

13
helios-ts/tsconfig.json Normal file
View File

@ -0,0 +1,13 @@
{
"compilerOptions": {
"outDir": "./dist/",
"noImplicitAny": true,
"module": "es6",
"target": "es6",
"jsx": "react",
"allowJs": true,
"moduleResolution": "node",
"sourceMap": true,
"declaration": true
}
}

View File

@ -0,0 +1,40 @@
const path = require("path");
const WasmPackPlugin = require("@wasm-tool/wasm-pack-plugin");
module.exports = {
entry: "./lib.ts",
module: {
rules: [
{
test: /\.ts?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
{
test: /\.wasm$/,
type: "asset/inline",
},
],
},
resolve: {
extensions: ['.ts', '.js'],
},
output: {
filename: "lib.js",
globalObject: 'this',
path: path.resolve(__dirname, "dist"),
library: {
name: "helios",
type: "umd",
}
},
experiments: {
asyncWebAssembly: true,
},
plugins: [
new WasmPackPlugin({
extraArgs: "--target web",
crateDirectory: path.resolve(__dirname),
}),
],
};

View File

@ -44,4 +44,4 @@ fi
TARBALL_URL="https://github.com/$REPO/releases/download/${TAG}/${NAME}_${PLATFORM}_${ARCHITECTURE}.tar.gz"
curl -L $TARBALL_URL | tar -xzC $BIN_DIR
echo "Installed $NAME"
echo "Installed $NAME"

27
rpc.md Normal file
View File

@ -0,0 +1,27 @@
# Helios Remote Procedure Calls
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.
## RPC Methods
| RPC Method | Client Function | Description | Example |
| ---------- | --------------- | ----------- | ------- |
| `eth_getBalance` | `get_balance` | Returns the balance of the account given an address. | `client.get_balance(&self, address: &str, block: BlockTag)` |
| `eth_getTransactionCount` | `get_nonce` | Returns the number of transactions sent from the given address. | `client.get_nonce(&self, address: &str, block: BlockTag)` |
| `eth_getCode` | `get_code` | Returns the code at a given address. | `client.get_code(&self, address: &str, block: BlockTag)` |
| `eth_call` | `call` | Executes a new message call immediately without creating a transaction on the blockchain. | `client.call(&self, opts: CallOpts, block: BlockTag)` |
| `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)` |
| `eth_getChainId` | `chain_id` | Returns the chain ID of the current network. | `client.chain_id(&self)` |
| `eth_gasPrice` | `gas_price` | Returns the current price per gas in wei. | `client.gas_price(&self)` |
| `eth_maxPriorityFeePerGas` | `max_priority_fee_per_gas` | Returns the current max priority fee per gas in wei. | `client.max_priority_fee_per_gas(&self)` |
| `eth_blockNumber` | `block_number` | Returns the number of the most recent block. | `client.block_number(&self)` |
| `eth_getBlockByNumber` | `get_block_by_number` | Returns the information of a block by number. | `get_block_by_number(&self, block: BlockTag, full_tx: bool)` |
| `eth_getBlockByHash` | `get_block_by_hash` | Returns the information of a block by hash. | `get_block_by_hash(&self, hash: &str, full_tx: bool)` |
| `eth_sendRawTransaction` | `send_raw_transaction` | Submits a raw transaction to the network. | `client.send_raw_transaction(&self, bytes: &str)` |
| `eth_getTransactionReceipt` | `get_transaction_receipt` | Returns the receipt of a transaction by transaction hash. | `client.get_transaction_receipt(&self, hash: &str)` |
| `eth_getLogs` | `get_logs` | Returns an array of logs matching the filter. | `client.get_logs(&self, filter: Filter)` |
| `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)` |
| `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)` |
| `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)` |
| `eth_coinbase` | `get_coinbase` | Returns the client coinbase address. | `client.get_coinbase(&self)` |
| `eth_syncing` | `syncing` | Returns an object with data about the sync status or false. | `client.syncing(&self)` |

1
rust-toolchain Normal file
View File

@ -0,0 +1 @@
nightly-2023-01-23

View File

@ -1,13 +1,68 @@
#![warn(missing_debug_implementations, rust_2018_idioms, unreachable_pub)]
#![deny(rustdoc::broken_intra_doc_links)]
#![doc(test(
no_crate_inject,
attr(deny(warnings, rust_2018_idioms), allow(dead_code, unused_variables))
))]
//! # Ethereum light client written in Rust.
//!
//! > helios is a fully trustless, efficient, and portable Ethereum light client written in Rust.
//!
//! 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.
//!
//! 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.
//!
//! ## Quickstart: `prelude`
//!
//! The prelude imports all the necessary data types and traits from helios. Use this to quickly bootstrap a new project.
//!
//! ```no_run
//! # #[allow(unused)]
//! use helios::prelude::*;
//! ```
//!
//! Examples on how you can use the types imported by the prelude can be found in
//! the [`examples` directory of the repository](https://github.com/a16z/helios/tree/master/examples)
//! and in the `tests/` directories of each crate.
//!
//! ## Breakdown of exported helios modules
//!
//! ### `client`
//!
//! The `client` module exports three main types: `Client`, `ClientBuilder`, and `FileDB`.
//!
//! `ClientBuilder` is a builder for the `Client` type. It allows you to configure the client using the fluent builder pattern.
//!
//! `Client` serves Ethereum RPC endpoints locally that call a node on the backend.
//!
//! Finally, the `FileDB` type is a simple local database. It is used by the `Client` to store checkpoint data.
//!
//! ### `config`
//!
//! The `config` module provides the configuration types for all of helios. It is used by the `ClientBuilder` to configure the `Client`.
//!
//! ### `types`
//!
//! Generic types used across helios.
//!
//! ### `errors`
//!
//! Errors used across helios.
pub mod client {
pub use client::{database::FileDB, Client, ClientBuilder};
#[cfg(not(target_arch = "wasm32"))]
pub use client::database::FileDB;
pub use client::{database::ConfigDB, Client, ClientBuilder};
}
pub mod config {
pub use config::{networks, Config};
pub use config::{checkpoints, networks, Config};
}
pub mod types {
pub use common::types::BlockTag;
pub use execution::types::{Account, CallOpts, ExecutionBlock, Transactions};
}
pub mod errors {
@ -15,3 +70,10 @@ pub mod errors {
pub use consensus::errors::*;
pub use execution::errors::*;
}
pub mod prelude {
pub use crate::client::*;
pub use crate::config::*;
pub use crate::errors::*;
pub use crate::types::*;
}

122
tests/feehistory.rs Normal file
View File

@ -0,0 +1,122 @@
use env_logger::Env;
use eyre::Result;
use helios::{config::networks::Network, prelude::*};
use std::time::Duration;
use std::{env, path::PathBuf};
#[tokio::test]
async fn feehistory() -> Result<()> {
env_logger::Builder::from_env(Env::default().default_filter_or("info")).init();
// Client Configuration
let api_key = env::var("MAINNET_RPC_URL").expect("MAINNET_RPC_URL env variable missing");
let checkpoint = "0x4d9b87a319c52e54068b7727a93dd3d52b83f7336ed93707bcdf7b37aefce700";
let consensus_rpc = "https://www.lightclientdata.org";
let data_dir = "/tmp/helios";
log::info!("Using consensus RPC URL: {}", consensus_rpc);
// Instantiate Client
let mut client: Client<FileDB> = ClientBuilder::new()
.network(Network::MAINNET)
.consensus_rpc(consensus_rpc)
.execution_rpc(&api_key)
.checkpoint(checkpoint)
.load_external_fallback()
.data_dir(PathBuf::from(data_dir))
.build()?;
log::info!(
"Built client on \"{}\" with external checkpoint fallbacks",
Network::MAINNET
);
client.start().await?;
// Wait for syncing
std::thread::sleep(Duration::from_secs(5));
// Get inputs for fee_history calls
let head_block_num = client.get_block_number().await?;
log::info!("head_block_num: {}", &head_block_num);
let block = BlockTag::Latest;
let block_number = BlockTag::Number(head_block_num);
log::info!("block {:?} and block_number {:?}", block, block_number);
let reward_percentiles: Vec<f64> = vec![];
// Get fee history for 1 block back from latest
let fee_history = client
.get_fee_history(1, head_block_num, &reward_percentiles)
.await?
.unwrap();
assert_eq!(fee_history.base_fee_per_gas.len(), 2);
assert_eq!(fee_history.oldest_block.as_u64(), head_block_num - 1);
// Fetch 10000 delta, helios will return as many as it can
let fee_history = match client
.get_fee_history(10_000, head_block_num, &reward_percentiles)
.await?
{
Some(fee_history) => fee_history,
None => panic!(
"empty gas fee returned with inputs: Block count: {:?}, Head Block #: {:?}, Reward Percentiles: {:?}",
10_000, head_block_num, &reward_percentiles
),
};
assert!(
!fee_history.base_fee_per_gas.is_empty(),
"fee_history.base_fee_per_gas.len() {:?}",
fee_history.base_fee_per_gas.len()
);
// Fetch 10000 blocks in the past
// Helios will error since it won't have those historical blocks
let fee_history = client
.get_fee_history(1, head_block_num - 10_000, &reward_percentiles)
.await;
assert!(fee_history.is_err(), "fee_history() {fee_history:?}");
// Fetch 20 block away
// Should return array of size 21: our 20 block of interest + the next one
// The oldest block should be 19 block away, including it
let fee_history = client
.get_fee_history(20, head_block_num, &reward_percentiles)
.await?
.unwrap();
assert_eq!(
fee_history.base_fee_per_gas.len(),
21,
"fee_history.base_fee_per_gas.len() {:?} vs 21",
fee_history.base_fee_per_gas.len()
);
assert_eq!(
fee_history.oldest_block.as_u64(),
head_block_num - 20,
"fee_history.oldest_block.as_u64() {:?} vs head_block_num {:?} - 19",
fee_history.oldest_block.as_u64(),
head_block_num
);
// Fetch whatever blocks ahead, but that will fetch one block behind.
// This should return an answer of size two as Helios will cap this request to the newest block it knows
// we refresh parameters to make sure head_block_num is in line with newest block of our payload
let head_block_num = client.get_block_number().await?;
let fee_history = client
.get_fee_history(1, head_block_num + 1000, &reward_percentiles)
.await?
.unwrap();
assert_eq!(
fee_history.base_fee_per_gas.len(),
2,
"fee_history.base_fee_per_gas.len() {:?} vs 2",
fee_history.base_fee_per_gas.len()
);
assert_eq!(
fee_history.oldest_block.as_u64(),
head_block_num - 1,
"fee_history.oldest_block.as_u64() {:?} vs head_block_num {:?}",
fee_history.oldest_block.as_u64(),
head_block_num
);
Ok(())
}