diff --git a/Cargo.lock b/Cargo.lock index 18960458f..bcb9d2384 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index f177c8147..fb1f37104 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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"] diff --git a/crates/rpc/rpc-types/Cargo.toml b/crates/rpc/rpc-types/Cargo.toml index 68a879c3c..98d1c9503 100644 --- a/crates/rpc/rpc-types/Cargo.toml +++ b/crates/rpc/rpc-types/Cargo.toml @@ -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"] } diff --git a/crates/rpc/rpc-types/src/eth/engine/beacon_api/events.rs b/crates/rpc/rpc-types/src/eth/engine/beacon_api/events.rs new file mode 100644 index 000000000..fca3fb876 --- /dev/null +++ b/crates/rpc/rpc-types/src/eth/engine/beacon_api/events.rs @@ -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` 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::(s).unwrap(); + let input = serde_json::from_str::(s).unwrap(); + let json = serde_json::to_value(event).unwrap(); + assert_eq!(input, json); + } +} diff --git a/crates/rpc/rpc-types/src/eth/engine/beacon_api/mod.rs b/crates/rpc/rpc-types/src/eth/engine/beacon_api/mod.rs new file mode 100644 index 000000000..0a3764a21 --- /dev/null +++ b/crates/rpc/rpc-types/src/eth/engine/beacon_api/mod.rs @@ -0,0 +1,2 @@ +/// Beacon API events support. +pub mod events; diff --git a/crates/rpc/rpc-types/src/eth/engine/mod.rs b/crates/rpc/rpc-types/src/eth/engine/mod.rs index 86df72fb5..ead7e56a7 100644 --- a/crates/rpc/rpc-types/src/eth/engine/mod.rs +++ b/crates/rpc/rpc-types/src/eth/engine/mod.rs @@ -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", diff --git a/crates/rpc/rpc-types/src/eth/engine/payload.rs b/crates/rpc/rpc-types/src/eth/engine/payload.rs index 2e8eb5792..bfb0a6b07 100644 --- a/crates/rpc/rpc-types/src/eth/engine/payload.rs +++ b/crates/rpc/rpc-types/src/eth/engine/payload.rs @@ -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>; @@ -411,6 +413,63 @@ pub struct PayloadAttributes { pub parent_beacon_block_root: Option, } +#[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>")] + withdrawals: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + parent_beacon_block_root: Option, +} +/// 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( + payload_attributes: &PayloadAttributes, + serializer: S, + ) -> Result + 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 + 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::(s).unwrap(); + let json = serde_json::to_value(event).unwrap(); + assert_eq!(input, json); + } } diff --git a/crates/rpc/rpc-types/src/eth/mod.rs b/crates/rpc/rpc-types/src/eth/mod.rs index c966b7495..0e02a8e50 100644 --- a/crates/rpc/rpc-types/src/eth/mod.rs +++ b/crates/rpc/rpc-types/src/eth/mod.rs @@ -15,7 +15,7 @@ mod syncing; pub mod trace; mod transaction; pub mod txpool; -mod withdrawal; +pub mod withdrawal; mod work; pub use account::*; diff --git a/crates/rpc/rpc-types/src/eth/withdrawal.rs b/crates/rpc/rpc-types/src/eth/withdrawal.rs index 2e03aded4..d989794d8 100644 --- a/crates/rpc/rpc-types/src/eth/withdrawal.rs +++ b/crates/rpc/rpc-types/src/eth/withdrawal.rs @@ -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 for BeaconAPIWithdrawal { + fn serialize_as(source: &Withdrawal, serializer: S) -> Result + where + S: Serializer, + { + beacon_api_withdrawals::serialize(source, serializer) + } +} + +impl<'de> DeserializeAs<'de, Withdrawal> for BeaconAPIWithdrawal { + fn deserialize_as(deserializer: D) -> Result + 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(payload_attributes: &Withdrawal, serializer: S) -> Result + 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 + 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::*; diff --git a/examples/beacon-api-sse/Cargo.toml b/examples/beacon-api-sse/Cargo.toml new file mode 100644 index 000000000..8f2f2c611 --- /dev/null +++ b/examples/beacon-api-sse/Cargo.toml @@ -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" \ No newline at end of file diff --git a/examples/beacon-api-sse/src/main.rs b/examples/beacon-api-sse/src/main.rs new file mode 100644 index 000000000..04f0be111 --- /dev/null +++ b/examples/beacon-api-sse/src/main.rs @@ -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: +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::::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 { + 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(&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()); + } +}