mirror of
https://github.com/hl-archive-node/nanoreth.git
synced 2025-12-06 02:49:55 +00:00
feat: --debug.etherscan for fake consensus client (#8082)
Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de> Co-authored-by: Alexey Shekhirin <a.shekhirin@gmail.com>
This commit is contained in:
173
Cargo.lock
generated
173
Cargo.lock
generated
@ -317,11 +317,13 @@ dependencies = [
|
||||
"alloy-json-rpc",
|
||||
"alloy-network",
|
||||
"alloy-primitives",
|
||||
"alloy-pubsub",
|
||||
"alloy-rpc-client",
|
||||
"alloy-rpc-types 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=cc68b93)",
|
||||
"alloy-rpc-types-trace",
|
||||
"alloy-transport",
|
||||
"alloy-transport-http",
|
||||
"alloy-transport-ws",
|
||||
"async-stream",
|
||||
"async-trait",
|
||||
"auto_impl",
|
||||
@ -338,6 +340,24 @@ dependencies = [
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "alloy-pubsub"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/alloy-rs/alloy?rev=cc68b93#cc68b93605f4521c2b0bce1a7efaeff2046cf07c"
|
||||
dependencies = [
|
||||
"alloy-json-rpc",
|
||||
"alloy-primitives",
|
||||
"alloy-transport",
|
||||
"bimap",
|
||||
"futures",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"tower",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "alloy-rlp"
|
||||
version = "0.3.5"
|
||||
@ -366,8 +386,11 @@ version = "0.1.0"
|
||||
source = "git+https://github.com/alloy-rs/alloy?rev=cc68b93#cc68b93605f4521c2b0bce1a7efaeff2046cf07c"
|
||||
dependencies = [
|
||||
"alloy-json-rpc",
|
||||
"alloy-primitives",
|
||||
"alloy-pubsub",
|
||||
"alloy-transport",
|
||||
"alloy-transport-http",
|
||||
"alloy-transport-ws",
|
||||
"futures",
|
||||
"pin-project",
|
||||
"reqwest 0.12.4",
|
||||
@ -627,6 +650,22 @@ dependencies = [
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "alloy-transport-ws"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/alloy-rs/alloy?rev=cc68b93#cc68b93605f4521c2b0bce1a7efaeff2046cf07c"
|
||||
dependencies = [
|
||||
"alloy-pubsub",
|
||||
"alloy-transport",
|
||||
"futures",
|
||||
"http 0.2.12",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
"tokio-tungstenite",
|
||||
"tracing",
|
||||
"ws_stream_wasm",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "alloy-trie"
|
||||
version = "0.4.1"
|
||||
@ -986,6 +1025,17 @@ dependencies = [
|
||||
"syn 2.0.66",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async_io_stream"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6d7b9decdf35d8908a7e3ef02f64c5e9b1695e230154c0e8de3969142d9b94c"
|
||||
dependencies = [
|
||||
"futures",
|
||||
"pharos",
|
||||
"rustc_version 0.4.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "atomic-waker"
|
||||
version = "1.1.2"
|
||||
@ -1127,6 +1177,12 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bimap"
|
||||
version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "230c5f1ca6a325a32553f8640d31ac9b49f2411e901e427570154868b46da4f7"
|
||||
|
||||
[[package]]
|
||||
name = "bincode"
|
||||
version = "1.3.3"
|
||||
@ -3124,7 +3180,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24"
|
||||
dependencies = [
|
||||
"gloo-timers",
|
||||
"send_wrapper",
|
||||
"send_wrapper 0.4.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -4147,7 +4203,7 @@ dependencies = [
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
"url",
|
||||
"webpki-roots",
|
||||
"webpki-roots 0.26.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -5413,6 +5469,16 @@ dependencies = [
|
||||
"wyhash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pharos"
|
||||
version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e9567389417feee6ce15dd6527a8a1ecac205ef62c2932bcf3d9f6fc5b78b414"
|
||||
dependencies = [
|
||||
"futures",
|
||||
"rustc_version 0.4.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf"
|
||||
version = "0.11.2"
|
||||
@ -6139,7 +6205,7 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
"webpki-roots",
|
||||
"webpki-roots 0.26.2",
|
||||
"winreg 0.52.0",
|
||||
]
|
||||
|
||||
@ -6446,6 +6512,28 @@ dependencies = [
|
||||
"reth-storage-api",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "reth-consensus-debug-client"
|
||||
version = "0.2.0-beta.9"
|
||||
dependencies = [
|
||||
"alloy-consensus 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=cc68b93)",
|
||||
"alloy-eips 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=cc68b93)",
|
||||
"alloy-provider",
|
||||
"auto_impl",
|
||||
"eyre",
|
||||
"futures",
|
||||
"reqwest 0.12.4",
|
||||
"reth-node-api",
|
||||
"reth-node-core",
|
||||
"reth-rpc-api",
|
||||
"reth-rpc-builder",
|
||||
"reth-rpc-types",
|
||||
"reth-tracing",
|
||||
"ringbuffer",
|
||||
"serde",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "reth-db"
|
||||
version = "0.2.0-beta.9"
|
||||
@ -7231,6 +7319,7 @@ dependencies = [
|
||||
"reth-blockchain-tree",
|
||||
"reth-config",
|
||||
"reth-consensus",
|
||||
"reth-consensus-debug-client",
|
||||
"reth-db",
|
||||
"reth-db-api",
|
||||
"reth-db-common",
|
||||
@ -8271,6 +8360,12 @@ dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ringbuffer"
|
||||
version = "0.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3df6368f71f205ff9c33c076d170dd56ebf68e8161c733c0caa07a7a5509ed53"
|
||||
|
||||
[[package]]
|
||||
name = "ripemd"
|
||||
version = "0.1.3"
|
||||
@ -8739,6 +8834,12 @@ version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f638d531eccd6e23b980caf34876660d38e265409d8e99b397ab71eb3612fad0"
|
||||
|
||||
[[package]]
|
||||
name = "send_wrapper"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.203"
|
||||
@ -9643,6 +9744,21 @@ dependencies = [
|
||||
"tokio-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-tungstenite"
|
||||
version = "0.20.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "212d5dcb2a1ce06d81107c3d0ffa3121fe974b73f068c8282cb1c32328113b6c"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"log",
|
||||
"rustls 0.21.12",
|
||||
"tokio",
|
||||
"tokio-rustls 0.24.1",
|
||||
"tungstenite",
|
||||
"webpki-roots 0.25.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-util"
|
||||
version = "0.7.11"
|
||||
@ -9962,6 +10078,26 @@ dependencies = [
|
||||
"toml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tungstenite"
|
||||
version = "0.20.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e3dac10fd62eaf6617d3a904ae222845979aec67c615d1c842b4002c7666fb9"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"bytes",
|
||||
"data-encoding",
|
||||
"http 0.2.12",
|
||||
"httparse",
|
||||
"log",
|
||||
"rand 0.8.5",
|
||||
"rustls 0.21.12",
|
||||
"sha1",
|
||||
"thiserror",
|
||||
"url",
|
||||
"utf-8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "txpool-tracing"
|
||||
version = "0.0.0"
|
||||
@ -10101,6 +10237,12 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "utf-8"
|
||||
version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
|
||||
|
||||
[[package]]
|
||||
name = "utf16_iter"
|
||||
version = "1.0.5"
|
||||
@ -10301,6 +10443,12 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "0.25.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1"
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "0.26.2"
|
||||
@ -10620,6 +10768,25 @@ version = "0.5.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51"
|
||||
|
||||
[[package]]
|
||||
name = "ws_stream_wasm"
|
||||
version = "0.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7999f5f4217fe3818726b66257a4475f71e74ffd190776ad053fa159e50737f5"
|
||||
dependencies = [
|
||||
"async_io_stream",
|
||||
"futures",
|
||||
"js-sys",
|
||||
"log",
|
||||
"pharos",
|
||||
"rustc_version 0.4.0",
|
||||
"send_wrapper 0.6.0",
|
||||
"thiserror",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wyhash"
|
||||
version = "0.5.0"
|
||||
|
||||
@ -18,6 +18,8 @@ members = [
|
||||
"crates/consensus/beacon/",
|
||||
"crates/consensus/common/",
|
||||
"crates/consensus/consensus/",
|
||||
"crates/consensus/debug-client/",
|
||||
"crates/ethereum-forks/",
|
||||
"crates/e2e-test-utils/",
|
||||
"crates/engine-primitives/",
|
||||
"crates/errors/",
|
||||
@ -246,6 +248,7 @@ reth-codecs-derive = { path = "crates/storage/codecs/derive" }
|
||||
reth-config = { path = "crates/config" }
|
||||
reth-consensus = { path = "crates/consensus/consensus" }
|
||||
reth-consensus-common = { path = "crates/consensus/common" }
|
||||
reth-consensus-debug-client = { path = "crates/consensus/debug-client" }
|
||||
reth-db = { path = "crates/storage/db", default-features = false }
|
||||
reth-db-api = { path = "crates/storage/db-api" }
|
||||
reth-db-common = { path = "crates/storage/db-common" }
|
||||
|
||||
6
book/cli/reth/node.md
vendored
6
book/cli/reth/node.md
vendored
@ -463,6 +463,12 @@ Debug:
|
||||
--debug.max-block <MAX_BLOCK>
|
||||
Runs the sync only up to the specified block
|
||||
|
||||
--debug.etherscan [<ETHERSCAN_API_URL>]
|
||||
Runs a fake consensus client that advances the chain using recent block hashes on Etherscan. If specified, requires an `ETHERSCAN_API_KEY` environment variable
|
||||
|
||||
--debug.rpc-consensus-ws <RPC_CONSENSUS_WS>
|
||||
Runs a fake consensus client using blocks fetched from an RPC `WebSocket` endpoint
|
||||
|
||||
--debug.skip-fcu <SKIP_FCU>
|
||||
If provided, the engine will skip `n` consecutive FCUs
|
||||
|
||||
|
||||
34
crates/consensus/debug-client/Cargo.toml
Normal file
34
crates/consensus/debug-client/Cargo.toml
Normal file
@ -0,0 +1,34 @@
|
||||
[package]
|
||||
name = "reth-consensus-debug-client"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
# reth
|
||||
reth-node-api.workspace = true
|
||||
reth-node-core.workspace = true
|
||||
reth-rpc-api.workspace = true
|
||||
reth-rpc-types.workspace = true
|
||||
reth-rpc-builder.workspace = true
|
||||
reth-tracing.workspace = true
|
||||
|
||||
# ethereum
|
||||
alloy-consensus = { workspace = true, features = ["serde"] }
|
||||
alloy-eips.workspace = true
|
||||
alloy-provider = { workspace = true, features = ["ws"] }
|
||||
|
||||
auto_impl.workspace = true
|
||||
futures.workspace = true
|
||||
eyre.workspace = true
|
||||
reqwest = { workspace = true, features = ["rustls-tls", "json"] }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
tokio = { workspace = true, features = ["time"] }
|
||||
|
||||
ringbuffer = "0.15.0"
|
||||
224
crates/consensus/debug-client/src/client.rs
Normal file
224
crates/consensus/debug-client/src/client.rs
Normal file
@ -0,0 +1,224 @@
|
||||
use alloy_consensus::TxEnvelope;
|
||||
use alloy_eips::eip2718::Encodable2718;
|
||||
use reth_node_api::EngineTypes;
|
||||
use reth_node_core::{
|
||||
primitives::B256,
|
||||
rpc::types::{BlockTransactions, ExecutionPayloadV2, ExecutionPayloadV3, RichBlock},
|
||||
};
|
||||
use reth_rpc_builder::auth::AuthServerHandle;
|
||||
use reth_rpc_types::ExecutionPayloadV1;
|
||||
use reth_tracing::tracing::warn;
|
||||
use ringbuffer::{AllocRingBuffer, RingBuffer};
|
||||
use std::future::Future;
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
/// Supplies consensus client with new blocks sent in `tx` and a callback to find specific blocks
|
||||
/// by number to fetch past finalized and safe blocks.
|
||||
#[auto_impl::auto_impl(&, Arc, Box)]
|
||||
pub trait BlockProvider: Send + Sync + 'static {
|
||||
/// Runs a block provider to send new blocks to the given sender.
|
||||
///
|
||||
/// Note: This is expected to be spawned in a separate task.
|
||||
fn subscribe_blocks(&self, tx: mpsc::Sender<RichBlock>) -> impl Future<Output = ()> + Send;
|
||||
|
||||
/// Get a past block by number.
|
||||
fn get_block(&self, block_number: u64) -> impl Future<Output = eyre::Result<RichBlock>> + Send;
|
||||
|
||||
/// Get previous block hash using previous block hash buffer. If it isn't available (buffer
|
||||
/// started more recently than `offset`), fetch it using `get_block`.
|
||||
fn get_or_fetch_previous_block(
|
||||
&self,
|
||||
previous_block_hashes: &AllocRingBuffer<B256>,
|
||||
current_block_number: u64,
|
||||
offset: usize,
|
||||
) -> impl Future<Output = eyre::Result<B256>> + Send {
|
||||
async move {
|
||||
let stored_hash = previous_block_hashes
|
||||
.len()
|
||||
.checked_sub(offset)
|
||||
.and_then(|index| previous_block_hashes.get(index));
|
||||
if let Some(hash) = stored_hash {
|
||||
return Ok(*hash);
|
||||
}
|
||||
|
||||
// Return zero hash if the chain isn't long enough to have the block at the offset.
|
||||
let previous_block_number = match current_block_number.checked_sub(offset as u64) {
|
||||
Some(number) => number,
|
||||
None => return Ok(B256::default()),
|
||||
};
|
||||
let block = self.get_block(previous_block_number).await?;
|
||||
block.header.hash.ok_or_else(|| eyre::eyre!("previous block does not have hash"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Debug consensus client that sends FCUs and new payloads using recent blocks from an external
|
||||
/// provider like Etherscan or an RPC endpoint.
|
||||
#[derive(Debug)]
|
||||
pub struct DebugConsensusClient<P: BlockProvider> {
|
||||
/// Handle to execution client.
|
||||
auth_server: AuthServerHandle,
|
||||
/// Provider to get consensus blocks from.
|
||||
block_provider: P,
|
||||
}
|
||||
|
||||
impl<P: BlockProvider> DebugConsensusClient<P> {
|
||||
/// Create a new debug consensus client with the given handle to execution
|
||||
/// client and block provider.
|
||||
pub const fn new(auth_server: AuthServerHandle, block_provider: P) -> Self {
|
||||
Self { auth_server, block_provider }
|
||||
}
|
||||
}
|
||||
|
||||
impl<P: BlockProvider + Clone> DebugConsensusClient<P> {
|
||||
/// Spawn the client to start sending FCUs and new payloads by periodically fetching recent
|
||||
/// blocks.
|
||||
pub async fn run<T: EngineTypes>(self) {
|
||||
let execution_client = self.auth_server.http_client();
|
||||
let mut previous_block_hashes = AllocRingBuffer::new(64);
|
||||
|
||||
let mut block_stream = {
|
||||
let (tx, rx) = mpsc::channel::<RichBlock>(64);
|
||||
let block_provider = self.block_provider.clone();
|
||||
tokio::spawn(async move {
|
||||
block_provider.subscribe_blocks(tx).await;
|
||||
});
|
||||
rx
|
||||
};
|
||||
|
||||
while let Some(block) = block_stream.recv().await {
|
||||
let payload = rich_block_to_execution_payload_v3(block);
|
||||
|
||||
let block_hash = payload.block_hash();
|
||||
let block_number = payload.block_number();
|
||||
|
||||
// Send new events to execution client
|
||||
reth_rpc_api::EngineApiClient::<T>::new_payload_v3(
|
||||
&execution_client,
|
||||
payload.execution_payload_v3,
|
||||
payload.versioned_hashes,
|
||||
payload.parent_beacon_block_root,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
previous_block_hashes.push(block_hash);
|
||||
|
||||
// Load previous block hashes. We're using (head - 32) and (head - 64) as the safe and
|
||||
// finalized block hashes.
|
||||
let safe_block_hash = self.block_provider.get_or_fetch_previous_block(
|
||||
&previous_block_hashes,
|
||||
block_number,
|
||||
32,
|
||||
);
|
||||
let finalized_block_hash = self.block_provider.get_or_fetch_previous_block(
|
||||
&previous_block_hashes,
|
||||
block_number,
|
||||
64,
|
||||
);
|
||||
let (safe_block_hash, finalized_block_hash) =
|
||||
tokio::join!(safe_block_hash, finalized_block_hash);
|
||||
let (safe_block_hash, finalized_block_hash) = match (
|
||||
safe_block_hash,
|
||||
finalized_block_hash,
|
||||
) {
|
||||
(Ok(safe_block_hash), Ok(finalized_block_hash)) => {
|
||||
(safe_block_hash, finalized_block_hash)
|
||||
}
|
||||
(safe_block_hash, finalized_block_hash) => {
|
||||
warn!(target: "consensus::debug-client", ?safe_block_hash, ?finalized_block_hash, "failed to fetch safe or finalized hash from etherscan");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
reth_rpc_api::EngineApiClient::<T>::fork_choice_updated_v3(
|
||||
&execution_client,
|
||||
reth_rpc_types::engine::ForkchoiceState {
|
||||
head_block_hash: block_hash,
|
||||
safe_block_hash,
|
||||
finalized_block_hash,
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Cancun "new payload" event.
|
||||
#[derive(Debug)]
|
||||
struct ExecutionNewPayload {
|
||||
execution_payload_v3: ExecutionPayloadV3,
|
||||
versioned_hashes: Vec<B256>,
|
||||
parent_beacon_block_root: B256,
|
||||
}
|
||||
|
||||
impl ExecutionNewPayload {
|
||||
/// Get block hash from block in the payload
|
||||
const fn block_hash(&self) -> B256 {
|
||||
self.execution_payload_v3.payload_inner.payload_inner.block_hash
|
||||
}
|
||||
|
||||
/// Get block number from block in the payload
|
||||
const fn block_number(&self) -> u64 {
|
||||
self.execution_payload_v3.payload_inner.payload_inner.block_number
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert a rich block from RPC / Etherscan to params for an execution client's "new payload"
|
||||
/// method. Assumes that the block contains full transactions.
|
||||
fn rich_block_to_execution_payload_v3(block: RichBlock) -> ExecutionNewPayload {
|
||||
let transactions = match &block.transactions {
|
||||
BlockTransactions::Full(txs) => txs.clone(),
|
||||
// Empty array gets deserialized as BlockTransactions::Hashes.
|
||||
BlockTransactions::Hashes(txs) if txs.is_empty() => vec![],
|
||||
BlockTransactions::Hashes(_) | BlockTransactions::Uncle => {
|
||||
panic!("Received uncle block or hash-only transactions from Etherscan API")
|
||||
}
|
||||
};
|
||||
|
||||
// Concatenate all blob hashes from all transactions in order
|
||||
// https://github.com/ethereum/execution-apis/blob/main/src/engine/cancun.md#specification
|
||||
let versioned_hashes = transactions
|
||||
.iter()
|
||||
.flat_map(|tx| tx.blob_versioned_hashes.clone().unwrap_or_default())
|
||||
.collect();
|
||||
|
||||
let payload: ExecutionPayloadV3 = ExecutionPayloadV3 {
|
||||
payload_inner: ExecutionPayloadV2 {
|
||||
payload_inner: ExecutionPayloadV1 {
|
||||
parent_hash: block.header.parent_hash,
|
||||
fee_recipient: block.header.miner,
|
||||
state_root: block.header.state_root,
|
||||
receipts_root: block.header.receipts_root,
|
||||
logs_bloom: block.header.logs_bloom,
|
||||
prev_randao: block.header.mix_hash.unwrap(),
|
||||
block_number: block.header.number.unwrap(),
|
||||
gas_limit: block.header.gas_limit.try_into().unwrap(),
|
||||
gas_used: block.header.gas_used.try_into().unwrap(),
|
||||
timestamp: block.header.timestamp,
|
||||
extra_data: block.header.extra_data.clone(),
|
||||
base_fee_per_gas: block.header.base_fee_per_gas.unwrap().try_into().unwrap(),
|
||||
block_hash: block.header.hash.unwrap(),
|
||||
transactions: transactions
|
||||
.into_iter()
|
||||
.map(|tx| {
|
||||
let envelope: TxEnvelope = tx.try_into().unwrap();
|
||||
let mut buffer: Vec<u8> = vec![];
|
||||
envelope.encode_2718(&mut buffer);
|
||||
buffer.into()
|
||||
})
|
||||
.collect(),
|
||||
},
|
||||
withdrawals: block.withdrawals.clone().unwrap_or_default(),
|
||||
},
|
||||
blob_gas_used: block.header.blob_gas_used.unwrap().try_into().unwrap(),
|
||||
excess_blob_gas: block.header.excess_blob_gas.unwrap().try_into().unwrap(),
|
||||
};
|
||||
|
||||
ExecutionNewPayload {
|
||||
execution_payload_v3: payload,
|
||||
versioned_hashes,
|
||||
parent_beacon_block_root: block.header.parent_beacon_block_root.unwrap(),
|
||||
}
|
||||
}
|
||||
19
crates/consensus/debug-client/src/lib.rs
Normal file
19
crates/consensus/debug-client/src/lib.rs
Normal file
@ -0,0 +1,19 @@
|
||||
//! Debug consensus client.
|
||||
//!
|
||||
//! This is a worker that sends FCUs and new payloads by fetching recent blocks from an external
|
||||
//! provider like Etherscan or an RPC endpoint. This allows to quickly test the execution client
|
||||
//! without running a consensus node.
|
||||
|
||||
#![doc(
|
||||
html_logo_url = "https://raw.githubusercontent.com/paradigmxyz/reth/main/assets/reth-docs.png",
|
||||
html_favicon_url = "https://avatars0.githubusercontent.com/u/97369466?s=256",
|
||||
issue_tracker_base_url = "https://github.com/paradigmxyz/reth/issues/"
|
||||
)]
|
||||
#![cfg_attr(not(test), warn(unused_crate_dependencies))]
|
||||
#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))]
|
||||
|
||||
mod client;
|
||||
mod providers;
|
||||
|
||||
pub use client::{BlockProvider, DebugConsensusClient};
|
||||
pub use providers::{EtherscanBlockProvider, RpcBlockProvider};
|
||||
105
crates/consensus/debug-client/src/providers/etherscan.rs
Normal file
105
crates/consensus/debug-client/src/providers/etherscan.rs
Normal file
@ -0,0 +1,105 @@
|
||||
use crate::BlockProvider;
|
||||
use alloy_eips::BlockNumberOrTag;
|
||||
use reqwest::Client;
|
||||
use reth_node_core::rpc::types::RichBlock;
|
||||
use reth_tracing::tracing::warn;
|
||||
use serde::Deserialize;
|
||||
use std::time::Duration;
|
||||
use tokio::{sync::mpsc, time::interval};
|
||||
|
||||
/// Block provider that fetches new blocks from Etherscan API.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct EtherscanBlockProvider {
|
||||
http_client: Client,
|
||||
base_url: String,
|
||||
api_key: String,
|
||||
interval: Duration,
|
||||
}
|
||||
|
||||
impl EtherscanBlockProvider {
|
||||
/// Create a new Etherscan block provider with the given base URL and API key.
|
||||
pub fn new(base_url: String, api_key: String) -> Self {
|
||||
Self { http_client: Client::new(), base_url, api_key, interval: Duration::from_secs(3) }
|
||||
}
|
||||
|
||||
/// Sets the interval at which the provider fetches new blocks.
|
||||
pub const fn with_interval(mut self, interval: Duration) -> Self {
|
||||
self.interval = interval;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl BlockProvider for EtherscanBlockProvider {
|
||||
async fn subscribe_blocks(&self, tx: mpsc::Sender<RichBlock>) {
|
||||
let mut last_block_number: Option<u64> = None;
|
||||
let mut interval = interval(self.interval);
|
||||
loop {
|
||||
interval.tick().await;
|
||||
let block = match load_etherscan_block(
|
||||
&self.http_client,
|
||||
&self.base_url,
|
||||
&self.api_key,
|
||||
BlockNumberOrTag::Latest,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(block) => block,
|
||||
Err(err) => {
|
||||
warn!(target: "consensus::debug-client", %err, "failed to fetch a block from Etherscan");
|
||||
continue
|
||||
}
|
||||
};
|
||||
let block_number = block.header.number.unwrap();
|
||||
if Some(block_number) == last_block_number {
|
||||
continue;
|
||||
}
|
||||
|
||||
if tx.send(block).await.is_err() {
|
||||
// channel closed
|
||||
break;
|
||||
}
|
||||
|
||||
last_block_number = Some(block_number);
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_block(&self, block_number: u64) -> eyre::Result<RichBlock> {
|
||||
load_etherscan_block(
|
||||
&self.http_client,
|
||||
&self.base_url,
|
||||
&self.api_key,
|
||||
BlockNumberOrTag::Number(block_number),
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct EtherscanBlockResponse {
|
||||
result: RichBlock,
|
||||
}
|
||||
|
||||
/// Load block using Etherscan API. Note: only `BlockNumberOrTag::Latest`,
|
||||
/// `BlockNumberOrTag::Earliest`, `BlockNumberOrTag::Pending`, `BlockNumberOrTag::Number(u64)` are
|
||||
/// supported.
|
||||
async fn load_etherscan_block(
|
||||
http_client: &Client,
|
||||
base_url: &str,
|
||||
api_key: &str,
|
||||
block_number_or_tag: BlockNumberOrTag,
|
||||
) -> eyre::Result<RichBlock> {
|
||||
let block: EtherscanBlockResponse = http_client
|
||||
.get(base_url)
|
||||
.query(&[
|
||||
("module", "proxy"),
|
||||
("action", "eth_getBlockByNumber"),
|
||||
("tag", &block_number_or_tag.to_string()),
|
||||
("boolean", "true"),
|
||||
("apikey", api_key),
|
||||
])
|
||||
.send()
|
||||
.await?
|
||||
.json()
|
||||
.await?;
|
||||
Ok(block.result)
|
||||
}
|
||||
5
crates/consensus/debug-client/src/providers/mod.rs
Normal file
5
crates/consensus/debug-client/src/providers/mod.rs
Normal file
@ -0,0 +1,5 @@
|
||||
mod etherscan;
|
||||
mod rpc;
|
||||
|
||||
pub use etherscan::EtherscanBlockProvider;
|
||||
pub use rpc::RpcBlockProvider;
|
||||
58
crates/consensus/debug-client/src/providers/rpc.rs
Normal file
58
crates/consensus/debug-client/src/providers/rpc.rs
Normal file
@ -0,0 +1,58 @@
|
||||
use crate::BlockProvider;
|
||||
use alloy_eips::BlockNumberOrTag;
|
||||
use alloy_provider::{Provider, ProviderBuilder};
|
||||
use futures::StreamExt;
|
||||
use reth_node_core::rpc::types::RichBlock;
|
||||
use tokio::sync::mpsc::Sender;
|
||||
|
||||
/// Block provider that fetches new blocks from an RPC endpoint using a websocket connection.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RpcBlockProvider {
|
||||
ws_rpc_url: String,
|
||||
}
|
||||
|
||||
impl RpcBlockProvider {
|
||||
/// Create a new RPC block provider with the given WS RPC URL.
|
||||
pub const fn new(ws_rpc_url: String) -> Self {
|
||||
Self { ws_rpc_url }
|
||||
}
|
||||
}
|
||||
|
||||
impl BlockProvider for RpcBlockProvider {
|
||||
async fn subscribe_blocks(&self, tx: Sender<RichBlock>) {
|
||||
let ws_provider = ProviderBuilder::new()
|
||||
.on_builtin(&self.ws_rpc_url)
|
||||
.await
|
||||
.expect("failed to create WS provider");
|
||||
let mut stream = ws_provider
|
||||
.subscribe_blocks()
|
||||
.await
|
||||
.expect("failed to subscribe on new blocks")
|
||||
.into_stream();
|
||||
|
||||
while let Some(block) = stream.next().await {
|
||||
let full_block = ws_provider
|
||||
.get_block_by_hash(block.header.hash.unwrap(), true)
|
||||
.await
|
||||
.expect("failed to get block")
|
||||
.expect("block not found");
|
||||
if tx.send(full_block.into()).await.is_err() {
|
||||
// channel closed
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_block(&self, block_number: u64) -> eyre::Result<RichBlock> {
|
||||
let ws_provider = ProviderBuilder::new()
|
||||
.on_builtin(&self.ws_rpc_url)
|
||||
.await
|
||||
.expect("failed to create WS provider");
|
||||
let block: RichBlock = ws_provider
|
||||
.get_block_by_number(BlockNumberOrTag::Number(block_number), true)
|
||||
.await?
|
||||
.ok_or_else(|| eyre::eyre!("block not found by number {}", block_number))?
|
||||
.into();
|
||||
Ok(block)
|
||||
}
|
||||
}
|
||||
@ -28,6 +28,26 @@ pub struct DebugArgs {
|
||||
#[arg(long = "debug.max-block", help_heading = "Debug")]
|
||||
pub max_block: Option<u64>,
|
||||
|
||||
/// Runs a fake consensus client that advances the chain using recent block hashes
|
||||
/// on Etherscan. If specified, requires an `ETHERSCAN_API_KEY` environment variable.
|
||||
#[arg(
|
||||
long = "debug.etherscan",
|
||||
help_heading = "Debug",
|
||||
conflicts_with = "tip",
|
||||
conflicts_with = "rpc_consensus_ws",
|
||||
value_name = "ETHERSCAN_API_URL"
|
||||
)]
|
||||
pub etherscan: Option<Option<String>>,
|
||||
|
||||
/// Runs a fake consensus client using blocks fetched from an RPC `WebSocket` endpoint.
|
||||
#[arg(
|
||||
long = "debug.rpc-consensus-ws",
|
||||
help_heading = "Debug",
|
||||
conflicts_with = "tip",
|
||||
conflicts_with = "etherscan"
|
||||
)]
|
||||
pub rpc_consensus_ws: Option<String>,
|
||||
|
||||
/// If provided, the engine will skip `n` consecutive FCUs.
|
||||
#[arg(long = "debug.skip-fcu", help_heading = "Debug")]
|
||||
pub skip_fcu: Option<usize>,
|
||||
|
||||
@ -41,7 +41,9 @@ reth-config.workspace = true
|
||||
reth-downloaders.workspace = true
|
||||
reth-node-events.workspace = true
|
||||
reth-consensus.workspace = true
|
||||
reth-consensus-debug-client.workspace = true
|
||||
reth-rpc-types.workspace = true
|
||||
|
||||
## async
|
||||
futures.workspace = true
|
||||
tokio = { workspace = true, features = [
|
||||
|
||||
@ -18,6 +18,7 @@ use reth_blockchain_tree::{
|
||||
TreeExternals,
|
||||
};
|
||||
use reth_consensus::Consensus;
|
||||
use reth_consensus_debug_client::{DebugConsensusClient, EtherscanBlockProvider, RpcBlockProvider};
|
||||
use reth_exex::ExExManagerHandle;
|
||||
use reth_network::NetworkEvents;
|
||||
use reth_node_api::{FullNodeComponents, FullNodeTypes};
|
||||
@ -397,6 +398,48 @@ where
|
||||
let _ = tx.send(res);
|
||||
});
|
||||
|
||||
if let Some(maybe_custom_etherscan_url) = ctx.node_config().debug.etherscan.clone() {
|
||||
info!(target: "reth::cli", "Using etherscan as consensus client");
|
||||
|
||||
let chain = ctx.node_config().chain.chain;
|
||||
let etherscan_url = maybe_custom_etherscan_url.map(Ok).unwrap_or_else(|| {
|
||||
// If URL isn't provided, use default Etherscan URL for the chain if it is known
|
||||
chain
|
||||
.etherscan_urls()
|
||||
.map(|urls| urls.0.to_string())
|
||||
.ok_or_else(|| eyre::eyre!("failed to get etherscan url for chain: {chain}"))
|
||||
})?;
|
||||
|
||||
let block_provider = EtherscanBlockProvider::new(
|
||||
etherscan_url,
|
||||
chain.etherscan_api_key().ok_or_else(|| {
|
||||
eyre::eyre!(
|
||||
"etherscan api key not found for rpc consensus client for chain: {chain}"
|
||||
)
|
||||
})?,
|
||||
);
|
||||
let rpc_consensus_client = DebugConsensusClient::new(
|
||||
rpc_server_handles.auth.clone(),
|
||||
Arc::new(block_provider),
|
||||
);
|
||||
ctx.task_executor().spawn_critical("etherscan consensus client", async move {
|
||||
rpc_consensus_client.run::<T::Engine>().await
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(rpc_ws_url) = ctx.node_config().debug.rpc_consensus_ws.clone() {
|
||||
info!(target: "reth::cli", "Using rpc provider as consensus client");
|
||||
|
||||
let block_provider = RpcBlockProvider::new(rpc_ws_url);
|
||||
let rpc_consensus_client = DebugConsensusClient::new(
|
||||
rpc_server_handles.auth.clone(),
|
||||
Arc::new(block_provider),
|
||||
);
|
||||
ctx.task_executor().spawn_critical("rpc consensus client", async move {
|
||||
rpc_consensus_client.run::<T::Engine>().await
|
||||
});
|
||||
}
|
||||
|
||||
let full_node = FullNode {
|
||||
evm_config: node_adapter.components.evm_config().clone(),
|
||||
block_executor: node_adapter.components.block_executor().clone(),
|
||||
|
||||
Reference in New Issue
Block a user