From 57ad84b259d6c7f821ca5dbf9f080e101015ef7d Mon Sep 17 00:00:00 2001 From: Pedro Carnerero Date: Tue, 7 Oct 2025 19:38:39 +0200 Subject: [PATCH 1/5] feat: support lido modules Added new types in `crates/common/src/types.rs` to model the different Lido modules we'll encounter depending on the Chain. Also added a `HashMap` that introduces a new dimension to the pseudo-map that previously always returned the registry address for the Lido curated module in `crates/common/src/config/mux.rs`, with the goal of being able to return the corresponding address based on the Chain and Lido Module id. To specify the required Lido module, a new optional field was added to `examples/configs/pbs_mux.toml` for lido loader, called `lido_module_id` which always defaults to 1. --- config.example.toml | 2 +- crates/common/src/config/mux.rs | 64 ++++++++++++++++++++++++++------- crates/common/src/types.rs | 22 +++++++++++- examples/configs/pbs_mux.toml | 2 +- 4 files changed, 75 insertions(+), 15 deletions(-) diff --git a/config.example.toml b/config.example.toml index ad0c5340..77c83311 100644 --- a/config.example.toml +++ b/config.example.toml @@ -135,7 +135,7 @@ validator_pubkeys = [ # OPTIONAL loader = "./tests/data/mux_keys.example.json" # loader = { url = "http://localhost:8000/keys" } -# loader = { registry = "lido", node_operator_id = 8 } +# loader = { registry = "lido", node_operator_id = 8, lido_module_id = 1 } # loader = { registry = "ssv", node_operator_id = 8 } late_in_slot_time_ms = 1500 timeout_get_header_ms = 900 diff --git a/crates/common/src/config/mux.rs b/crates/common/src/config/mux.rs index 346eaf78..3aa3746e 100644 --- a/crates/common/src/config/mux.rs +++ b/crates/common/src/config/mux.rs @@ -22,7 +22,7 @@ use super::{MUX_PATH_ENV, PbsConfig, RelayConfig, load_optional_env_var}; use crate::{ config::{remove_duplicate_keys, safe_read_http_response}, pbs::RelayClient, - types::{BlsPublicKey, Chain}, + types::{BlsPublicKey, Chain, HoleskyModules, HoodiModules, MainnetModules}, }; #[derive(Debug, Deserialize, Serialize)] @@ -167,6 +167,8 @@ pub enum MuxKeysLoader { Registry { registry: NORegistry, node_operator_id: u64, + #[serde(default)] + lido_module_id: Option }, } @@ -210,7 +212,7 @@ impl MuxKeysLoader { .wrap_err("failed to fetch mux keys from HTTP endpoint") } - Self::Registry { registry, node_operator_id } => match registry { + Self::Registry { registry, node_operator_id, lido_module_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"); @@ -220,6 +222,7 @@ impl MuxKeysLoader { rpc_url, chain, U256::from(*node_operator_id), + *lido_module_id, http_timeout, ) .await @@ -257,21 +260,58 @@ sol! { "src/abi/LidoNORegistry.json" } -// Fetching Lido Curated Module -fn lido_registry_address(chain: Chain) -> eyre::Result
{ - match chain { - Chain::Mainnet => Ok(address!("55032650b14df07b85bF18A3a3eC8E0Af2e028d5")), - Chain::Holesky => Ok(address!("595F64Ddc3856a3b5Ff4f4CC1d1fb4B46cFd2bAC")), - Chain::Hoodi => Ok(address!("5cDbE1590c083b5A2A64427fAA63A7cfDB91FbB5")), - Chain::Sepolia => Ok(address!("33d6E15047E8644F8DDf5CD05d202dfE587DA6E3")), - _ => bail!("Lido registry not supported for chain: {chain:?}"), - } +fn lido_registry_addresses_by_module() -> HashMap> { + let mut map: HashMap> = HashMap::new(); + + // --- Mainnet --- + let mut mainnet = HashMap::new(); + mainnet.insert(MainnetModules::Curated as u8, address!("55032650b14df07b85bF18A3a3eC8E0Af2e028d5")); + mainnet.insert(MainnetModules::SimpleDVT as u8, address!("aE7B191A31f627b4eB1d4DaC64eaB9976995b433")); + mainnet.insert(MainnetModules::CommunityStaking as u8, address!("dA7dE2ECdDfccC6c3AF10108Db212ACBBf9EA83F")); + map.insert(Chain::Mainnet, mainnet); + + // --- Holesky --- + let mut holesky = HashMap::new(); + holesky.insert(HoleskyModules::Curated as u8, address!("595F64Ddc3856a3b5Ff4f4CC1d1fb4B46cFd2bAC")); + holesky.insert(HoleskyModules::SimpleDVT as u8, address!("11a93807078f8BB880c1BD0ee4C387537de4b4b6")); + holesky.insert(HoleskyModules::Sandbox as u8, address!("D6C2ce3BB8bea2832496Ac8b5144819719f343AC")); + holesky.insert(HoleskyModules::CommunityStaking as u8, address!("4562c3e63c2e586cD1651B958C22F88135aCAd4f")); + map.insert(Chain::Holesky, holesky); + + // --- Hoodi --- + let mut hoodi = HashMap::new(); + hoodi.insert(HoodiModules::Curated as u8, address!("5cDbE1590c083b5A2A64427fAA63A7cfDB91FbB5")); + hoodi.insert(HoodiModules::SimpleDVT as u8, address!("0B5236BECA68004DB89434462DfC3BB074d2c830")); + hoodi.insert(HoodiModules::Sandbox as u8, address!("682E94d2630846a503BDeE8b6810DF71C9806891")); + hoodi.insert(HoodiModules::CommunityStaking as u8, address!("79CEf36D84743222f37765204Bec41E92a93E59d")); + map.insert(Chain::Hoodi, hoodi); + + // --- Sepolia -- + let mut sepolia = HashMap::new(); + sepolia.insert(1, address!("33d6E15047E8644F8DDf5CD05d202dfE587DA6E3")); + map.insert(Chain::Sepolia, sepolia); + + map +} + +// Fetching appropiate registry address +fn lido_registry_address(chain: Chain, maybe_module: Option) -> eyre::Result
{ + lido_registry_addresses_by_module() + .get(&chain) + .ok_or_else(|| eyre::eyre!("Lido registry not supported for chain: {chain:?}"))? + .get(&maybe_module.unwrap_or(1)) + .copied() + .ok_or_else(|| eyre::eyre!( + "Lido module id {:?} not found for chain: {chain:?}", + maybe_module.unwrap_or(1) + )) } async fn fetch_lido_registry_keys( rpc_url: Url, chain: Chain, node_operator_id: U256, + lido_module_id: Option, http_timeout: Duration, ) -> eyre::Result> { debug!(?chain, %node_operator_id, "loading operator keys from Lido registry"); @@ -283,7 +323,7 @@ async fn fetch_lido_registry_keys( let rpc_client = RpcClient::new(http, is_local); let provider = ProviderBuilder::new().connect_client(rpc_client); - let registry_address = lido_registry_address(chain)?; + let registry_address = lido_registry_address(chain, lido_module_id)?; let registry = LidoRegistry::new(registry_address, provider); let total_keys = registry.getTotalSigningKeyCount(node_operator_id).call().await?.try_into()?; diff --git a/crates/common/src/types.rs b/crates/common/src/types.rs index 077b4ccd..ef98ef22 100644 --- a/crates/common/src/types.rs +++ b/crates/common/src/types.rs @@ -29,7 +29,7 @@ pub struct JwtClaims { pub module: String, } -#[derive(Clone, Copy, PartialEq, Eq)] +#[derive(Clone, Copy, PartialEq, Eq, Hash)] pub enum Chain { Mainnet, Holesky, @@ -44,6 +44,26 @@ pub enum Chain { }, } +pub enum MainnetModules { + Curated = 1, + SimpleDVT = 2, + CommunityStaking = 3 +} + +pub enum HoleskyModules { + Curated = 1, + SimpleDVT = 2, + Sandbox = 3, + CommunityStaking = 4 +} + +pub enum HoodiModules { + Curated = 1, + SimpleDVT = 2, + Sandbox = 3, + CommunityStaking = 4 +} + pub type ForkVersion = [u8; 4]; impl std::fmt::Display for Chain { diff --git a/examples/configs/pbs_mux.toml b/examples/configs/pbs_mux.toml index 3ea9f355..fcf4ea8c 100644 --- a/examples/configs/pbs_mux.toml +++ b/examples/configs/pbs_mux.toml @@ -33,7 +33,7 @@ target_first_request_ms = 200 [[mux]] id = "lido-mux" -loader = { registry = "lido", node_operator_id = 8 } +loader = { registry = "lido", node_operator_id = 8, lido_module_id = 1 } [[mux.relays]] id = "relay-3" From 3adc6cc39361d72546119cd33763f4ad96c87f02 Mon Sep 17 00:00:00 2001 From: Pedro Carnerero Date: Wed, 8 Oct 2025 09:54:06 +0200 Subject: [PATCH 2/5] refactor: rename lido module enums to singular and more descriptive name --- crates/common/src/config/mux.rs | 24 ++++++++++++------------ crates/common/src/types.rs | 6 +++--- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/crates/common/src/config/mux.rs b/crates/common/src/config/mux.rs index 3aa3746e..6c629959 100644 --- a/crates/common/src/config/mux.rs +++ b/crates/common/src/config/mux.rs @@ -22,7 +22,7 @@ use super::{MUX_PATH_ENV, PbsConfig, RelayConfig, load_optional_env_var}; use crate::{ config::{remove_duplicate_keys, safe_read_http_response}, pbs::RelayClient, - types::{BlsPublicKey, Chain, HoleskyModules, HoodiModules, MainnetModules}, + types::{BlsPublicKey, Chain, HoleskyLidoModule, HoodiLidoModule, MainnetLidoModule}, }; #[derive(Debug, Deserialize, Serialize)] @@ -265,25 +265,25 @@ fn lido_registry_addresses_by_module() -> HashMap> { // --- Mainnet --- let mut mainnet = HashMap::new(); - mainnet.insert(MainnetModules::Curated as u8, address!("55032650b14df07b85bF18A3a3eC8E0Af2e028d5")); - mainnet.insert(MainnetModules::SimpleDVT as u8, address!("aE7B191A31f627b4eB1d4DaC64eaB9976995b433")); - mainnet.insert(MainnetModules::CommunityStaking as u8, address!("dA7dE2ECdDfccC6c3AF10108Db212ACBBf9EA83F")); + mainnet.insert(MainnetLidoModule::Curated as u8, address!("55032650b14df07b85bF18A3a3eC8E0Af2e028d5")); + mainnet.insert(MainnetLidoModule::SimpleDVT as u8, address!("aE7B191A31f627b4eB1d4DaC64eaB9976995b433")); + mainnet.insert(MainnetLidoModule::CommunityStaking as u8, address!("dA7dE2ECdDfccC6c3AF10108Db212ACBBf9EA83F")); map.insert(Chain::Mainnet, mainnet); // --- Holesky --- let mut holesky = HashMap::new(); - holesky.insert(HoleskyModules::Curated as u8, address!("595F64Ddc3856a3b5Ff4f4CC1d1fb4B46cFd2bAC")); - holesky.insert(HoleskyModules::SimpleDVT as u8, address!("11a93807078f8BB880c1BD0ee4C387537de4b4b6")); - holesky.insert(HoleskyModules::Sandbox as u8, address!("D6C2ce3BB8bea2832496Ac8b5144819719f343AC")); - holesky.insert(HoleskyModules::CommunityStaking as u8, address!("4562c3e63c2e586cD1651B958C22F88135aCAd4f")); + holesky.insert(HoleskyLidoModule::Curated as u8, address!("595F64Ddc3856a3b5Ff4f4CC1d1fb4B46cFd2bAC")); + holesky.insert(HoleskyLidoModule::SimpleDVT as u8, address!("11a93807078f8BB880c1BD0ee4C387537de4b4b6")); + holesky.insert(HoleskyLidoModule::Sandbox as u8, address!("D6C2ce3BB8bea2832496Ac8b5144819719f343AC")); + holesky.insert(HoleskyLidoModule::CommunityStaking as u8, address!("4562c3e63c2e586cD1651B958C22F88135aCAd4f")); map.insert(Chain::Holesky, holesky); // --- Hoodi --- let mut hoodi = HashMap::new(); - hoodi.insert(HoodiModules::Curated as u8, address!("5cDbE1590c083b5A2A64427fAA63A7cfDB91FbB5")); - hoodi.insert(HoodiModules::SimpleDVT as u8, address!("0B5236BECA68004DB89434462DfC3BB074d2c830")); - hoodi.insert(HoodiModules::Sandbox as u8, address!("682E94d2630846a503BDeE8b6810DF71C9806891")); - hoodi.insert(HoodiModules::CommunityStaking as u8, address!("79CEf36D84743222f37765204Bec41E92a93E59d")); + hoodi.insert(HoodiLidoModule::Curated as u8, address!("5cDbE1590c083b5A2A64427fAA63A7cfDB91FbB5")); + hoodi.insert(HoodiLidoModule::SimpleDVT as u8, address!("0B5236BECA68004DB89434462DfC3BB074d2c830")); + hoodi.insert(HoodiLidoModule::Sandbox as u8, address!("682E94d2630846a503BDeE8b6810DF71C9806891")); + hoodi.insert(HoodiLidoModule::CommunityStaking as u8, address!("79CEf36D84743222f37765204Bec41E92a93E59d")); map.insert(Chain::Hoodi, hoodi); // --- Sepolia -- diff --git a/crates/common/src/types.rs b/crates/common/src/types.rs index ef98ef22..e5eb593c 100644 --- a/crates/common/src/types.rs +++ b/crates/common/src/types.rs @@ -44,20 +44,20 @@ pub enum Chain { }, } -pub enum MainnetModules { +pub enum MainnetLidoModule { Curated = 1, SimpleDVT = 2, CommunityStaking = 3 } -pub enum HoleskyModules { +pub enum HoleskyLidoModule { Curated = 1, SimpleDVT = 2, Sandbox = 3, CommunityStaking = 4 } -pub enum HoodiModules { +pub enum HoodiLidoModule { Curated = 1, SimpleDVT = 2, Sandbox = 3, From 4a40bb5e3d8c971af432a5a006f37793efb475ca Mon Sep 17 00:00:00 2001 From: Pedro Carnerero Date: Wed, 8 Oct 2025 12:02:59 +0200 Subject: [PATCH 3/5] chore: include lido_module_id in debug log --- crates/common/src/config/mux.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/common/src/config/mux.rs b/crates/common/src/config/mux.rs index 6c629959..3de4ed0b 100644 --- a/crates/common/src/config/mux.rs +++ b/crates/common/src/config/mux.rs @@ -314,7 +314,7 @@ async fn fetch_lido_registry_keys( lido_module_id: Option, http_timeout: Duration, ) -> eyre::Result> { - debug!(?chain, %node_operator_id, "loading operator keys from Lido registry"); + debug!(?chain, %node_operator_id, ?lido_module_id, "loading operator keys from Lido registry"); // Create an RPC provider with HTTP timeout support let client = Client::builder().timeout(http_timeout).build()?; From 9a7dfcd1d7de1ed84020b57dc06f2e9ee34c64d9 Mon Sep 17 00:00:00 2001 From: Pedro Carnerero Date: Wed, 8 Oct 2025 17:22:01 +0200 Subject: [PATCH 4/5] refactor: make lido_module_id argument mandatory --- crates/common/src/config/mux.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/common/src/config/mux.rs b/crates/common/src/config/mux.rs index 3de4ed0b..de3a4d6d 100644 --- a/crates/common/src/config/mux.rs +++ b/crates/common/src/config/mux.rs @@ -222,7 +222,7 @@ impl MuxKeysLoader { rpc_url, chain, U256::from(*node_operator_id), - *lido_module_id, + lido_module_id.unwrap_or(1), http_timeout, ) .await @@ -295,15 +295,15 @@ fn lido_registry_addresses_by_module() -> HashMap> { } // Fetching appropiate registry address -fn lido_registry_address(chain: Chain, maybe_module: Option) -> eyre::Result
{ +fn lido_registry_address(chain: Chain, lido_module_id: u8) -> eyre::Result
{ lido_registry_addresses_by_module() .get(&chain) .ok_or_else(|| eyre::eyre!("Lido registry not supported for chain: {chain:?}"))? - .get(&maybe_module.unwrap_or(1)) + .get(&lido_module_id) .copied() .ok_or_else(|| eyre::eyre!( "Lido module id {:?} not found for chain: {chain:?}", - maybe_module.unwrap_or(1) + lido_module_id )) } @@ -311,7 +311,7 @@ async fn fetch_lido_registry_keys( rpc_url: Url, chain: Chain, node_operator_id: U256, - lido_module_id: Option, + lido_module_id: u8, http_timeout: Duration, ) -> eyre::Result> { debug!(?chain, %node_operator_id, ?lido_module_id, "loading operator keys from Lido registry"); From 83d137d1edd97c0e066d496a68903361850ef2fa Mon Sep 17 00:00:00 2001 From: Pedro Carnerero Date: Wed, 8 Oct 2025 20:06:00 +0200 Subject: [PATCH 5/5] feat: support lido community staking module MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added support for Lido's Community Staking Module (CSM), since the deployed contract has a different ABI, so the same registry used for the other modules cannot be used. Because this difference in ABIs, the CSM contract ABI has been imported, containing the required functions. One key difference is that in the CSM contract, `getTotalSigningKeyCount´ does not exist; therefore, for CSM this call is replaced with `getNodeOperatorSummary´, which returns a tuple of several values, including `totalDepositedValidators´ and `depositableValidatorsCount´, whose sum is used to compute the total number of keys. Another difference between ABIs is the `getSigningKeys´ method, which in CSM returns directly a `Bytes´ type. The logic for obtaining `total_keys´ and the iteration to fetch the keys has been extracted into smaller functions since this new case for CSM encourages code duplication; therefore, the main function `fetch_lido_registry_keys´ clearly separate both cases. --- .../src/abi/LidoCSModuleNORegistry.json | 37 +++ crates/common/src/config/mux.rs | 235 ++++++++++++++++-- 2 files changed, 247 insertions(+), 25 deletions(-) create mode 100644 crates/common/src/abi/LidoCSModuleNORegistry.json diff --git a/crates/common/src/abi/LidoCSModuleNORegistry.json b/crates/common/src/abi/LidoCSModuleNORegistry.json new file mode 100644 index 00000000..a0b98aab --- /dev/null +++ b/crates/common/src/abi/LidoCSModuleNORegistry.json @@ -0,0 +1,37 @@ +[ + { + "constant": true, + "inputs": [ + { "name": "nodeOperatorId", "type": "uint256" } + ], + "name": "getNodeOperatorSummary", + "outputs": [ + { "name": "targetLimitMode", "type": "uint256" }, + { "name": "targetValidatorsCount", "type": "uint256" }, + { "name": "stuckValidatorsCount", "type": "uint256" }, + { "name": "refundedValidatorsCount", "type": "uint256" }, + { "name": "stuckPenaltyEndTimestamp", "type": "uint256" }, + { "name": "totalExitedValidators", "type": "uint256" }, + { "name": "totalDepositedValidators", "type": "uint256" }, + { "name": "depositableValidatorsCount", "type": "uint256" } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { "name": "nodeOperatorId", "type": "uint256" }, + { "name": "startIndex", "type": "uint256" }, + { "name": "keysCount", "type": "uint256" } + ], + "name": "getSigningKeys", + "outputs": [ + { "name": "", "type": "bytes" } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + } +] diff --git a/crates/common/src/config/mux.rs b/crates/common/src/config/mux.rs index de3a4d6d..89b951c0 100644 --- a/crates/common/src/config/mux.rs +++ b/crates/common/src/config/mux.rs @@ -6,17 +6,18 @@ use std::{ }; use alloy::{ - primitives::{Address, U256, address}, + primitives::{address, Address, Bytes, U256}, providers::ProviderBuilder, rpc::{client::RpcClient, types::beacon::constants::BLS_PUBLIC_KEY_BYTES_LEN}, sol, transports::http::Http, }; -use eyre::{Context, bail, ensure}; +use eyre::{bail, ensure, Context}; use reqwest::Client; use serde::{Deserialize, Deserializer, Serialize}; use tracing::{debug, info, warn}; use url::Url; +use LidoCSMRegistry::getNodeOperatorSummaryReturn; use super::{MUX_PATH_ENV, PbsConfig, RelayConfig, load_optional_env_var}; use crate::{ @@ -260,6 +261,13 @@ sol! { "src/abi/LidoNORegistry.json" } +sol! { + #[allow(missing_docs)] + #[sol(rpc)] + LidoCSMRegistry, + "src/abi/LidoCSModuleNORegistry.json" +} + fn lido_registry_addresses_by_module() -> HashMap> { let mut map: HashMap> = HashMap::new(); @@ -307,46 +315,128 @@ fn lido_registry_address(chain: Chain, lido_module_id: u8) -> eyre::Result bool { + match chain { + Chain::Mainnet => module_id == MainnetLidoModule::CommunityStaking as u8, + Chain::Holesky => module_id == HoleskyLidoModule::CommunityStaking as u8, + Chain::Hoodi => module_id == HoodiLidoModule::CommunityStaking as u8, + _ => false, + } +} + +fn get_lido_csm_registry

( + registry_address: Address, + provider: P, +) -> LidoCSMRegistry::LidoCSMRegistryInstance

+where + P: Clone + Send + Sync + 'static + alloy::providers::Provider, +{ + LidoCSMRegistry::new(registry_address, provider) +} + +fn get_lido_module_registry

( + registry_address: Address, + provider: P, +) -> LidoRegistry::LidoRegistryInstance

+where + P: Clone + Send + Sync + 'static + alloy::providers::Provider, +{ + LidoRegistry::new(registry_address, provider) +} + +async fn fetch_lido_csm_keys_total

( + registry: &LidoCSMRegistry::LidoCSMRegistryInstance

, node_operator_id: U256, - lido_module_id: u8, - http_timeout: Duration, -) -> eyre::Result> { - debug!(?chain, %node_operator_id, ?lido_module_id, "loading operator keys from Lido registry"); +) -> eyre::Result +where + P: Clone + Send + Sync + 'static + alloy::providers::Provider, +{ + let summary: getNodeOperatorSummaryReturn = registry + .getNodeOperatorSummary(node_operator_id) + .call() + .await?; - // Create an RPC provider with HTTP timeout support - let client = Client::builder().timeout(http_timeout).build()?; - let http = Http::with_client(client, rpc_url); - let is_local = http.guess_local(); - let rpc_client = RpcClient::new(http, is_local); - let provider = ProviderBuilder::new().connect_client(rpc_client); + let total_u256 = summary.totalDepositedValidators + summary.depositableValidatorsCount; - let registry_address = lido_registry_address(chain, lido_module_id)?; - let registry = LidoRegistry::new(registry_address, provider); + let total_u64 = u64::try_from(total_u256) + .wrap_err_with(|| format!("total keys ({total_u256}) does not fit into u64"))?; + + Ok(total_u64) +} + +async fn fetch_lido_module_keys_total

( + registry: &LidoRegistry::LidoRegistryInstance

, + node_operator_id: U256, +) -> eyre::Result +where + P: Clone + Send + Sync + 'static + alloy::providers::Provider, +{ + let total_keys: u64 = registry + .getTotalSigningKeyCount(node_operator_id) + .call() + .await? + .try_into()?; + + Ok(total_keys) +} + +async fn fetch_lido_csm_keys_batch

( + registry: &LidoCSMRegistry::LidoCSMRegistryInstance

, + node_operator_id: U256, + offset: u64, + limit: u64 +) -> eyre::Result +where + P: Clone + Send + Sync + 'static + alloy::providers::Provider, +{ + let pubkeys = registry + .getSigningKeys(node_operator_id, U256::from(offset), U256::from(limit)) + .call() + .await?; + + Ok(pubkeys) +} - let total_keys = registry.getTotalSigningKeyCount(node_operator_id).call().await?.try_into()?; +async fn fetch_lido_module_keys_batch

( + registry: &LidoRegistry::LidoRegistryInstance

, + node_operator_id: U256, + offset: u64, + limit: u64 +) -> eyre::Result +where + P: Clone + Send + Sync + 'static + alloy::providers::Provider, +{ + let pubkeys = registry + .getSigningKeys(node_operator_id, U256::from(offset), U256::from(limit)) + .call() + .await? + .pubkeys; + + Ok(pubkeys) +} +async fn collect_registry_keys( + total_keys: u64, + mut fetch_batch: F, +) -> eyre::Result> +where + F: FnMut(u64, u64) -> Fut, + Fut: std::future::Future>, +{ if total_keys == 0 { return Ok(Vec::new()); } - debug!("fetching {total_keys} total keys"); const CALL_BATCH_SIZE: u64 = 250u64; let mut keys = vec![]; - let mut offset = 0; + let mut offset: u64 = 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; + let pubkeys = fetch_batch(offset, limit).await?; ensure!( pubkeys.len() % BLS_PUBLIC_KEY_BYTES_LEN == 0, @@ -373,6 +463,58 @@ async fn fetch_lido_registry_keys( Ok(keys) } +async fn fetch_lido_csm_registry_keys ( + registry_address: Address, + rpc_client: RpcClient, + node_operator_id: U256, +) -> eyre::Result> { + let provider = ProviderBuilder::new().connect_client(rpc_client); + let registry = get_lido_csm_registry(registry_address, provider); + + let total_keys = fetch_lido_csm_keys_total(®istry, node_operator_id).await?.try_into()?; + + collect_registry_keys(total_keys, |offset, limit| { + fetch_lido_csm_keys_batch(®istry, node_operator_id, offset, limit) + }).await +} + +async fn fetch_lido_module_registry_keys ( + registry_address: Address, + rpc_client: RpcClient, + node_operator_id: U256, +) -> eyre::Result> { + let provider = ProviderBuilder::new().connect_client(rpc_client); + let registry = get_lido_module_registry(registry_address, provider); + let total_keys: u64 = fetch_lido_module_keys_total(®istry, node_operator_id).await?.try_into()?; + + collect_registry_keys(total_keys, |offset, limit| { + fetch_lido_module_keys_batch(®istry, node_operator_id, offset, limit) + }).await +} + +async fn fetch_lido_registry_keys( + rpc_url: Url, + chain: Chain, + node_operator_id: U256, + lido_module_id: u8, + http_timeout: Duration, +) -> eyre::Result> { + debug!(?chain, %node_operator_id, ?lido_module_id, "loading operator keys from Lido registry"); + + // Create an RPC provider with HTTP timeout support + let client = Client::builder().timeout(http_timeout).build()?; + let http = Http::with_client(client, rpc_url); + let is_local = http.guess_local(); + let rpc_client = RpcClient::new(http, is_local); + let registry_address = lido_registry_address(chain, lido_module_id)?; + + if is_csm_module(chain, lido_module_id) { + fetch_lido_csm_registry_keys(registry_address, rpc_client, node_operator_id).await + } else { + fetch_lido_module_registry_keys(registry_address, rpc_client, node_operator_id).await + } +} + async fn fetch_ssv_pubkeys( chain: Chain, node_operator_id: U256, @@ -520,6 +662,49 @@ mod tests { Ok(()) } + #[tokio::test] + async fn test_lido_csm_registry_address() -> eyre::Result<()> { + use alloy::{primitives::U256, providers::ProviderBuilder}; + + let url = Url::parse("https://ethereum-rpc.publicnode.com")?; + let provider = ProviderBuilder::new().connect_http(url); + + let registry = LidoCSMRegistry::new( + address!("dA7dE2ECdDfccC6c3AF10108Db212ACBBf9EA83F"), + provider, + ); + + const LIMIT: usize = 3; + let node_operator_id = U256::from(1); + + let summary = registry + .getNodeOperatorSummary(node_operator_id) + .call() + .await?; + + let total_keys_u256 = summary.totalDepositedValidators + summary.depositableValidatorsCount; + let total_keys: u64 = total_keys_u256.try_into()?; + + assert!(total_keys > LIMIT as u64, "expected more than {LIMIT} keys, got {total_keys}"); + + let pubkeys = registry + .getSigningKeys(node_operator_id, U256::ZERO, U256::from(LIMIT)) + .call() + .await?; + + let mut vec = Vec::new(); + for chunk in pubkeys.chunks(BLS_PUBLIC_KEY_BYTES_LEN) { + vec.push( + BlsPublicKey::deserialize(chunk) + .map_err(|_| eyre::eyre!("invalid BLS public key"))?, + ); + } + + assert_eq!(vec.len(), LIMIT, "expected {LIMIT} keys, got {}", vec.len()); + + Ok(()) + } + #[tokio::test] /// Tests that a successful SSV network fetch is handled and parsed properly async fn test_ssv_network_fetch() -> eyre::Result<()> {