Compare commits
25 Commits
nightly-10
...
master
Author | SHA1 | Date |
---|---|---|
Noah Citron | 2e6b948c8f | |
Noah Citron | f5760f7a8a | |
Noah Citron | ba3f31111f | |
refcell.eth | 3afa312776 | |
Noah Citron | 5a32f30686 | |
Noah Citron | 3c471c2bef | |
refcell.eth | a73f9c648b | |
Noah Citron | 0e20b5f783 | |
Noah Citron | 8da632f8f2 | |
Noah Citron | ef5a6a216f | |
Giovanni Vignone | 32d09736e0 | |
Noah Citron | da520290ce | |
Noah Citron | 8e006d623b | |
Noah Citron | 4066828387 | |
christn | 1b1a540340 | |
danilowhk | a0032835f3 | |
Noah Citron | 4a84ecabed | |
Noah Citron | 1fa2dede25 | |
Noah Citron | 7b7dc708f8 | |
Noah Citron | 6b662f903b | |
Noah Citron | 5b9e90436a | |
Noah Citron | 72267b4563 | |
James Prestwich | 604b325983 | |
Noah Citron | 2c5c318529 | |
Noah Citron | de90eb9158 |
|
@ -1,8 +1,9 @@
|
|||
name: benchmarks
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "master" ]
|
||||
workflow_bench:
|
||||
# push:
|
||||
# branches: [ "master" ]
|
||||
|
||||
env:
|
||||
MAINNET_RPC_URL: ${{ secrets.MAINNET_RPC_URL }}
|
||||
|
|
|
@ -6,6 +6,10 @@ on:
|
|||
pull_request:
|
||||
branches: [ "master" ]
|
||||
|
||||
env:
|
||||
MAINNET_RPC_URL: ${{ secrets.MAINNET_RPC_URL }}
|
||||
GOERLI_RPC_URL: ${{ secrets.GOERLI_RPC_URL }}
|
||||
|
||||
jobs:
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
/target
|
||||
|
||||
.DS_Store
|
||||
target
|
||||
*.env
|
||||
|
||||
helios-ts/node_modules
|
||||
helios-ts/dist
|
||||
helios-ts/helios-*.tgz
|
||||
|
|
File diff suppressed because it is too large
Load Diff
27
Cargo.toml
27
Cargo.toml
|
@ -1,8 +1,11 @@
|
|||
[package]
|
||||
name = "helios"
|
||||
version = "0.1.3"
|
||||
version = "0.3.0"
|
||||
edition = "2021"
|
||||
autobenches = false
|
||||
exclude = [
|
||||
"benches"
|
||||
]
|
||||
|
||||
[workspace]
|
||||
members = [
|
||||
|
@ -12,7 +15,12 @@ members = [
|
|||
"config",
|
||||
"consensus",
|
||||
"execution",
|
||||
"helios-ts",
|
||||
]
|
||||
default-members = ["cli"]
|
||||
|
||||
[profile.bench]
|
||||
debug = true
|
||||
|
||||
[dependencies]
|
||||
client = { path = "./client" }
|
||||
|
@ -20,18 +28,19 @@ config = { path = "./config" }
|
|||
common = { path = "./common" }
|
||||
consensus = { path = "./consensus" }
|
||||
execution = { path = "./execution" }
|
||||
serde = { version = "1.0.154", features = ["derive"] }
|
||||
|
||||
[dev-dependencies]
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
eyre = "0.6.8"
|
||||
home = "0.5.4"
|
||||
ethers = "1.0.2"
|
||||
dirs = "4.0.0"
|
||||
ethers = { version = "1.0.2", features = [ "abigen" ] }
|
||||
env_logger = "0.9.0"
|
||||
log = "0.4.17"
|
||||
tracing-test = "0.2.3"
|
||||
tracing-test = "0.2.4"
|
||||
criterion = { version = "0.4", features = [ "async_tokio", "plotters" ]}
|
||||
plotters = "0.3.3"
|
||||
tempfile = "3.3.0"
|
||||
plotters = "0.3.4"
|
||||
tempfile = "3.4.0"
|
||||
hex = "0.4.3"
|
||||
|
||||
[profile.release]
|
||||
|
@ -61,6 +70,10 @@ path = "examples/client.rs"
|
|||
name = "config"
|
||||
path = "examples/config.rs"
|
||||
|
||||
[[example]]
|
||||
name = "call"
|
||||
path = "examples/call.rs"
|
||||
|
||||
######################################
|
||||
# Benchmarks
|
||||
######################################
|
||||
|
|
|
@ -191,6 +191,9 @@ Benchmarks are defined in the [benches](./benches/) subdirectory. They are built
|
|||
|
||||
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.
|
||||
|
|
|
@ -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
|
||||
```
|
|
@ -1,4 +1,5 @@
|
|||
use client::database::Database;
|
||||
use config::Config;
|
||||
use criterion::{criterion_group, criterion_main, Criterion};
|
||||
use helios::prelude::FileDB;
|
||||
use tempfile::tempdir;
|
||||
|
@ -17,8 +18,12 @@ pub fn save_checkpoint(c: &mut Criterion) {
|
|||
c.bench_function("save_checkpoint", |b| {
|
||||
let checkpoint = vec![1, 2, 3];
|
||||
b.iter(|| {
|
||||
let temp_dir = tempdir().unwrap().into_path();
|
||||
let db = FileDB::new(temp_dir);
|
||||
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();
|
||||
})
|
||||
});
|
||||
|
@ -28,14 +33,17 @@ pub fn save_checkpoint(c: &mut Criterion) {
|
|||
pub fn load_checkpoint(c: &mut Criterion) {
|
||||
c.bench_function("load_checkpoint", |b| {
|
||||
// First write to the db
|
||||
let temp_dir = tempdir().unwrap().into_path();
|
||||
let db = FileDB::new(temp_dir.clone());
|
||||
let written_checkpoint = vec![1, 2, 3];
|
||||
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 db = FileDB::new(temp_dir.clone());
|
||||
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 |
|
@ -48,7 +48,14 @@ pub fn bench_goerli_get_balance(c: &mut Criterion) {
|
|||
.unwrap();
|
||||
|
||||
// Construct a goerli client using our harness and tokio runtime.
|
||||
let client = std::sync::Arc::new(harness::construct_goerli_client(&rt).unwrap());
|
||||
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();
|
||||
|
|
|
@ -2,12 +2,12 @@
|
|||
|
||||
use std::{str::FromStr, sync::Arc};
|
||||
|
||||
use ::client::Client;
|
||||
use ::client::{database::ConfigDB, Client};
|
||||
use ethers::{
|
||||
abi::Address,
|
||||
types::{H256, U256},
|
||||
};
|
||||
use helios::{client, config::networks, prelude::FileDB, types::BlockTag};
|
||||
use helios::{client, config::networks, types::BlockTag};
|
||||
|
||||
/// Fetches the latest mainnet checkpoint from the fallback service.
|
||||
///
|
||||
|
@ -29,11 +29,11 @@ pub async fn fetch_mainnet_checkpoint() -> eyre::Result<H256> {
|
|||
/// 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<client::FileDB>> {
|
||||
) -> eyre::Result<client::Client<ConfigDB>> {
|
||||
rt.block_on(inner_construct_mainnet_client())
|
||||
}
|
||||
|
||||
pub async fn inner_construct_mainnet_client() -> eyre::Result<client::Client<client::FileDB>> {
|
||||
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)
|
||||
|
@ -47,7 +47,7 @@ pub async fn inner_construct_mainnet_client() -> eyre::Result<client::Client<cli
|
|||
|
||||
pub async fn construct_mainnet_client_with_checkpoint(
|
||||
checkpoint: &str,
|
||||
) -> eyre::Result<client::Client<client::FileDB>> {
|
||||
) -> 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)
|
||||
|
@ -79,7 +79,7 @@ pub fn construct_runtime() -> tokio::runtime::Runtime {
|
|||
/// 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<client::FileDB>> {
|
||||
) -> 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()
|
||||
|
@ -96,7 +96,7 @@ pub fn construct_goerli_client(
|
|||
/// Gets the balance of the given address on mainnet.
|
||||
pub fn get_balance(
|
||||
rt: &tokio::runtime::Runtime,
|
||||
client: Arc<Client<FileDB>>,
|
||||
client: Arc<Client<ConfigDB>>,
|
||||
address: &str,
|
||||
) -> eyre::Result<U256> {
|
||||
rt.block_on(async {
|
||||
|
|
|
@ -2,7 +2,7 @@ cargo-features = ["different-binary-name"]
|
|||
|
||||
[package]
|
||||
name = "cli"
|
||||
version = "0.1.3"
|
||||
version = "0.3.0"
|
||||
edition = "2021"
|
||||
|
||||
[[bin]]
|
||||
|
|
|
@ -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,
|
||||
|
@ -95,10 +105,10 @@ struct Cli {
|
|||
|
||||
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,
|
||||
|
@ -112,21 +122,6 @@ impl Cli {
|
|||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
fn get_data_dir(&self) -> PathBuf {
|
||||
if let Some(dir) = &self.data_dir {
|
||||
PathBuf::from_str(dir).expect("cannot find data dir")
|
||||
|
|
|
@ -1,16 +1,14 @@
|
|||
[package]
|
||||
name = "client"
|
||||
version = "0.1.3"
|
||||
version = "0.3.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
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" }
|
||||
ethers = "1.0.2"
|
||||
jsonrpsee = { version = "0.15.1", features = ["full"] }
|
||||
ssz-rs = { git = "https://github.com/ralexstokes/ssz-rs", rev = "d09f55b4f8554491e3431e01af1c32347a8781cd" }
|
||||
ethers = "1.0.0"
|
||||
futures = "0.3.23"
|
||||
log = "0.4.17"
|
||||
thiserror = "1.0.37"
|
||||
|
@ -19,3 +17,13 @@ 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"] }
|
||||
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use config::networks::Network;
|
||||
use consensus::errors::ConsensusError;
|
||||
use ethers::prelude::{Address, U256};
|
||||
use ethers::types::{Filter, Log, Transaction, TransactionReceipt, H256};
|
||||
use ethers::types::{
|
||||
FeeHistory, Filter, Log, SyncingStatus, Transaction, TransactionReceipt, H256,
|
||||
};
|
||||
use eyre::{eyre, Result};
|
||||
|
||||
use common::types::BlockTag;
|
||||
|
@ -12,13 +13,25 @@ use config::{CheckpointFallback, Config};
|
|||
use consensus::{types::Header, ConsensusClient};
|
||||
use execution::types::{CallOpts, ExecutionBlock};
|
||||
use log::{error, info, warn};
|
||||
use tokio::spawn;
|
||||
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;
|
||||
|
||||
#[derive(Default)]
|
||||
|
@ -27,7 +40,9 @@ pub struct ClientBuilder {
|
|||
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>,
|
||||
|
@ -62,11 +77,13 @@ impl ClientBuilder {
|
|||
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
|
||||
|
@ -92,7 +109,7 @@ impl ClientBuilder {
|
|||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> Result<Client<FileDB>> {
|
||||
pub fn build<DB: Database>(self) -> Result<Client<DB>> {
|
||||
let base_config = if let Some(network) = self.network {
|
||||
network.to_base_config()
|
||||
} else {
|
||||
|
@ -120,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 {
|
||||
|
@ -135,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 {
|
||||
|
@ -167,8 +192,15 @@ impl ClientBuilder {
|
|||
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,
|
||||
|
@ -183,35 +215,40 @@ impl ClientBuilder {
|
|||
|
||||
pub struct Client<DB: Database> {
|
||||
node: Arc<RwLock<Node>>,
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
rpc: Option<Rpc>,
|
||||
db: Option<DB>,
|
||||
db: DB,
|
||||
fallback: Option<String>,
|
||||
load_external_fallback: bool,
|
||||
}
|
||||
|
||||
impl Client<FileDB> {
|
||||
fn new(config: Config) -> Result<Self> {
|
||||
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));
|
||||
|
||||
let data_dir = config.data_dir.clone();
|
||||
let db = data_dir.map(FileDB::new);
|
||||
|
||||
Ok(Client {
|
||||
node,
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
rpc,
|
||||
db,
|
||||
fallback: config.fallback.clone(),
|
||||
load_external_fallback: config.load_external_fallback,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<DB: Database> Client<DB> {
|
||||
pub async fn start(&mut self) -> Result<()> {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
if let Some(rpc) = &mut self.rpc {
|
||||
rpc.start().await?;
|
||||
}
|
||||
|
@ -220,11 +257,19 @@ impl<DB: Database> Client<DB> {
|
|||
|
||||
if let Err(err) = sync_res {
|
||||
match err {
|
||||
NodeError::ConsensusSyncError(err) => match err.downcast_ref().unwrap() {
|
||||
ConsensusError::CheckpointTooOld => {
|
||||
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),
|
||||
hex::encode(
|
||||
self.node
|
||||
.read()
|
||||
.await
|
||||
.config
|
||||
.checkpoint
|
||||
.clone()
|
||||
.unwrap_or_default()
|
||||
),
|
||||
);
|
||||
|
||||
let fallback = self.boot_from_fallback().await;
|
||||
|
@ -241,6 +286,13 @@ impl<DB: Database> Client<DB> {
|
|||
}
|
||||
}
|
||||
|
||||
self.start_advance_thread();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn start_advance_thread(&self) {
|
||||
let node = self.node.clone();
|
||||
spawn(async move {
|
||||
loop {
|
||||
|
@ -250,11 +302,25 @@ impl<DB: Database> Client<DB> {
|
|||
}
|
||||
|
||||
let next_update = node.read().await.duration_until_next_update();
|
||||
|
||||
sleep(next_update).await;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
#[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<()> {
|
||||
|
@ -335,8 +401,8 @@ 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");
|
||||
}
|
||||
}
|
||||
|
@ -437,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,
|
||||
|
@ -477,6 +556,10 @@ impl<DB: Database> Client<DB> {
|
|||
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()
|
||||
}
|
||||
|
|
|
@ -1,27 +1,40 @@
|
|||
#[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)?;
|
||||
|
||||
|
@ -37,13 +50,40 @@ impl Database for FileDB {
|
|||
}
|
||||
|
||||
fn load_checkpoint(&self) -> Result<Vec<u8>> {
|
||||
let mut f = fs::OpenOptions::new()
|
||||
.read(true)
|
||||
.open(self.data_dir.join("checkpoint"))?;
|
||||
|
||||
let mut buf = Vec::new();
|
||||
f.read_to_end(&mut buf)?;
|
||||
|
||||
Ok(buf)
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,10 @@ use thiserror::Error;
|
|||
#[derive(Debug, Error)]
|
||||
pub enum NodeError {
|
||||
#[error(transparent)]
|
||||
ExecutionError(#[from] EvmError),
|
||||
ExecutionEvmError(#[from] EvmError),
|
||||
|
||||
#[error("execution error: {0}")]
|
||||
ExecutionError(Report),
|
||||
|
||||
#[error("out of sync: {0} slots behind")]
|
||||
OutOfSync(u64),
|
||||
|
@ -34,10 +37,11 @@ pub enum NodeError {
|
|||
BlockNotFoundError(#[from] BlockNotFoundError),
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
impl NodeError {
|
||||
pub fn to_json_rpsee_error(self) -> jsonrpsee::core::Error {
|
||||
match self {
|
||||
NodeError::ExecutionError(evm_err) => match evm_err {
|
||||
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) {
|
||||
|
|
|
@ -3,6 +3,8 @@ pub use crate::client::*;
|
|||
|
||||
pub mod database;
|
||||
pub mod errors;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub mod rpc;
|
||||
|
||||
mod node;
|
||||
pub mod node;
|
||||
|
|
|
@ -3,12 +3,15 @@ use std::sync::Arc;
|
|||
use std::time::Duration;
|
||||
|
||||
use ethers::prelude::{Address, U256};
|
||||
use ethers::types::{Filter, Log, 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;
|
||||
|
@ -25,13 +28,14 @@ pub struct Node {
|
|||
pub config: Arc<Config>,
|
||||
payloads: BTreeMap<u64, ExecutionPayload>,
|
||||
finalized_payloads: BTreeMap<u64, ExecutionPayload>,
|
||||
current_slot: Option<u64>,
|
||||
pub history_size: usize,
|
||||
}
|
||||
|
||||
impl Node {
|
||||
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())
|
||||
|
@ -49,15 +53,28 @@ impl Node {
|
|||
config,
|
||||
payloads,
|
||||
finalized_payloads,
|
||||
current_slot: None,
|
||||
history_size: 64,
|
||||
})
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -92,11 +109,25 @@ impl Node {
|
|||
.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();
|
||||
|
@ -121,7 +152,7 @@ impl Node {
|
|||
&self.payloads,
|
||||
self.chain_id(),
|
||||
);
|
||||
evm.call(opts).await.map_err(NodeError::ExecutionError)
|
||||
evm.call(opts).await.map_err(NodeError::ExecutionEvmError)
|
||||
}
|
||||
|
||||
pub async fn estimate_gas(&self, opts: &CallOpts) -> Result<u64, NodeError> {
|
||||
|
@ -136,7 +167,7 @@ impl Node {
|
|||
);
|
||||
evm.estimate_gas(opts)
|
||||
.await
|
||||
.map_err(NodeError::ExecutionError)
|
||||
.map_err(NodeError::ExecutionEvmError)
|
||||
}
|
||||
|
||||
pub async fn get_balance(&self, address: &Address, block: BlockTag) -> Result<U256> {
|
||||
|
@ -157,14 +188,14 @@ impl Node {
|
|||
|
||||
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();
|
||||
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();
|
||||
let transaction_count = payload.transactions().len();
|
||||
|
||||
Ok(transaction_count as u64)
|
||||
}
|
||||
|
@ -238,7 +269,7 @@ impl Node {
|
|||
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)
|
||||
}
|
||||
|
@ -253,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(
|
||||
|
@ -269,6 +300,17 @@ impl Node {
|
|||
}
|
||||
}
|
||||
|
||||
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>,
|
||||
|
@ -286,6 +328,37 @@ 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())
|
||||
|
@ -294,7 +367,7 @@ impl Node {
|
|||
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);
|
||||
let coinbase_address = Address::from_slice(payload.fee_recipient());
|
||||
Ok(coinbase_address)
|
||||
}
|
||||
|
||||
|
@ -325,7 +398,7 @@ impl Node {
|
|||
let payloads = self
|
||||
.payloads
|
||||
.iter()
|
||||
.filter(|entry| &entry.1.block_hash.to_vec() == hash)
|
||||
.filter(|entry| &entry.1.block_hash().to_vec() == hash)
|
||||
.collect::<Vec<(&u64, &ExecutionPayload)>>();
|
||||
|
||||
payloads
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use ethers::{
|
||||
abi::AbiEncode,
|
||||
types::{Address, Filter, Log, Transaction, TransactionReceipt, H256, U256},
|
||||
types::{Address, Filter, Log, SyncingStatus, Transaction, TransactionReceipt, H256, U256},
|
||||
};
|
||||
use eyre::Result;
|
||||
use log::info;
|
||||
|
@ -114,6 +114,8 @@ trait EthRpc {
|
|||
) -> 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")]
|
||||
|
@ -278,6 +280,11 @@ impl EthRpcServer for RpcInner {
|
|||
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)
|
||||
|
@ -335,5 +342,11 @@ fn format_hex(num: &U256) -> String {
|
|||
.trim_start_matches('0')
|
||||
.to_string();
|
||||
|
||||
let stripped = if stripped.is_empty() {
|
||||
"0".to_string()
|
||||
} else {
|
||||
stripped
|
||||
};
|
||||
|
||||
format!("0x{stripped}")
|
||||
}
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
[package]
|
||||
name = "common"
|
||||
version = "0.1.3"
|
||||
version = "0.3.0"
|
||||
edition = "2021"
|
||||
|
||||
[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" }
|
||||
ethers = "1.0.2"
|
||||
ssz-rs = { git = "https://github.com/ralexstokes/ssz-rs", rev = "d09f55b4f8554491e3431e01af1c32347a8781cd" }
|
||||
ethers = "1.0.0"
|
||||
thiserror = "1.0.37"
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -1,24 +1,24 @@
|
|||
|
||||
[package]
|
||||
name = "config"
|
||||
version = "0.1.3"
|
||||
version = "0.3.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
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" }
|
||||
ethers = "1.0.2"
|
||||
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"
|
||||
|
||||
common = { path = "../common" }
|
||||
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"] }
|
||||
|
|
|
@ -12,7 +12,7 @@ pub struct BaseConfig {
|
|||
deserialize_with = "bytes_deserialize",
|
||||
serialize_with = "bytes_serialize"
|
||||
)]
|
||||
pub checkpoint: Vec<u8>,
|
||||
pub default_checkpoint: Vec<u8>,
|
||||
pub chain: ChainConfig,
|
||||
pub forks: Forks,
|
||||
pub max_checkpoint_age: u64,
|
||||
|
|
|
@ -136,28 +136,36 @@ impl CheckpointFallback {
|
|||
// Iterate over all mainnet checkpoint sync services and get the latest checkpoint slot for each.
|
||||
let tasks: Vec<_> = services
|
||||
.iter()
|
||||
.map(|service| {
|
||||
.map(|service| async move {
|
||||
let service = service.clone();
|
||||
tokio::spawn(async move {
|
||||
match Self::query_service(&service.endpoint).await {
|
||||
Some(raw) => {
|
||||
if raw.data.slots.is_empty() {
|
||||
return Err(eyre::eyre!("no slots"));
|
||||
}
|
||||
Ok(raw.data.slots[0].clone())
|
||||
match Self::query_service(&service.endpoint).await {
|
||||
Some(raw) => {
|
||||
if raw.data.slots.is_empty() {
|
||||
return Err(eyre::eyre!("no slots"));
|
||||
}
|
||||
None => Err(eyre::eyre!("failed to query service")),
|
||||
|
||||
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(Ok(s)) => Some(s.clone()),
|
||||
Ok(s) => Some(s.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.filter(|s| s.block_root.is_some())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Get the max epoch
|
||||
|
|
|
@ -2,25 +2,25 @@ use figment::{
|
|||
providers::{Format, Serialized, Toml},
|
||||
Figment,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
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_serialize};
|
||||
use crate::utils::{bytes_deserialize, bytes_opt_deserialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Default)]
|
||||
#[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",
|
||||
serialize_with = "bytes_serialize"
|
||||
)]
|
||||
pub checkpoint: Vec<u8>,
|
||||
#[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,
|
||||
|
@ -73,7 +73,9 @@ impl Config {
|
|||
pub fn fork_version(&self, slot: u64) -> Vec<u8> {
|
||||
let epoch = slot / 32;
|
||||
|
||||
if epoch >= self.forks.bellatrix.epoch {
|
||||
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()
|
||||
|
@ -86,7 +88,7 @@ impl Config {
|
|||
BaseConfig {
|
||||
rpc_port: self.rpc_port.unwrap_or(8545),
|
||||
consensus_rpc: Some(self.consensus_rpc.clone()),
|
||||
checkpoint: self.checkpoint.clone(),
|
||||
default_checkpoint: self.default_checkpoint.clone(),
|
||||
chain: self.chain.clone(),
|
||||
forks: self.forks.clone(),
|
||||
max_checkpoint_age: self.max_checkpoint_age,
|
||||
|
|
|
@ -35,8 +35,8 @@ impl Network {
|
|||
|
||||
pub fn mainnet() -> BaseConfig {
|
||||
BaseConfig {
|
||||
checkpoint: hex_str_to_bytes(
|
||||
"0x428ce0b5f5bbed1fc2b3feb5d4152ae0fe98a80b1bfa8de36681868e81e9222a",
|
||||
default_checkpoint: hex_str_to_bytes(
|
||||
"0x766647f3c4e1fc91c0db9a9374032ae038778411fbff222974e11f2e3ce7dadf",
|
||||
)
|
||||
.unwrap(),
|
||||
rpc_port: 8545,
|
||||
|
@ -62,6 +62,10 @@ 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
|
||||
}
|
||||
|
@ -69,7 +73,7 @@ pub fn mainnet() -> BaseConfig {
|
|||
|
||||
pub fn goerli() -> BaseConfig {
|
||||
BaseConfig {
|
||||
checkpoint: hex_str_to_bytes(
|
||||
default_checkpoint: hex_str_to_bytes(
|
||||
"0xd4344682866dbede543395ecf5adf9443a27f423a4b00f270458e7932686ced1",
|
||||
)
|
||||
.unwrap(),
|
||||
|
@ -96,6 +100,10 @@ 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
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ pub struct Forks {
|
|||
pub genesis: Fork,
|
||||
pub altair: Fork,
|
||||
pub bellatrix: Fork,
|
||||
pub capella: Fork,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
|
||||
|
|
|
@ -15,3 +15,15 @@ where
|
|||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,27 +1,33 @@
|
|||
[package]
|
||||
name = "consensus"
|
||||
version = "0.1.3"
|
||||
version = "0.3.0"
|
||||
edition = "2021"
|
||||
|
||||
[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"
|
||||
ethers = "1.0.2"
|
||||
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"
|
||||
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
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::*;
|
||||
|
@ -21,9 +21,20 @@ 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,
|
||||
|
@ -59,6 +70,16 @@ impl<R: ConsensusRpc> ConsensusClient<R> {
|
|||
})
|
||||
}
|
||||
|
||||
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?;
|
||||
|
@ -82,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
|
||||
}
|
||||
|
@ -470,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);
|
||||
|
||||
|
@ -499,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();
|
||||
|
@ -530,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);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -24,4 +24,6 @@ pub enum ConsensusError {
|
|||
PayloadNotFound(u64),
|
||||
#[error("checkpoint is too old")]
|
||||
CheckpointTooOld,
|
||||
#[error("consensus rpc is for the incorrect network")]
|
||||
IncorrectRpcNetwork,
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,8 @@ 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: &'_ [u8]) -> Result<Bootstrap>;
|
||||
|
@ -15,4 +16,5 @@ pub trait ConsensusRpc {
|
|||
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>;
|
||||
}
|
||||
|
|
|
@ -1,33 +1,23 @@
|
|||
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,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -38,8 +28,8 @@ impl ConsensusRpc for NimbusRpc {
|
|||
self.rpc, root_hex
|
||||
);
|
||||
|
||||
let res = self
|
||||
.client
|
||||
let client = reqwest::Client::new();
|
||||
let res = client
|
||||
.get(req)
|
||||
.send()
|
||||
.await
|
||||
|
@ -58,8 +48,8 @@ impl ConsensusRpc for NimbusRpc {
|
|||
self.rpc, period, count
|
||||
);
|
||||
|
||||
let res = self
|
||||
.client
|
||||
let client = reqwest::Client::new();
|
||||
let res = client
|
||||
.get(req)
|
||||
.send()
|
||||
.await
|
||||
|
@ -73,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>()
|
||||
|
@ -88,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>()
|
||||
|
@ -103,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>()
|
||||
|
@ -115,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)]
|
||||
|
@ -148,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,
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
@ -376,7 +512,7 @@ where
|
|||
.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>,
|
||||
{
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -38,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);
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
use std::str::FromStr;
|
||||
use std::{path::PathBuf, str::FromStr};
|
||||
|
||||
use env_logger::Env;
|
||||
use ethers::{types::Address, utils};
|
||||
use eyre::Result;
|
||||
use helios::{client::ClientBuilder, config::networks::Network, types::BlockTag};
|
||||
use helios::{config::networks::Network, prelude::*};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
|
@ -15,12 +15,14 @@ async fn main() -> Result<()> {
|
|||
let consensus_rpc = "https://www.lightclientdata.org";
|
||||
log::info!("Using consensus RPC URL: {}", consensus_rpc);
|
||||
|
||||
let mut client = ClientBuilder::new()
|
||||
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
|
||||
|
|
|
@ -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(ð_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(())
|
||||
}
|
|
@ -35,7 +35,7 @@ async fn main() -> Result<()> {
|
|||
builder = builder.load_external_fallback();
|
||||
|
||||
// Build the client
|
||||
let _client = builder.build().unwrap();
|
||||
let _client: Client<FileDB> = builder.build().unwrap();
|
||||
println!("Constructed client!");
|
||||
|
||||
Ok(())
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use config::CliConfig;
|
||||
use dirs::home_dir;
|
||||
use eyre::Result;
|
||||
|
||||
use helios::prelude::*;
|
||||
|
@ -6,7 +7,7 @@ use helios::prelude::*;
|
|||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
// Load the config from the global config file
|
||||
let config_path = home::home_dir().unwrap().join(".helios/helios.toml");
|
||||
let config_path = home_dir().unwrap().join(".helios/helios.toml");
|
||||
let config = Config::from_file(&config_path, "mainnet", &CliConfig::default());
|
||||
println!("Constructed config: {config:#?}");
|
||||
|
||||
|
|
|
@ -1,18 +1,17 @@
|
|||
[package]
|
||||
name = "execution"
|
||||
version = "0.1.3"
|
||||
version = "0.3.0"
|
||||
edition = "2021"
|
||||
|
||||
[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" }
|
||||
ethers = "1.0.2"
|
||||
revm = "2.1.0"
|
||||
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"
|
||||
bytes = "1.2.1"
|
||||
futures = "0.3.23"
|
||||
toml = "0.5.9"
|
||||
|
@ -20,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"] }
|
||||
|
|
|
@ -24,6 +24,18 @@ pub enum ExecutionError {
|
|||
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
|
||||
|
|
|
@ -6,17 +6,19 @@ 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;
|
||||
|
||||
|
@ -109,7 +111,7 @@ impl<'a, R: ExecutionRpc> Evm<'a, R> {
|
|||
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,
|
||||
|
@ -132,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(),
|
||||
};
|
||||
|
||||
|
@ -173,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();
|
||||
|
||||
|
@ -225,8 +227,7 @@ impl<'a, R: ExecutionRpc> ProofDB<'a, R> {
|
|||
|
||||
let handle = thread::spawn(move || {
|
||||
let account_fut = execution.get_account(&address, Some(&slots), &payload);
|
||||
let runtime = Runtime::new()?;
|
||||
runtime.block_on(account_fut)
|
||||
block_on(account_fut)
|
||||
});
|
||||
|
||||
handle.join().unwrap()
|
||||
|
@ -265,15 +266,11 @@ 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);
|
||||
|
||||
|
@ -284,13 +281,13 @@ impl<'a, R: ExecutionRpc> Database for ProofDB<'a, R> {
|
|||
.get_account(address, &[slot])?
|
||||
.slots
|
||||
.get(&slot)
|
||||
.unwrap(),
|
||||
.ok_or(SlotNotFoundError::new(slot))?,
|
||||
},
|
||||
None => *self
|
||||
.get_account(address, &[slot])?
|
||||
.slots
|
||||
.get(&slot)
|
||||
.unwrap(),
|
||||
.ok_or(SlotNotFoundError::new(slot))?,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -303,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()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ use std::str::FromStr;
|
|||
|
||||
use ethers::abi::AbiEncode;
|
||||
use ethers::prelude::{Address, U256};
|
||||
use ethers::types::{Filter, Log, Transaction, TransactionReceipt, H256};
|
||||
use ethers::types::{FeeHistory, Filter, Log, Transaction, TransactionReceipt, H256};
|
||||
use ethers::utils::keccak256;
|
||||
use ethers::utils::rlp::{encode, Encodable, RlpStream};
|
||||
use eyre::Result;
|
||||
|
@ -32,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,
|
||||
|
@ -46,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();
|
||||
|
@ -54,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,
|
||||
);
|
||||
|
@ -90,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 {
|
||||
|
@ -128,7 +136,7 @@ impl<R: ExecutionRpc> ExecutionClient<R> {
|
|||
let empty_uncle_hash = "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347";
|
||||
|
||||
let tx_hashes = payload
|
||||
.transactions
|
||||
.transactions()
|
||||
.iter()
|
||||
.map(|tx| H256::from_slice(&keccak256(tx)))
|
||||
.collect::<Vec<H256>>();
|
||||
|
@ -136,7 +144,7 @@ impl<R: ExecutionRpc> ExecutionClient<R> {
|
|||
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?
|
||||
|
@ -155,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,
|
||||
|
@ -184,10 +192,10 @@ impl<R: ExecutionRpc> ExecutionClient<R> {
|
|||
payload: &ExecutionPayload,
|
||||
index: usize,
|
||||
) -> Result<Option<Transaction>> {
|
||||
let tx = payload.transactions[index].clone();
|
||||
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());
|
||||
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"))?;
|
||||
|
||||
|
@ -214,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>>();
|
||||
|
@ -231,7 +239,7 @@ impl<R: ExecutionRpc> ExecutionClient<R> {
|
|||
|
||||
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());
|
||||
|
@ -267,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<_>>();
|
||||
|
@ -283,7 +291,22 @@ impl<R: ExecutionRpc> ExecutionClient<R> {
|
|||
filter: &Filter,
|
||||
payloads: &BTreeMap<u64, ExecutionPayload>,
|
||||
) -> Result<Vec<Log>> {
|
||||
let logs = self.rpc.get_logs(filter).await?;
|
||||
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(),
|
||||
|
@ -322,6 +345,120 @@ impl<R: ExecutionRpc> ExecutionClient<R> {
|
|||
}
|
||||
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> {
|
||||
|
|
|
@ -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, Filter, Log, 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)));
|
||||
|
@ -128,4 +131,27 @@ impl ExecutionRpc for HttpRpc {
|
|||
.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))?)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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, Filter, Log, 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);
|
||||
|
@ -61,4 +62,18 @@ impl ExecutionRpc for MockRpc {
|
|||
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)?)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use async_trait::async_trait;
|
||||
use ethers::types::{
|
||||
transaction::eip2930::AccessList, Address, EIP1186ProofResponse, Filter, Log, 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
|
||||
|
@ -29,4 +30,11 @@ pub trait ExecutionRpc: Send + Clone + Sync + 'static {
|
|||
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>;
|
||||
}
|
||||
|
|
|
@ -64,7 +64,7 @@ pub enum Transactions {
|
|||
#[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>,
|
||||
|
@ -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),
|
||||
|
|
|
@ -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
|
||||
]
|
||||
}
|
||||
}
|
|
@ -5,7 +5,7 @@ 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,13 +18,13 @@ async fn test_get_account() {
|
|||
let execution = get_client();
|
||||
let address = Address::from_str("14f9D4aF749609c1438528C0Cce1cC3f6D411c47").unwrap();
|
||||
|
||||
let payload = ExecutionPayload {
|
||||
let payload = ExecutionPayload::Bellatrix(ExecutionPayloadBellatrix {
|
||||
state_root: Vector::from_iter(
|
||||
hex_str_to_bytes("0xaa02f5db2ee75e3da400d10f3c30e894b6016ce8a2501680380a907b6674ce0d")
|
||||
.unwrap(),
|
||||
),
|
||||
..ExecutionPayload::default()
|
||||
};
|
||||
..ExecutionPayloadBellatrix::default()
|
||||
});
|
||||
|
||||
let account = execution
|
||||
.get_account(&address, None, &payload)
|
||||
|
@ -55,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);
|
||||
|
@ -104,15 +104,15 @@ async fn test_get_tx_not_included() {
|
|||
#[tokio::test]
|
||||
async fn test_get_logs() {
|
||||
let execution = get_client();
|
||||
let mut payload = ExecutionPayload {
|
||||
let mut payload = ExecutionPayload::Bellatrix(ExecutionPayloadBellatrix {
|
||||
receipts_root: Vector::from_iter(
|
||||
hex_str_to_bytes("dd82a78eccb333854f0c99e5632906e092d8a49c27a21c25cae12b82ec2a113f")
|
||||
.unwrap(),
|
||||
),
|
||||
..ExecutionPayload::default()
|
||||
};
|
||||
..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);
|
||||
|
@ -134,15 +134,15 @@ async fn test_get_receipt() {
|
|||
let tx_hash =
|
||||
H256::from_str("2dac1b27ab58b493f902dda8b63979a112398d747f1761c0891777c0983e591f").unwrap();
|
||||
|
||||
let mut payload = ExecutionPayload {
|
||||
let mut payload = ExecutionPayload::Bellatrix(ExecutionPayloadBellatrix {
|
||||
receipts_root: Vector::from_iter(
|
||||
hex_str_to_bytes("dd82a78eccb333854f0c99e5632906e092d8a49c27a21c25cae12b82ec2a113f")
|
||||
.unwrap(),
|
||||
),
|
||||
..ExecutionPayload::default()
|
||||
};
|
||||
..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);
|
||||
|
@ -163,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);
|
||||
|
@ -191,10 +191,10 @@ async fn test_get_receipt_not_included() {
|
|||
#[tokio::test]
|
||||
async fn test_get_block() {
|
||||
let execution = get_client();
|
||||
let payload = ExecutionPayload {
|
||||
let payload = ExecutionPayload::Bellatrix(ExecutionPayloadBellatrix {
|
||||
block_number: 12345,
|
||||
..ExecutionPayload::default()
|
||||
};
|
||||
..ExecutionPayloadBellatrix::default()
|
||||
});
|
||||
|
||||
let block = execution.get_block(&payload, false).await.unwrap();
|
||||
|
||||
|
@ -207,11 +207,11 @@ async fn test_get_tx_by_block_hash_and_index() {
|
|||
let tx_hash =
|
||||
H256::from_str("2dac1b27ab58b493f902dda8b63979a112398d747f1761c0891777c0983e591f").unwrap();
|
||||
|
||||
let mut payload = ExecutionPayload {
|
||||
let mut payload = ExecutionPayload::Bellatrix(ExecutionPayloadBellatrix {
|
||||
block_number: 7530933,
|
||||
..ExecutionPayload::default()
|
||||
};
|
||||
payload.transactions.push(List::from_iter(hex_str_to_bytes("0x02f8b20583623355849502f900849502f91082ea6094326c977e6efc84e512bb9c30f76e30c160ed06fb80b844a9059cbb0000000000000000000000007daccf9b3c1ae2fa5c55f1c978aeef700bc83be0000000000000000000000000000000000000000000000001158e460913d00000c080a0e1445466b058b6f883c0222f1b1f3e2ad9bee7b5f688813d86e3fa8f93aa868ca0786d6e7f3aefa8fe73857c65c32e4884d8ba38d0ecfb947fbffb82e8ee80c167").unwrap()));
|
||||
..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)
|
||||
|
|
|
@ -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",
|
||||
]
|
|
@ -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>
|
|
@ -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;
|
||||
}, {});
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist/",
|
||||
"noImplicitAny": true,
|
||||
"module": "es6",
|
||||
"target": "es6",
|
||||
"jsx": "react",
|
||||
"allowJs": true,
|
||||
"moduleResolution": "node",
|
||||
"sourceMap": true,
|
||||
"declaration": true
|
||||
}
|
||||
}
|
|
@ -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),
|
||||
}),
|
||||
],
|
||||
};
|
4
rpc.md
4
rpc.md
|
@ -22,4 +22,6 @@ Helios provides a variety of RPC methods for interacting with the Ethereum netwo
|
|||
| `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_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 +1 @@
|
|||
nightly
|
||||
nightly-2023-01-23
|
||||
|
|
|
@ -51,7 +51,9 @@
|
|||
//! 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 {
|
||||
|
|
|
@ -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(())
|
||||
}
|
Loading…
Reference in New Issue