feat: add sse payload attributes example and beacon api support (#5130)

This commit is contained in:
Matthias Seitz
2023-10-23 22:47:04 +02:00
committed by GitHub
parent b71a8ac85d
commit 7d46db4a21
11 changed files with 609 additions and 4 deletions

255
Cargo.lock generated
View File

@ -496,6 +496,17 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9"
[[package]]
name = "async-channel"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35"
dependencies = [
"concurrent-queue",
"event-listener",
"futures-core",
]
[[package]]
name = "async-compression"
version = "0.4.4"
@ -521,6 +532,20 @@ dependencies = [
"event-listener",
]
[[package]]
name = "async-sse"
version = "5.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e6fa871e4334a622afd6bb2f611635e8083a6f5e2936c0f90f37c7ef9856298"
dependencies = [
"async-channel",
"futures-lite",
"http-types",
"log",
"memchr",
"pin-project-lite",
]
[[package]]
name = "async-trait"
version = "0.1.74"
@ -661,6 +686,19 @@ dependencies = [
"serde",
]
[[package]]
name = "beacon-api-sse"
version = "0.0.0"
dependencies = [
"clap",
"eyre",
"futures-util",
"mev-share-sse",
"reth",
"tokio",
"tracing",
]
[[package]]
name = "bech32"
version = "0.9.1"
@ -1376,6 +1414,15 @@ dependencies = [
"unicode-width",
]
[[package]]
name = "concurrent-queue"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f057a694a54f12365049b0958a1685bb52d567f5593b355fbf685838e873d400"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "confy"
version = "0.5.1"
@ -1632,9 +1679,9 @@ checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
[[package]]
name = "crypto-bigint"
version = "0.5.3"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "740fe28e594155f10cfc383984cbefd529d7396050557148f79cb0f621204124"
checksum = "071c0f5945634bc9ba7a452f492377dd6b1993665ddb58f28704119b32f07a9a"
dependencies = [
"generic-array",
"rand_core 0.6.4",
@ -2703,6 +2750,21 @@ version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "foreign-types"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
dependencies = [
"foreign-types-shared",
]
[[package]]
name = "foreign-types-shared"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
[[package]]
name = "form_urlencoded"
version = "1.2.0"
@ -2772,6 +2834,21 @@ version = "0.3.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964"
[[package]]
name = "futures-lite"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce"
dependencies = [
"fastrand 1.9.0",
"futures-core",
"futures-io",
"memchr",
"parking",
"pin-project-lite",
"waker-fn",
]
[[package]]
name = "futures-locks"
version = "0.7.1"
@ -3167,6 +3244,26 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "add0ab9360ddbd88cfeb3bd9574a1d85cfdfa14db10b3e21d3700dbc4328758f"
[[package]]
name = "http-types"
version = "2.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e9b187a72d63adbfba487f48095306ac823049cb504ee195541e91c7775f5ad"
dependencies = [
"anyhow",
"async-channel",
"base64 0.13.1",
"futures-lite",
"infer",
"pin-project-lite",
"rand 0.7.3",
"serde",
"serde_json",
"serde_qs",
"serde_urlencoded",
"url",
]
[[package]]
name = "httparse"
version = "1.8.0"
@ -3255,6 +3352,19 @@ dependencies = [
"tracing",
]
[[package]]
name = "hyper-tls"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
dependencies = [
"bytes",
"hyper",
"native-tls",
"tokio",
"tokio-native-tls",
]
[[package]]
name = "iai"
version = "0.1.1"
@ -3535,6 +3645,12 @@ dependencies = [
"serde",
]
[[package]]
name = "infer"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64e9829a50b42bb782c1df523f78d332fe371b10c661e78b7a3c34b0198e9fac"
[[package]]
name = "inferno"
version = "0.11.17"
@ -4185,6 +4301,26 @@ dependencies = [
"sketches-ddsketch",
]
[[package]]
name = "mev-share-sse"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e59928ecfd8f9dd3211f2eb08bdc260c78c2e2af34d1a652445a6287d3c95942"
dependencies = [
"async-sse",
"bytes",
"ethers-core",
"futures-util",
"http-types",
"pin-project-lite",
"reqwest",
"serde",
"serde_json",
"thiserror",
"tokio",
"tracing",
]
[[package]]
name = "mime"
version = "0.3.17"
@ -4282,6 +4418,24 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7843ec2de400bcbc6a6328c958dc38e5359da6e93e72e37bc5246bf1ae776389"
[[package]]
name = "native-tls"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e"
dependencies = [
"lazy_static",
"libc",
"log",
"openssl",
"openssl-probe",
"openssl-sys",
"schannel",
"security-framework",
"security-framework-sys",
"tempfile",
]
[[package]]
name = "nibble_vec"
version = "0.1.0"
@ -4533,12 +4687,50 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "openssl"
version = "0.10.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bac25ee399abb46215765b1cb35bc0212377e58a061560d8b29b024fd0430e7c"
dependencies = [
"bitflags 2.4.1",
"cfg-if",
"foreign-types",
"libc",
"once_cell",
"openssl-macros",
"openssl-sys",
]
[[package]]
name = "openssl-macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.38",
]
[[package]]
name = "openssl-probe"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]]
name = "openssl-sys"
version = "0.9.93"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db4d56a4c0478783083cfafcc42493dd4a981d41669da64b4572a2a089b51b1d"
dependencies = [
"cc",
"libc",
"pkg-config",
"vcpkg",
]
[[package]]
name = "option-ext"
version = "0.2.0"
@ -4611,6 +4803,12 @@ dependencies = [
"winapi",
]
[[package]]
name = "parking"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae"
[[package]]
name = "parking_lot"
version = "0.11.2"
@ -5392,10 +5590,12 @@ dependencies = [
"http",
"http-body",
"hyper",
"hyper-tls",
"ipnet",
"js-sys",
"log",
"mime",
"native-tls",
"once_cell",
"percent-encoding",
"pin-project-lite",
@ -5404,10 +5604,13 @@ dependencies = [
"serde_urlencoded",
"system-configuration",
"tokio",
"tokio-native-tls",
"tokio-util",
"tower-service",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
"wasm-streams",
"web-sys",
"winreg",
]
@ -6313,6 +6516,7 @@ dependencies = [
"reth-primitives",
"serde",
"serde_json",
"serde_with",
"similar-asserts",
"thiserror",
]
@ -7032,6 +7236,17 @@ dependencies = [
"serde",
]
[[package]]
name = "serde_qs"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7715380eec75f029a4ef7de39a9200e0a63823176b759d055b613f5a87df6a6"
dependencies = [
"percent-encoding",
"serde",
"thiserror",
]
[[package]]
name = "serde_spanned"
version = "0.6.3"
@ -7796,6 +8011,16 @@ dependencies = [
"syn 2.0.38",
]
[[package]]
name = "tokio-native-tls"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
dependencies = [
"native-tls",
"tokio",
]
[[package]]
name = "tokio-rustls"
version = "0.24.1"
@ -8355,6 +8580,7 @@ dependencies = [
"form_urlencoded",
"idna 0.4.0",
"percent-encoding",
"serde",
]
[[package]]
@ -8406,6 +8632,12 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
[[package]]
name = "vcpkg"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "vergen"
version = "8.2.5"
@ -8432,6 +8664,12 @@ dependencies = [
"libc",
]
[[package]]
name = "waker-fn"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3c4517f54858c779bbcbf228f4fca63d121bf85fbecb2dc578cdf4a39395690"
[[package]]
name = "walkdir"
version = "2.4.0"
@ -8529,6 +8767,19 @@ version = "0.2.87"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1"
[[package]]
name = "wasm-streams"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4609d447824375f43e1ffbc051b50ad8f4b3ae8219680c94452ea05eb240ac7"
dependencies = [
"futures-util",
"js-sys",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
]
[[package]]
name = "web-sys"
version = "0.3.64"

View File

@ -51,6 +51,7 @@ members = [
"examples/cli-extension-event-hooks",
"examples/rpc-db",
"examples/manual-p2p",
"examples/beacon-api-sse",
"examples/trace-transaction-cli"
]
default-members = ["bin/reth"]

View File

@ -21,6 +21,7 @@ alloy-rlp = { workspace = true, features = ["arrayvec"] }
thiserror.workspace = true
itertools.workspace = true
serde = { workspace = true, features = ["derive"] }
serde_with = "3.3"
serde_json.workspace = true
jsonrpsee-types = { workspace = true, optional = true }
alloy-primitives = { workspace = true, features = ["rand", "rlp"] }

View File

@ -0,0 +1,71 @@
//! Support for the Beacon API events
//!
//! See also [ethereum-beacon-API eventstream](https://ethereum.github.io/beacon-APIs/#/Events/eventstream)
use crate::engine::PayloadAttributes;
use alloy_primitives::B256;
use serde::{Deserialize, Serialize};
use serde_with::{serde_as, DisplayFromStr};
/// Event for the `payload_attributes` topic of the beacon API node event stream.
///
/// This event gives block builders and relays sufficient information to construct or verify a block
/// at `proposal_slot`.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PayloadAttributesEvent {
/// the identifier of the beacon hard fork at `proposal_slot`, e.g `"bellatrix"`, `"capella"`.
pub version: String,
/// Wrapped data of the event.
pub data: PayloadAttributesData,
}
impl PayloadAttributesEvent {
/// Returns the payload attributes
pub fn attributes(&self) -> &PayloadAttributes {
&self.data.payload_attributes
}
}
/// Data of the event that contains the payload attributes
#[serde_as]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PayloadAttributesData {
/// The slot at which a block using these payload attributes may be built
#[serde_as(as = "DisplayFromStr")]
pub proposal_slot: u64,
/// the beacon block root of the parent block to be built upon.
pub parent_block_root: B256,
/// he execution block number of the parent block.
#[serde_as(as = "DisplayFromStr")]
pub parent_block_number: u64,
/// the execution block hash of the parent block.
pub parent_block_hash: B256,
/// The execution block number of the parent block.
/// the validator index of the proposer at `proposal_slot` on the chain identified by
/// `parent_block_root`.
#[serde_as(as = "DisplayFromStr")]
pub proposer_index: u64,
/// Beacon API encoding of `PayloadAttributesV<N>` as defined by the `execution-apis`
/// specification
///
/// Note: this uses the beacon API format which uses snake-case and quoted decimals rather than
/// big-endian hex.
#[serde(with = "crate::eth::engine::payload::beacon_api_payload_attributes")]
pub payload_attributes: PayloadAttributes,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn serde_payload_attributes_event() {
let s = r#"{"version":"capella","data":{"proposal_slot":"173332","proposer_index":"649112","parent_block_root":"0x5a49069647f6bf8f25d76b55ce920947654ade4ba1c6ab826d16712dd62b42bf","parent_block_number":"161093","parent_block_hash":"0x608b3d140ecb5bbcd0019711ac3704ece7be8e6d100816a55db440c1bcbb0251","payload_attributes":{"timestamp":"1697982384","prev_randao":"0x3142abd98055871ebf78f0f8e758fd3a04df3b6e34d12d09114f37a737f8f01e","suggested_fee_recipient":"0x0000000000000000000000000000000000000001","withdrawals":[{"index":"2461612","validator_index":"853570","address":"0x778f5f13c4be78a3a4d7141bcb26999702f407cf","amount":"45016211"},{"index":"2461613","validator_index":"853571","address":"0x778f5f13c4be78a3a4d7141bcb26999702f407cf","amount":"5269785"},{"index":"2461614","validator_index":"853572","address":"0x778f5f13c4be78a3a4d7141bcb26999702f407cf","amount":"5275106"},{"index":"2461615","validator_index":"853573","address":"0x778f5f13c4be78a3a4d7141bcb26999702f407cf","amount":"5235962"},{"index":"2461616","validator_index":"853574","address":"0x778f5f13c4be78a3a4d7141bcb26999702f407cf","amount":"5252171"},{"index":"2461617","validator_index":"853575","address":"0x778f5f13c4be78a3a4d7141bcb26999702f407cf","amount":"5221319"},{"index":"2461618","validator_index":"853576","address":"0x778f5f13c4be78a3a4d7141bcb26999702f407cf","amount":"5260879"},{"index":"2461619","validator_index":"853577","address":"0x778f5f13c4be78a3a4d7141bcb26999702f407cf","amount":"5285244"},{"index":"2461620","validator_index":"853578","address":"0x778f5f13c4be78a3a4d7141bcb26999702f407cf","amount":"5266681"},{"index":"2461621","validator_index":"853579","address":"0x778f5f13c4be78a3a4d7141bcb26999702f407cf","amount":"5271322"},{"index":"2461622","validator_index":"853580","address":"0x778f5f13c4be78a3a4d7141bcb26999702f407cf","amount":"5231327"},{"index":"2461623","validator_index":"853581","address":"0x778f5f13c4be78a3a4d7141bcb26999702f407cf","amount":"5276761"},{"index":"2461624","validator_index":"853582","address":"0x778f5f13c4be78a3a4d7141bcb26999702f407cf","amount":"5246244"},{"index":"2461625","validator_index":"853583","address":"0x778f5f13c4be78a3a4d7141bcb26999702f407cf","amount":"5261011"},{"index":"2461626","validator_index":"853584","address":"0x778f5f13c4be78a3a4d7141bcb26999702f407cf","amount":"5276477"},{"index":"2461627","validator_index":"853585","address":"0x778f5f13c4be78a3a4d7141bcb26999702f407cf","amount":"5275319"}]}}}"#;
let event = serde_json::from_str::<PayloadAttributesEvent>(s).unwrap();
let input = serde_json::from_str::<serde_json::Value>(s).unwrap();
let json = serde_json::to_value(event).unwrap();
assert_eq!(input, json);
}
}

View File

@ -0,0 +1,2 @@
/// Beacon API events support.
pub mod events;

View File

@ -8,6 +8,9 @@ pub mod payload;
mod transition;
pub use self::{cancun::*, forkchoice::*, payload::*, transition::*};
/// Beacon API types
pub mod beacon_api;
/// The list of all supported Engine capabilities available over the engine endpoint.
pub const CAPABILITIES: [&str; 12] = [
"engine_forkchoiceUpdatedV1",

View File

@ -1,3 +1,4 @@
use crate::eth::withdrawal::BeaconAPIWithdrawal;
pub use crate::Withdrawal;
use alloy_primitives::{Address, Bloom, Bytes, B256, B64, U256, U64};
use reth_primitives::{
@ -5,6 +6,7 @@ use reth_primitives::{
BlobTransactionSidecar, SealedBlock,
};
use serde::{ser::SerializeMap, Deserialize, Serialize, Serializer};
use serde_with::{serde_as, DisplayFromStr};
/// The execution payload body response that allows for `null` values.
pub type ExecutionPayloadBodiesV1 = Vec<Option<ExecutionPayloadBodyV1>>;
@ -411,6 +413,63 @@ pub struct PayloadAttributes {
pub parent_beacon_block_root: Option<B256>,
}
#[serde_as]
#[derive(Serialize, Deserialize)]
struct BeaconAPIPayloadAttributes {
#[serde_as(as = "DisplayFromStr")]
timestamp: u64,
prev_randao: B256,
suggested_fee_recipient: Address,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde_as(as = "Option<Vec<BeaconAPIWithdrawal>>")]
withdrawals: Option<Vec<Withdrawal>>,
#[serde(skip_serializing_if = "Option::is_none")]
parent_beacon_block_root: Option<B256>,
}
/// A helper module for serializing and deserializing the payload attributes for the beacon API.
///
/// The beacon API encoded object has equivalent fields to the [PayloadAttributes] with two
/// differences:
/// 1) `snake_case` identifiers must be used rather than `camelCase`;
/// 2) integers must be encoded as quoted decimals rather than big-endian hex.
pub mod beacon_api_payload_attributes {
use super::*;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
/// Serialize the payload attributes for the beacon API.
pub fn serialize<S>(
payload_attributes: &PayloadAttributes,
serializer: S,
) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let beacon_api_payload_attributes = BeaconAPIPayloadAttributes {
timestamp: payload_attributes.timestamp.to(),
prev_randao: payload_attributes.prev_randao,
suggested_fee_recipient: payload_attributes.suggested_fee_recipient,
withdrawals: payload_attributes.withdrawals.clone(),
parent_beacon_block_root: payload_attributes.parent_beacon_block_root,
};
beacon_api_payload_attributes.serialize(serializer)
}
/// Deserialize the payload attributes for the beacon API.
pub fn deserialize<'de, D>(deserializer: D) -> Result<PayloadAttributes, D::Error>
where
D: Deserializer<'de>,
{
let beacon_api_payload_attributes = BeaconAPIPayloadAttributes::deserialize(deserializer)?;
Ok(PayloadAttributes {
timestamp: U64::from(beacon_api_payload_attributes.timestamp),
prev_randao: beacon_api_payload_attributes.prev_randao,
suggested_fee_recipient: beacon_api_payload_attributes.suggested_fee_recipient,
withdrawals: beacon_api_payload_attributes.withdrawals,
parent_beacon_block_root: beacon_api_payload_attributes.parent_beacon_block_root,
})
}
}
/// This structure contains the result of processing a payload or fork choice update.
#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "camelCase")]
@ -862,4 +921,21 @@ mod tests {
serde_json::from_str(input);
assert!(payload_res.is_err());
}
#[test]
fn beacon_api_payload_serde() {
#[derive(Serialize, Deserialize)]
#[serde(transparent)]
struct Event {
#[serde(with = "beacon_api_payload_attributes")]
payload: PayloadAttributes,
}
let s = r#"{"timestamp":"1697981664","prev_randao":"0x739947d9f0aed15e32ed05a978e53b55cdcfe3db4a26165890fa45a80a06c996","suggested_fee_recipient":"0x0000000000000000000000000000000000000001","withdrawals":[{"index":"2460700","validator_index":"852657","address":"0x778f5f13c4be78a3a4d7141bcb26999702f407cf","amount":"5268915"},{"index":"2460701","validator_index":"852658","address":"0x778f5f13c4be78a3a4d7141bcb26999702f407cf","amount":"5253066"},{"index":"2460702","validator_index":"852659","address":"0x778f5f13c4be78a3a4d7141bcb26999702f407cf","amount":"5266666"},{"index":"2460703","validator_index":"852660","address":"0x778f5f13c4be78a3a4d7141bcb26999702f407cf","amount":"5239026"},{"index":"2460704","validator_index":"852661","address":"0x778f5f13c4be78a3a4d7141bcb26999702f407cf","amount":"5273516"},{"index":"2460705","validator_index":"852662","address":"0x778f5f13c4be78a3a4d7141bcb26999702f407cf","amount":"5260842"},{"index":"2460706","validator_index":"852663","address":"0x778f5f13c4be78a3a4d7141bcb26999702f407cf","amount":"5238925"},{"index":"2460707","validator_index":"852664","address":"0x778f5f13c4be78a3a4d7141bcb26999702f407cf","amount":"5253956"},{"index":"2460708","validator_index":"852665","address":"0x778f5f13c4be78a3a4d7141bcb26999702f407cf","amount":"5284374"},{"index":"2460709","validator_index":"852666","address":"0x778f5f13c4be78a3a4d7141bcb26999702f407cf","amount":"5276798"},{"index":"2460710","validator_index":"852667","address":"0x778f5f13c4be78a3a4d7141bcb26999702f407cf","amount":"5239682"},{"index":"2460711","validator_index":"852668","address":"0x778f5f13c4be78a3a4d7141bcb26999702f407cf","amount":"5261544"},{"index":"2460712","validator_index":"852669","address":"0x778f5f13c4be78a3a4d7141bcb26999702f407cf","amount":"5247034"},{"index":"2460713","validator_index":"852670","address":"0x778f5f13c4be78a3a4d7141bcb26999702f407cf","amount":"5256750"},{"index":"2460714","validator_index":"852671","address":"0x778f5f13c4be78a3a4d7141bcb26999702f407cf","amount":"5261929"},{"index":"2460715","validator_index":"852672","address":"0x778f5f13c4be78a3a4d7141bcb26999702f407cf","amount":"5243188"}]}"#;
let event: Event = serde_json::from_str(s).unwrap();
let input = serde_json::from_str::<serde_json::Value>(s).unwrap();
let json = serde_json::to_value(event).unwrap();
assert_eq!(input, json);
}
}

View File

@ -15,7 +15,7 @@ mod syncing;
pub mod trace;
mod transaction;
pub mod txpool;
mod withdrawal;
pub mod withdrawal;
mod work;
pub use account::*;

View File

@ -1,7 +1,11 @@
//! Withdrawal type and serde helpers.
use alloy_primitives::{Address, U256};
use alloy_rlp::RlpEncodable;
use reth_primitives::{constants::GWEI_TO_WEI, serde_helper::u64_hex};
use serde::{Deserialize, Serialize};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use serde_with::{serde_as, DeserializeAs, DisplayFromStr, SerializeAs};
/// Withdrawal represents a validator withdrawal from the consensus layer.
#[derive(Debug, Clone, PartialEq, Eq, Default, Hash, RlpEncodable, Serialize, Deserialize)]
pub struct Withdrawal {
@ -25,6 +29,73 @@ impl Withdrawal {
}
}
/// Same as [Withdrawal] but respects the Beacon API format which uses snake-case and quoted
/// decimals.
#[serde_as]
#[derive(Serialize, Deserialize)]
pub(crate) struct BeaconAPIWithdrawal {
#[serde_as(as = "DisplayFromStr")]
index: u64,
#[serde_as(as = "DisplayFromStr")]
validator_index: u64,
address: Address,
#[serde_as(as = "DisplayFromStr")]
amount: u64,
}
impl SerializeAs<Withdrawal> for BeaconAPIWithdrawal {
fn serialize_as<S>(source: &Withdrawal, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
beacon_api_withdrawals::serialize(source, serializer)
}
}
impl<'de> DeserializeAs<'de, Withdrawal> for BeaconAPIWithdrawal {
fn deserialize_as<D>(deserializer: D) -> Result<Withdrawal, D::Error>
where
D: Deserializer<'de>,
{
beacon_api_withdrawals::deserialize(deserializer)
}
}
/// A helper serde module to convert from/to the Beacon API which uses quoted decimals rather than
/// big-endian hex.
pub mod beacon_api_withdrawals {
use super::*;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
/// Serialize the payload attributes for the beacon API.
pub fn serialize<S>(payload_attributes: &Withdrawal, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let withdrawal = BeaconAPIWithdrawal {
index: payload_attributes.index,
validator_index: payload_attributes.validator_index,
address: payload_attributes.address,
amount: payload_attributes.amount,
};
withdrawal.serialize(serializer)
}
/// Deserialize the payload attributes for the beacon API.
pub fn deserialize<'de, D>(deserializer: D) -> Result<Withdrawal, D::Error>
where
D: Deserializer<'de>,
{
let withdrawal = BeaconAPIWithdrawal::deserialize(deserializer)?;
Ok(Withdrawal {
index: withdrawal.index,
validator_index: withdrawal.validator_index,
address: withdrawal.address,
amount: withdrawal.amount,
})
}
}
#[cfg(test)]
mod tests {
use super::*;

View File

@ -0,0 +1,16 @@
[package]
name = "beacon-api-sse"
version = "0.0.0"
publish = false
edition.workspace = true
license.workspace = true
[dependencies]
reth.workspace = true
eyre.workspace = true
clap.workspace = true
tracing.workspace = true
futures-util.workspace = true
tokio = { workspace = true, features = ["time"] }
mev-share-sse = "0.1.5"

View File

@ -0,0 +1,113 @@
//! Example of how to subscribe to beacon chain events via SSE.
//!
//! See also [ethereum-beacon-API eventstream](https://ethereum.github.io/beacon-APIs/#/Events/eventstream)
//!
//! Run with
//!
//! ```not_rust
//! cargo run -p beacon-api-sse -- node
//! ```
//!
//! This launches a regular reth instance and subscribes to payload attributes event stream.
//!
//! **NOTE**: This expects that the CL client is running an http server on `localhost:5052` and is
//! configured to emit payload attributes events.
//!
//! See lighthouse beacon Node API: <https://lighthouse-book.sigmaprime.io/api-bn.html#beacon-node-api>
use clap::Parser;
use futures_util::stream::StreamExt;
use mev_share_sse::{client::EventStream, EventClient};
use reth::{
cli::{
components::RethNodeComponents,
ext::{RethCliExt, RethNodeCommandConfig},
Cli,
},
rpc::types::engine::beacon_api::events::PayloadAttributesEvent,
tasks::TaskSpawner,
};
use std::net::{IpAddr, Ipv4Addr};
use tracing::{info, warn};
fn main() {
Cli::<BeaconEventsExt>::parse().run().unwrap();
}
/// The type that tells the reth CLI what extensions to use
#[derive(Debug, Default)]
#[non_exhaustive]
struct BeaconEventsExt;
impl RethCliExt for BeaconEventsExt {
/// This tells the reth CLI to install additional CLI arguments
type Node = BeaconEventsConfig;
}
/// Our custom cli args extension that adds one flag to reth default CLI.
#[derive(Debug, Clone, clap::Parser)]
struct BeaconEventsConfig {
/// Beacon Node http server address
#[arg(long = "cl.addr", default_value_t = IpAddr::V4(Ipv4Addr::LOCALHOST))]
pub cl_addr: IpAddr,
/// Beacon Node http server port to listen on
#[arg(long = "cl.port", default_value_t = 5052)]
pub cl_port: u16,
}
impl BeaconEventsConfig {
/// Returns the http url of the beacon node
pub fn http_base_url(&self) -> String {
format!("http://{}:{}", self.cl_addr, self.cl_port)
}
/// Returns the URL to the events endpoint
pub fn events_url(&self) -> String {
format!("{}/eth/v1/events", self.http_base_url())
}
/// Service that subscribes to beacon chain payload attributes events
async fn run(self) {
let client = EventClient::default();
let mut subscription = self.new_payload_attributes_subscription(&client).await;
while let Some(event) = subscription.next().await {
info!("Received payload attributes: {:?}", event);
}
}
// It can take a bit until the CL endpoint is live so we retry a few times
async fn new_payload_attributes_subscription(
&self,
client: &EventClient,
) -> EventStream<PayloadAttributesEvent> {
let payloads_url = format!("{}?topics=payload_attributes", self.events_url());
loop {
match client.subscribe(&payloads_url).await {
Ok(subscription) => return subscription,
Err(err) => {
warn!("Failed to subscribe to payload attributes events: {:?}\nRetrying in 5 seconds...", err);
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
}
}
}
}
}
impl RethNodeCommandConfig for BeaconEventsConfig {
fn on_node_started<Reth: RethNodeComponents>(&mut self, components: &Reth) -> eyre::Result<()> {
components.task_executor().spawn(Box::pin(self.clone().run()));
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_config() {
let args = BeaconEventsConfig::try_parse_from(["reth"]);
assert!(args.is_ok());
}
}