From d3aedc028dfa5cc894190dff7b72000a9e3316ad Mon Sep 17 00:00:00 2001 From: ltitanb Date: Fri, 13 Dec 2024 15:16:46 +0000 Subject: [PATCH] lido mux --- Cargo.lock | 293 +++++++++++++++++++++- Cargo.toml | 1 + bin/pbs.rs | 4 +- bin/signer.rs | 2 +- config.example.toml | 7 +- configs/{pbs-mux.toml => pbs_mux.toml} | 11 + crates/cli/src/docker_init.rs | 4 +- crates/cli/src/lib.rs | 2 +- crates/common/src/abi/LidoNORegistry.json | 56 +++++ crates/common/src/config/mod.rs | 6 +- crates/common/src/config/mux.rs | 183 +++++++++++++- crates/common/src/config/pbs.rs | 41 ++- crates/common/src/types.rs | 21 ++ crates/common/src/utils.rs | 14 +- examples/status_api/src/main.rs | 2 +- tests/tests/config.rs | 6 +- 16 files changed, 604 insertions(+), 49 deletions(-) rename configs/{pbs-mux.toml => pbs_mux.toml} (73%) create mode 100644 crates/common/src/abi/LidoNORegistry.json diff --git a/Cargo.lock b/Cargo.lock index e38f7817..2ae77fa3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -76,16 +76,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "689e271a72a5c0b05bfdf41c9d0424f11e9df721385dc5bd9045a51f9ea3313b" dependencies = [ "alloy-consensus", + "alloy-contract", "alloy-core", "alloy-eips", "alloy-genesis", "alloy-network", "alloy-provider", + "alloy-pubsub", "alloy-rpc-client", "alloy-rpc-types", "alloy-serde", + "alloy-signer", + "alloy-signer-local", "alloy-transport", "alloy-transport-http", + "alloy-transport-ipc", + "alloy-transport-ws", ] [[package]] @@ -112,6 +118,7 @@ dependencies = [ "auto_impl", "c-kzg", "derive_more", + "k256", "serde", ] @@ -129,6 +136,27 @@ dependencies = [ "serde", ] +[[package]] +name = "alloy-contract" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3510769905590b8991a8e63a5e0ab4aa72cf07a13ab5fbe23f12f4454d161da" +dependencies = [ + "alloy-dyn-abi", + "alloy-json-abi", + "alloy-network", + "alloy-network-primitives", + "alloy-primitives", + "alloy-provider", + "alloy-pubsub", + "alloy-rpc-types-eth", + "alloy-sol-types", + "alloy-transport", + "futures", + "futures-util", + "thiserror 2.0.6", +] + [[package]] name = "alloy-core" version = "0.8.15" @@ -179,6 +207,7 @@ dependencies = [ "alloy-primitives", "alloy-rlp", "derive_more", + "k256", "serde", ] @@ -320,10 +349,13 @@ dependencies = [ "alloy-network", "alloy-network-primitives", "alloy-primitives", + "alloy-pubsub", "alloy-rpc-client", "alloy-rpc-types-eth", "alloy-transport", "alloy-transport-http", + "alloy-transport-ipc", + "alloy-transport-ws", "async-stream", "async-trait", "auto_impl", @@ -344,6 +376,25 @@ dependencies = [ "wasmtimer", ] +[[package]] +name = "alloy-pubsub" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b073afa409698d1b9a30522565815f3bf7010e5b47b997cf399209e6110df097" +dependencies = [ + "alloy-json-rpc", + "alloy-primitives", + "alloy-transport", + "bimap", + "futures", + "serde", + "serde_json", + "tokio", + "tokio-stream", + "tower 0.5.0", + "tracing", +] + [[package]] name = "alloy-rlp" version = "0.3.10" @@ -374,8 +425,11 @@ checksum = "5c6a0bd0ce5660ac48e4f3bb0c7c5c3a94db287a0be94971599d83928476cbcd" dependencies = [ "alloy-json-rpc", "alloy-primitives", + "alloy-pubsub", "alloy-transport", "alloy-transport-http", + "alloy-transport-ipc", + "alloy-transport-ws", "futures", "pin-project", "reqwest", @@ -494,6 +548,22 @@ dependencies = [ "thiserror 2.0.6", ] +[[package]] +name = "alloy-signer-local" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2184dab8c9493ab3e1c9f6bd3bdb563ed322b79023d81531935e84a4fdf7cf1" +dependencies = [ + "alloy-consensus", + "alloy-network", + "alloy-primitives", + "alloy-signer", + "async-trait", + "k256", + "rand", + "thiserror 2.0.6", +] + [[package]] name = "alloy-sol-macro" version = "0.8.15" @@ -514,6 +584,7 @@ version = "0.8.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6bf7ed1574b699f48bf17caab4e6e54c6d12bc3c006ab33d58b1e227c1c3559f" dependencies = [ + "alloy-json-abi", "alloy-sol-macro-input", "const-hex", "heck 0.5.0", @@ -532,11 +603,13 @@ version = "0.8.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c02997ccef5f34f9c099277d4145f183b422938ed5322dc57a089fe9b9ad9ee" dependencies = [ + "alloy-json-abi", "const-hex", "dunce", "heck 0.5.0", "proc-macro2", "quote", + "serde_json", "syn 2.0.90", "syn-solidity", ] @@ -599,6 +672,43 @@ dependencies = [ "url", ] +[[package]] +name = "alloy-transport-ipc" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0577a1f67ce70ece3f2b27cf1011da7222ef0a5701f7dcb558e5356278eeb531" +dependencies = [ + "alloy-json-rpc", + "alloy-pubsub", + "alloy-transport", + "bytes", + "futures", + "interprocess", + "pin-project", + "serde_json", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "alloy-transport-ws" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ca46272d17f9647fdb56080ed26c72b3ea5078416831130f5ed46f3b4be0ed6" +dependencies = [ + "alloy-pubsub", + "alloy-transport", + "futures", + "http", + "rustls", + "serde_json", + "tokio", + "tokio-tungstenite", + "tracing", + "ws_stream_wasm", +] + [[package]] name = "alloy-trie" version = "0.7.6" @@ -839,6 +949,17 @@ dependencies = [ "syn 2.0.90", ] +[[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" @@ -1550,9 +1671,9 @@ dependencies = [ [[package]] name = "crypto-mac" -version = "0.11.1" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1d1a86f49236c215f271d40892d5fc950490551400b02ef360692c29815c714" +checksum = "25fab6889090c8133f3deb8f73ba3c65a7f456f66436fc012a1b1e272b1e103e" dependencies = [ "generic-array", "subtle", @@ -1654,6 +1775,12 @@ dependencies = [ "parking_lot_core", ] +[[package]] +name = "data-encoding" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" + [[package]] name = "der" version = "0.7.9" @@ -1780,6 +1907,12 @@ dependencies = [ "serde_yaml", ] +[[package]] +name = "doctest-file" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac81fa3e28d21450aa4d2ac065992ba96a1d7303efbce51a95f4fd175b67562" + [[package]] name = "dotenvy" version = "0.15.7" @@ -2560,6 +2693,21 @@ dependencies = [ "generic-array", ] +[[package]] +name = "interprocess" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "894148491d817cb36b6f778017b8ac46b17408d522dd90f539d677ea938362eb" +dependencies = [ + "doctest-file", + "futures-core", + "libc", + "recvmsg", + "tokio", + "widestring", + "windows-sys 0.52.0", +] + [[package]] name = "ipnet" version = "2.9.0" @@ -3040,6 +3188,16 @@ dependencies = [ "ucd-trie", ] +[[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 = "pin-project" version = "1.1.5" @@ -3255,6 +3413,12 @@ dependencies = [ "rand_core", ] +[[package]] +name = "recvmsg" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3edd4d5d42c92f0a659926464d4cce56b562761267ecf0f469d85b7de384175" + [[package]] name = "redox_syscall" version = "0.5.1" @@ -3481,6 +3645,20 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rustls" +version = "0.23.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5065c3f250cbd332cd894be57c40fa52387247659b14a2d6041d121547903b1b" +dependencies = [ + "once_cell", + "ring 0.17.8", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + [[package]] name = "rustls-pemfile" version = "2.1.2" @@ -3493,9 +3671,20 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.7.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d" +checksum = "16f1201b3c9a7ee8039bcadc17b7e605e2945b27eee7631788c1bd2b0643674b" + +[[package]] +name = "rustls-webpki" +version = "0.102.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +dependencies = [ + "ring 0.17.8", + "rustls-pki-types", + "untrusted 0.9.0", +] [[package]] name = "rustversion" @@ -3629,6 +3818,12 @@ dependencies = [ "pest", ] +[[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.202" @@ -3946,9 +4141,9 @@ dependencies = [ [[package]] name = "subtle" -version = "2.4.1" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" @@ -4189,6 +4384,16 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-rustls" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6d0975eaace0cf0fcadee4e4aaa5da15b5c079146f2cffb67c113be122bf37" +dependencies = [ + "rustls", + "tokio", +] + [[package]] name = "tokio-stream" version = "0.1.15" @@ -4201,6 +4406,22 @@ dependencies = [ "tokio-util", ] +[[package]] +name = "tokio-tungstenite" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edc5f74e248dc973e0dbb7b74c7e0d6fcc301c694ff50049504004ef4d0cdcd9" +dependencies = [ + "futures-util", + "log", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tungstenite", + "webpki-roots", +] + [[package]] name = "tokio-util" version = "0.7.11" @@ -4438,6 +4659,26 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "tungstenite" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18e5b8366ee7a95b16d32197d0b2604b43a0be89dc5fac9f8e96ccafbaedda8a" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "rand", + "rustls", + "rustls-pki-types", + "sha1", + "thiserror 1.0.61", + "utf-8", +] + [[package]] name = "typenum" version = "1.17.0" @@ -4531,6 +4772,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 = "utf8parse" version = "0.2.1" @@ -4703,6 +4950,21 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki-roots" +version = "0.26.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d642ff16b7e79272ae451b7322067cdc17cadf68c23264be9d94a32319efe7e" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "widestring" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7219d36b6eac893fa81e84ebe06485e7dcbb616177469b142df14f1f4deb1311" + [[package]] name = "winapi" version = "0.3.9" @@ -4892,6 +5154,25 @@ dependencies = [ "windows-sys 0.48.0", ] +[[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", + "thiserror 1.0.61", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "wyz" version = "0.5.1" diff --git a/Cargo.toml b/Cargo.toml index 495d4afc..18d57ad6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,7 @@ cb-signer = { path = "crates/signer" } # ethereum alloy = { version = "0.8.0", features = [ + "full", "rpc-types-beacon", "serde", "ssz", diff --git a/bin/pbs.rs b/bin/pbs.rs index b8039bec..1514cf10 100644 --- a/bin/pbs.rs +++ b/bin/pbs.rs @@ -14,10 +14,10 @@ async fn main() -> Result<()> { if std::env::var_os("RUST_BACKTRACE").is_none() { std::env::set_var("RUST_BACKTRACE", "1"); } - - let pbs_config = load_pbs_config()?; let _guard = initialize_pbs_tracing_log(); + let pbs_config = load_pbs_config().await?; + let state = PbsState::new(pbs_config); PbsService::init_metrics()?; let server = PbsService::run::<_, DefaultBuilderApi>(state); diff --git a/bin/signer.rs b/bin/signer.rs index cd9480ad..8ae24046 100644 --- a/bin/signer.rs +++ b/bin/signer.rs @@ -14,9 +14,9 @@ async fn main() -> Result<()> { if std::env::var_os("RUST_BACKTRACE").is_none() { std::env::set_var("RUST_BACKTRACE", "1"); } + let _guard = initialize_tracing_log(SIGNER_MODULE_NAME); let config = StartSignerConfig::load_from_env()?; - let _guard = initialize_tracing_log(SIGNER_MODULE_NAME); let server = SigningService::run(config); tokio::select! { diff --git a/config.example.toml b/config.example.toml index 66e3d47e..45ad5365 100644 --- a/config.example.toml +++ b/config.example.toml @@ -57,7 +57,7 @@ late_in_slot_time_ms = 2000 extra_validation_enabled = false # Execution Layer RPC url to use for extra validation # OPTIONAL -rpc_url = "http://abc.xyz" +rpc_url = "https://ethereum-holesky-rpc.publicnode.com" # The PBS module needs one or more [[relays]] as defined below. [[relays]] @@ -111,9 +111,12 @@ validator_pubkeys = [ "0x80c7f782b2467c5898c5516a8b6595d75623960b4afc4f71ee07d40985d20e117ba35e7cd352a3e75fb85a8668a3b745", "0xa119589bb33ef52acbb8116832bec2b58fca590fe5c85eac5d3230b44d5bc09fe73ccd21f88eab31d6de16194d17782e", ] -# Path to a file containing a list of validator pubkeys +# Path to a file containing a list of validator pubkeys or details of a registry to load keys from. +# Supported registries: +# - Lido: NodeOperatorsRegistry # OPTIONAL loader = "./mux_keys.example.json" +# loader = { registry = "lido", node_operator_id = 8 } timeout_get_header_ms = 900 late_in_slot_time_ms = 1500 # For each mux, one or more [[mux.relays]] can be defined, which will be used for the matching validator pubkeys diff --git a/configs/pbs-mux.toml b/configs/pbs_mux.toml similarity index 73% rename from configs/pbs-mux.toml rename to configs/pbs_mux.toml index 0bb139f4..7cb5662e 100644 --- a/configs/pbs-mux.toml +++ b/configs/pbs_mux.toml @@ -6,6 +6,7 @@ chain = "Holesky" port = 18550 timeout_get_header_ms = 950 late_in_slot_time_ms = 2000 +rpc_url = "https://ethereum-holesky-rpc.publicnode.com" # Used for all validators except the ones in the mux [[relays]] @@ -22,8 +23,18 @@ loader = "./mux_keys.example.json" timeout_get_header_ms = 900 late_in_slot_time_ms = 1500 + [[mux.relays]] id = "relay-2" url = "http://0xa119589bb33ef52acbb8116832bec2b58fca590fe5c85eac5d3230b44d5bc09fe73ccd21f88eab31d6de16194d17782e@def.xyz" enable_timing_games = true target_first_request_ms = 200 + + +[[mux]] +id = "lido-mux" +loader = { registry = "lido", node_operator_id = 8 } + +[[mux.relays]] +id = "relay-3" +url = "http://0x80c7f782b2467c5898c5516a8b6595d75623960b4afc4f71ee07d40985d20e117ba35e7cd352a3e75fb85a8668a3b745@fgh.xyz" diff --git a/crates/cli/src/docker_init.rs b/crates/cli/src/docker_init.rs index 0d2b217e..e3ae71ef 100644 --- a/crates/cli/src/docker_init.rs +++ b/crates/cli/src/docker_init.rs @@ -39,9 +39,11 @@ const SIGNER_NETWORK: &str = "signer_network"; /// Builds the docker compose file for the Commit-Boost services // TODO: do more validation for paths, images, etc -pub fn handle_docker_init(config_path: String, output_dir: String) -> Result<()> { +pub async fn handle_docker_init(config_path: String, output_dir: String) -> Result<()> { println!("Initializing Commit-Boost with config file: {}", config_path); let cb_config = CommitBoostConfig::from_file(&config_path)?; + cb_config.validate().await?; + let chain_spec_path = CommitBoostConfig::chain_spec_file(&config_path); let metrics_enabled = cb_config.metrics.is_some(); diff --git a/crates/cli/src/lib.rs b/crates/cli/src/lib.rs index 9e382af2..3b80ff9e 100644 --- a/crates/cli/src/lib.rs +++ b/crates/cli/src/lib.rs @@ -73,7 +73,7 @@ impl Args { match self.cmd { Command::Init { config_path, output_path } => { - docker_init::handle_docker_init(config_path, output_path) + docker_init::handle_docker_init(config_path, output_path).await } Command::Start { compose_path, env_path } => { diff --git a/crates/common/src/abi/LidoNORegistry.json b/crates/common/src/abi/LidoNORegistry.json new file mode 100644 index 00000000..296a26bd --- /dev/null +++ b/crates/common/src/abi/LidoNORegistry.json @@ -0,0 +1,56 @@ +[ + { + "constant": true, + "inputs": [ + { + "name": "_nodeOperatorId", + "type": "uint256" + } + ], + "name": "getTotalSigningKeyCount", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_nodeOperatorId", + "type": "uint256" + }, + { + "name": "_offset", + "type": "uint256" + }, + { + "name": "_limit", + "type": "uint256" + } + ], + "name": "getSigningKeys", + "outputs": [ + { + "name": "pubkeys", + "type": "bytes" + }, + { + "name": "signatures", + "type": "bytes" + }, + { + "name": "used", + "type": "bool[]" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + } +] \ No newline at end of file diff --git a/crates/common/src/config/mod.rs b/crates/common/src/config/mod.rs index 70acfadd..b097d02a 100644 --- a/crates/common/src/config/mod.rs +++ b/crates/common/src/config/mod.rs @@ -38,14 +38,13 @@ pub struct CommitBoostConfig { impl CommitBoostConfig { /// Validate config - pub fn validate(&self) -> Result<()> { - self.pbs.pbs_config.validate()?; + pub async fn validate(&self) -> Result<()> { + self.pbs.pbs_config.validate(self.chain).await?; Ok(()) } pub fn from_file(path: &str) -> Result { let config: Self = load_from_file(path)?; - config.validate()?; Ok(config) } @@ -83,7 +82,6 @@ impl CommitBoostConfig { logs: helper_config.logs, }; - config.validate()?; Ok(config) } diff --git a/crates/common/src/config/mux.rs b/crates/common/src/config/mux.rs index 87ada0a8..6ef0dd76 100644 --- a/crates/common/src/config/mux.rs +++ b/crates/common/src/config/mux.rs @@ -4,12 +4,19 @@ use std::{ sync::Arc, }; -use alloy::rpc::types::beacon::BlsPublicKey; +use alloy::{ + primitives::{address, Address, U256}, + providers::ProviderBuilder, + rpc::types::beacon::BlsPublicKey, + sol, +}; use eyre::{bail, ensure, Context}; use serde::{Deserialize, Serialize}; +use tracing::{debug, info}; +use url::Url; use super::{load_optional_env_var, PbsConfig, RelayConfig, MUX_PATH_ENV}; -use crate::pbs::RelayClient; +use crate::{pbs::RelayClient, types::Chain}; #[derive(Debug, Deserialize, Serialize)] pub struct PbsMuxes { @@ -26,17 +33,26 @@ pub struct RuntimeMuxConfig { } impl PbsMuxes { - pub fn validate_and_fill( + pub async fn validate_and_fill( self, + chain: Chain, default_pbs: &PbsConfig, ) -> eyre::Result> { let mut muxes = self.muxes; for mux in muxes.iter_mut() { + ensure!(!mux.relays.is_empty(), "mux config {} must have at least one relay", mux.id); + if let Some(loader) = &mux.loader { - let extra_keys = loader.load(&mux.id)?; + let extra_keys = loader.load(&mux.id, chain, default_pbs.rpc_url.clone()).await?; mux.validator_pubkeys.extend(extra_keys); } + + ensure!( + !mux.validator_pubkeys.is_empty(), + "mux config {} must have at least one validator pubkey", + mux.id + ); } // check that validator pubkeys are in disjoint sets @@ -52,11 +68,11 @@ impl PbsMuxes { let mut configs = HashMap::new(); // fill the configs using the default pbs config and relay entries for mux in muxes { - ensure!(!mux.relays.is_empty(), "mux config {} must have at least one relay", mux.id); - ensure!( - !mux.validator_pubkeys.is_empty(), - "mux config {} must have at least one validator pubkey", - mux.id + info!( + id = mux.id, + keys = mux.validator_pubkeys.len(), + relays = mux.relays.len(), + "using mux" ); let mut relay_clients = Vec::with_capacity(mux.relays.len()); @@ -102,16 +118,18 @@ pub struct MuxConfig { } impl MuxConfig { - /// Returns the env, actual path, and internal path to use for the loader + /// Returns the env, actual path, and internal path to use for the file + /// loader pub fn loader_env(&self) -> Option<(String, String, String)> { - self.loader.as_ref().map(|loader| match loader { + self.loader.as_ref().and_then(|loader| match loader { MuxKeysLoader::File(path_buf) => { let path = path_buf.to_str().unwrap_or_else(|| panic!("invalid path: {:?}", path_buf)); let internal_path = get_mux_path(&self.id); - (get_mux_env(&self.id), path.to_owned(), internal_path) + Some((get_mux_env(&self.id), path.to_owned(), internal_path)) } + MuxKeysLoader::Registry { .. } => None, }) } } @@ -121,10 +139,25 @@ impl MuxConfig { pub enum MuxKeysLoader { /// A file containing a list of validator pubkeys File(PathBuf), + Registry { + registry: NORegistry, + node_operator_id: u64, + }, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub enum NORegistry { + #[serde(alias = "lido")] + Lido, } impl MuxKeysLoader { - pub fn load(&self, mux_id: &str) -> eyre::Result> { + pub async fn load( + &self, + mux_id: &str, + chain: Chain, + rpc_url: Option, + ) -> eyre::Result> { match self { Self::File(config_path) => { // First try loading from env @@ -134,6 +167,16 @@ impl MuxKeysLoader { let file = load_file(path)?; serde_json::from_str(&file).wrap_err("failed to parse mux keys file") } + + Self::Registry { registry, node_operator_id } => match registry { + NORegistry::Lido => { + let Some(rpc_url) = rpc_url else { + bail!("Lido registry requires RPC URL to be set in the PBS config"); + }; + + fetch_lido_registry_keys(rpc_url, chain, U256::from(*node_operator_id)).await + } + }, } } } @@ -151,3 +194,117 @@ fn get_mux_env(mux_id: &str) -> String { fn get_mux_path(mux_id: &str) -> String { format!("/{mux_id}-mux_keys.json") } + +sol! { + #[allow(missing_docs)] + #[sol(rpc)] + LidoRegistry, + "src/abi/LidoNORegistry.json" +} + +fn lido_registry_address(chain: Chain) -> eyre::Result
{ + match chain { + Chain::Mainnet => Ok(address!("55032650b14df07b85bF18A3a3eC8E0Af2e028d5")), + Chain::Holesky => Ok(address!("595F64Ddc3856a3b5Ff4f4CC1d1fb4B46cFd2bAC")), + Chain::Sepolia => Ok(address!("33d6E15047E8644F8DDf5CD05d202dfE587DA6E3")), + _ => bail!("Lido registry not supported for chain: {chain:?}"), + } +} + +async fn fetch_lido_registry_keys( + rpc_url: Url, + chain: Chain, + node_operator_id: U256, +) -> eyre::Result> { + debug!( + "loading operator keys from Lido registry: chain={:?}, node_operator_id={}", + chain, node_operator_id + ); + + let provider = ProviderBuilder::new().on_http(rpc_url); + let registry_address = lido_registry_address(chain)?; + let registry = LidoRegistry::new(registry_address, provider); + + let total_keys = + registry.getTotalSigningKeyCount(node_operator_id).call().await?._0.try_into()?; + + debug!("fetching {total_keys} total keys"); + + const CALL_BATCH_SIZE: u64 = 250u64; + const BLS_PK_LEN: usize = BlsPublicKey::len_bytes(); + + let mut keys = vec![]; + let mut offset = 0; + + while offset < total_keys { + let limit = CALL_BATCH_SIZE.min(total_keys - offset); + + let pubkeys = registry + .getSigningKeys(node_operator_id, U256::from(offset), U256::from(limit)) + .call() + .await? + .pubkeys; + + ensure!( + pubkeys.len() % BLS_PK_LEN == 0, + "unexpected number of keys in batch, expected multiple of {BLS_PK_LEN}, got {}", + pubkeys.len() + ); + + for chunk in pubkeys.chunks(BLS_PK_LEN) { + keys.push(BlsPublicKey::try_from(chunk)?); + } + + offset += CALL_BATCH_SIZE; + + if offset % 1000 == 0 { + debug!("fetched {offset} keys"); + } + } + + ensure!(keys.len() == total_keys as usize, "expected {total_keys} keys, got {}", keys.len()); + let unique: Vec<_> = keys.iter().collect::>().into_iter().collect(); + ensure!(unique.len() == keys.len(), "found duplicate keys in registry"); + + Ok(keys) +} + +#[cfg(test)] +mod tests { + use alloy::{primitives::U256, providers::ProviderBuilder}; + use url::Url; + + use super::*; + + #[tokio::test] + async fn test_lido_registry_address() -> eyre::Result<()> { + let url = Url::parse("https://ethereum-rpc.publicnode.com")?; + let provider = ProviderBuilder::new().on_http(url); + + let registry = + LidoRegistry::new(address!("55032650b14df07b85bF18A3a3eC8E0Af2e028d5"), provider); + + const LIMIT: usize = 3; + let node_operator_id = U256::from(1); + + let total_keys: u64 = + registry.getTotalSigningKeyCount(node_operator_id).call().await?._0.try_into()?; + + assert!(total_keys > LIMIT as u64); + + let pubkeys = registry + .getSigningKeys(node_operator_id, U256::ZERO, U256::from(LIMIT)) + .call() + .await? + .pubkeys; + + let mut vec = vec![]; + for chunk in pubkeys.chunks(BlsPublicKey::len_bytes()) { + vec.push(BlsPublicKey::try_from(chunk)?); + } + + assert_eq!(vec.len(), LIMIT); + + Ok(()) + } +} diff --git a/crates/common/src/config/pbs.rs b/crates/common/src/config/pbs.rs index d5075848..1922552f 100644 --- a/crates/common/src/config/pbs.rs +++ b/crates/common/src/config/pbs.rs @@ -8,6 +8,7 @@ use std::{ use alloy::{ primitives::{utils::format_ether, U256}, + providers::{Provider, ProviderBuilder}, rpc::types::beacon::BlsPublicKey, }; use eyre::{ensure, Result}; @@ -101,7 +102,7 @@ pub struct PbsConfig { impl PbsConfig { /// Validate PBS config parameters - pub fn validate(&self) -> Result<()> { + pub async fn validate(&self, chain: Chain) -> Result<()> { // timeouts must be positive ensure!(self.timeout_get_header_ms > 0, "timeout_get_header_ms must be greater than 0"); ensure!(self.timeout_get_payload_ms > 0, "timeout_get_payload_ms must be greater than 0"); @@ -128,6 +129,21 @@ impl PbsConfig { ); } + if let Some(rpc_url) = &self.rpc_url { + // TODO: remove this once we support chain ids for custom chains + if !matches!(chain, Chain::Custom { .. }) { + let provider = ProviderBuilder::new().on_http(rpc_url.clone()); + let chain_id = provider.get_chain_id().await?; + ensure!( + chain_id == chain.id(), + "Rpc url is for the wrong chain, expected: {} ({:?}) got {}", + chain.id(), + chain, + chain_id + ); + } + } + Ok(()) } } @@ -170,9 +186,9 @@ fn default_pbs() -> String { } /// Loads the default pbs config, i.e. with no signer client or custom data -pub fn load_pbs_config() -> Result { +pub async fn load_pbs_config() -> Result { let config = CommitBoostConfig::from_env_path()?; - config.validate()?; + config.validate().await?; // use endpoint from env if set, otherwise use default host and port let endpoint = if let Some(endpoint) = load_optional_env_var(PBS_ENDPOINT_ENV) { @@ -181,8 +197,13 @@ pub fn load_pbs_config() -> Result { SocketAddr::from((config.pbs.pbs_config.host, config.pbs.pbs_config.port)) }; - let muxes = - config.muxes.map(|muxes| muxes.validate_and_fill(&config.pbs.pbs_config)).transpose()?; + let muxes = match config.muxes { + Some(muxes) => { + let mux_configs = muxes.validate_and_fill(config.chain, &config.pbs.pbs_config).await?; + Some(mux_configs) + } + None => None, + }; let relay_clients = config.relays.into_iter().map(RelayClient::new).collect::>>()?; @@ -200,7 +221,7 @@ pub fn load_pbs_config() -> Result { } /// Loads a custom pbs config, i.e. with signer client and/or custom data -pub fn load_pbs_custom_config() -> Result<(PbsModuleConfig, T)> { +pub async fn load_pbs_custom_config() -> Result<(PbsModuleConfig, T)> { #[derive(Debug, Deserialize)] struct CustomPbsConfig { #[serde(flatten)] @@ -219,7 +240,7 @@ pub fn load_pbs_custom_config() -> Result<(PbsModuleConfig, // load module config including the extra data (if any) let cb_config: StubConfig = load_file_from_env(CONFIG_ENV)?; - cb_config.pbs.static_config.pbs_config.validate()?; + cb_config.pbs.static_config.pbs_config.validate(cb_config.chain).await?; // use endpoint from env if set, otherwise use default host and port let endpoint = if let Some(endpoint) = load_optional_env_var(PBS_ENDPOINT_ENV) { @@ -232,7 +253,11 @@ pub fn load_pbs_custom_config() -> Result<(PbsModuleConfig, }; let muxes = match cb_config.muxes { - Some(muxes) => Some(muxes.validate_and_fill(&cb_config.pbs.static_config.pbs_config)?), + Some(muxes) => Some( + muxes + .validate_and_fill(cb_config.chain, &cb_config.pbs.static_config.pbs_config) + .await?, + ), None => None, }; diff --git a/crates/common/src/types.rs b/crates/common/src/types.rs index c7684520..58632dcb 100644 --- a/crates/common/src/types.rs +++ b/crates/common/src/types.rs @@ -46,6 +46,18 @@ impl std::fmt::Debug for Chain { } impl Chain { + pub fn id(&self) -> u64 { + match self { + Chain::Mainnet => KnownChain::Mainnet.id(), + Chain::Holesky => KnownChain::Holesky.id(), + Chain::Sepolia => KnownChain::Sepolia.id(), + Chain::Helder => KnownChain::Helder.id(), + Chain::Custom { .. } => { + unimplemented!("chain id is not supported on custom chains, plase file an issue") + } + } + } + pub fn builder_domain(&self) -> [u8; 32] { match self { Chain::Mainnet => KnownChain::Mainnet.builder_domain(), @@ -101,6 +113,15 @@ pub enum KnownChain { // Constants impl KnownChain { + pub fn id(&self) -> u64 { + match self { + KnownChain::Mainnet => 1, + KnownChain::Holesky => 17000, + KnownChain::Sepolia => 11155111, + KnownChain::Helder => 167000, + } + } + pub fn builder_domain(&self) -> [u8; 32] { match self { KnownChain::Mainnet => [ diff --git a/crates/common/src/utils.rs b/crates/common/src/utils.rs index a1bbfe8d..6f2a2d07 100644 --- a/crates/common/src/utils.rs +++ b/crates/common/src/utils.rs @@ -18,7 +18,7 @@ use tracing_appender::{non_blocking::WorkerGuard, rolling::Rotation}; use tracing_subscriber::{fmt::Layer, prelude::*, EnvFilter}; use crate::{ - config::{LogsSettings, LOGS_DIR_DEFAULT, PBS_MODULE_NAME}, + config::{load_optional_env_var, LogsSettings, LOGS_DIR_DEFAULT, PBS_MODULE_NAME}, pbs::HEADER_VERSION_VALUE, types::Chain, }; @@ -165,13 +165,13 @@ pub fn initialize_tracing_log(module_id: &str) -> eyre::Result { let settings = settings.unwrap_or_default(); // Log level for stdout - let stdout_log_level = match settings.log_level.parse::() { - Ok(f) => f, - Err(_) => { - eprintln!("Invalid RUST_LOG value {}, defaulting to info", settings.log_level); - Level::INFO - } + + let stdout_log_level = if let Some(log_level) = load_optional_env_var("RUST_LOG") { + log_level.parse::().expect("invalid RUST_LOG value") + } else { + settings.log_level.parse::().expect("invalid log_level value in settings") }; + let stdout_filter = format_crates_filter(Level::INFO.as_str(), stdout_log_level.as_str()); if use_file_logs { diff --git a/examples/status_api/src/main.rs b/examples/status_api/src/main.rs index 58d3b52a..40bed730 100644 --- a/examples/status_api/src/main.rs +++ b/examples/status_api/src/main.rs @@ -81,7 +81,7 @@ async fn handle_check(State(state): State>) -> Response async fn main() -> Result<()> { color_eyre::install()?; - let (pbs_config, extra) = load_pbs_custom_config::()?; + let (pbs_config, extra) = load_pbs_custom_config::().await?; let _guard = initialize_pbs_tracing_log()?; let custom_state = MyBuilderState::from_config(extra); diff --git a/tests/tests/config.rs b/tests/tests/config.rs index 684a2b84..044a62c8 100644 --- a/tests/tests/config.rs +++ b/tests/tests/config.rs @@ -1,10 +1,10 @@ use cb_common::{config::CommitBoostConfig, types::Chain}; use eyre::Result; -#[test] -fn test_load_config() -> Result<()> { +#[tokio::test] +async fn test_load_config() -> Result<()> { let config = CommitBoostConfig::from_file("../config.example.toml")?; - + config.validate().await?; assert_eq!(config.chain, Chain::Holesky); assert!(config.relays[0].headers.is_some());