Skip to content

Commit 7dba8d1

Browse files
PedroCM96jclapisltitanbnilsfs7
authored
Support lido modules (#392)
Co-authored-by: Joe Clapis <[email protected]> Co-authored-by: ltitanb <[email protected]> Co-authored-by: Nils Effinghausen <[email protected]>
1 parent 3ad487b commit 7dba8d1

File tree

12 files changed

+445
-117
lines changed

12 files changed

+445
-117
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

config.example.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ validator_pubkeys = [
150150
# OPTIONAL
151151
loader = "./tests/data/mux_keys.example.json"
152152
# loader = { url = "http://localhost:8000/keys" }
153-
# loader = { registry = "lido", node_operator_id = 8, enable_refreshing = false }
153+
# loader = { registry = "lido", node_operator_id = 8, lido_module_id = 1, enable_refreshing = false }
154154
# loader = { registry = "ssv", node_operator_id = 8, enable_refreshing = false }
155155
late_in_slot_time_ms = 1500
156156
timeout_get_header_ms = 900

crates/common/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ ethereum_ssz_derive.workspace = true
2626
eyre.workspace = true
2727
futures.workspace = true
2828
jsonwebtoken.workspace = true
29+
lazy_static.workspace = true
2930
lh_eth2.workspace = true
3031
lh_eth2_keystore.workspace = true
3132
lh_types.workspace = true
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
[
2+
{
3+
"constant": true,
4+
"inputs": [
5+
{ "name": "nodeOperatorId", "type": "uint256" }
6+
],
7+
"name": "getNodeOperatorSummary",
8+
"outputs": [
9+
{ "name": "targetLimitMode", "type": "uint256" },
10+
{ "name": "targetValidatorsCount", "type": "uint256" },
11+
{ "name": "stuckValidatorsCount", "type": "uint256" },
12+
{ "name": "refundedValidatorsCount", "type": "uint256" },
13+
{ "name": "stuckPenaltyEndTimestamp", "type": "uint256" },
14+
{ "name": "totalExitedValidators", "type": "uint256" },
15+
{ "name": "totalDepositedValidators", "type": "uint256" },
16+
{ "name": "depositableValidatorsCount", "type": "uint256" }
17+
],
18+
"payable": false,
19+
"stateMutability": "view",
20+
"type": "function"
21+
},
22+
{
23+
"constant": true,
24+
"inputs": [
25+
{ "name": "nodeOperatorId", "type": "uint256" },
26+
{ "name": "startIndex", "type": "uint256" },
27+
{ "name": "keysCount", "type": "uint256" }
28+
],
29+
"name": "getSigningKeys",
30+
"outputs": [
31+
{ "name": "", "type": "bytes" }
32+
],
33+
"payable": false,
34+
"stateMutability": "view",
35+
"type": "function"
36+
}
37+
]

crates/common/src/config/mux.rs

Lines changed: 93 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,9 @@ use std::{
77
};
88

99
use alloy::{
10-
primitives::{Address, U256, address},
10+
primitives::{Address, Bytes, U256},
1111
providers::ProviderBuilder,
1212
rpc::{client::RpcClient, types::beacon::constants::BLS_PUBLIC_KEY_BYTES_LEN},
13-
sol,
1413
transports::http::Http,
1514
};
1615
use eyre::{Context, bail, ensure};
@@ -22,7 +21,7 @@ use url::Url;
2221
use super::{MUX_PATH_ENV, PbsConfig, RelayConfig, load_optional_env_var};
2322
use crate::{
2423
config::{remove_duplicate_keys, safe_read_http_response},
25-
interop::ssv::utils::fetch_ssv_pubkeys_from_url,
24+
interop::{lido::utils::*, ssv::utils::*},
2625
pbs::RelayClient,
2726
types::{BlsPublicKey, Chain},
2827
utils::default_bool,
@@ -193,6 +192,8 @@ pub enum MuxKeysLoader {
193192
Registry {
194193
registry: NORegistry,
195194
node_operator_id: u64,
195+
#[serde(default)]
196+
lido_module_id: Option<u8>,
196197
#[serde(default = "default_bool::<false>")]
197198
enable_refreshing: bool,
198199
},
@@ -239,30 +240,33 @@ impl MuxKeysLoader {
239240
.wrap_err("failed to fetch mux keys from HTTP endpoint")
240241
}
241242

242-
Self::Registry { registry, node_operator_id, enable_refreshing: _ } => match registry {
243-
NORegistry::Lido => {
244-
let Some(rpc_url) = rpc_url else {
245-
bail!("Lido registry requires RPC URL to be set in the PBS config");
246-
};
247-
248-
fetch_lido_registry_keys(
249-
rpc_url,
250-
chain,
251-
U256::from(*node_operator_id),
252-
http_timeout,
253-
)
254-
.await
255-
}
256-
NORegistry::SSV => {
257-
fetch_ssv_pubkeys(
258-
ssv_api_url,
259-
chain,
260-
U256::from(*node_operator_id),
261-
http_timeout,
262-
)
263-
.await
243+
Self::Registry { registry, node_operator_id, lido_module_id, enable_refreshing: _ } => {
244+
match registry {
245+
NORegistry::Lido => {
246+
let Some(rpc_url) = rpc_url else {
247+
bail!("Lido registry requires RPC URL to be set in the PBS config");
248+
};
249+
250+
fetch_lido_registry_keys(
251+
rpc_url,
252+
chain,
253+
U256::from(*node_operator_id),
254+
lido_module_id.unwrap_or(1),
255+
http_timeout,
256+
)
257+
.await
258+
}
259+
NORegistry::SSV => {
260+
fetch_ssv_pubkeys(
261+
ssv_api_url,
262+
chain,
263+
U256::from(*node_operator_id),
264+
http_timeout,
265+
)
266+
.await
267+
}
264268
}
265-
},
269+
}
266270
}?;
267271

268272
// Remove duplicates
@@ -285,63 +289,28 @@ fn get_mux_path(mux_id: &str) -> String {
285289
format!("/{mux_id}-mux_keys.json")
286290
}
287291

288-
sol! {
289-
#[allow(missing_docs)]
290-
#[sol(rpc)]
291-
LidoRegistry,
292-
"src/abi/LidoNORegistry.json"
293-
}
294-
295-
// Fetching Lido Curated Module
296-
fn lido_registry_address(chain: Chain) -> eyre::Result<Address> {
297-
match chain {
298-
Chain::Mainnet => Ok(address!("55032650b14df07b85bF18A3a3eC8E0Af2e028d5")),
299-
Chain::Holesky => Ok(address!("595F64Ddc3856a3b5Ff4f4CC1d1fb4B46cFd2bAC")),
300-
Chain::Hoodi => Ok(address!("5cDbE1590c083b5A2A64427fAA63A7cfDB91FbB5")),
301-
Chain::Sepolia => Ok(address!("33d6E15047E8644F8DDf5CD05d202dfE587DA6E3")),
302-
_ => bail!("Lido registry not supported for chain: {chain:?}"),
303-
}
304-
}
305-
306-
async fn fetch_lido_registry_keys(
307-
rpc_url: Url,
308-
chain: Chain,
309-
node_operator_id: U256,
310-
http_timeout: Duration,
311-
) -> eyre::Result<Vec<BlsPublicKey>> {
312-
debug!(?chain, %node_operator_id, "loading operator keys from Lido registry");
313-
314-
// Create an RPC provider with HTTP timeout support
315-
let client = Client::builder().timeout(http_timeout).build()?;
316-
let http = Http::with_client(client, rpc_url);
317-
let is_local = http.guess_local();
318-
let rpc_client = RpcClient::new(http, is_local);
319-
let provider = ProviderBuilder::new().connect_client(rpc_client);
320-
321-
let registry_address = lido_registry_address(chain)?;
322-
let registry = LidoRegistry::new(registry_address, provider);
323-
324-
let total_keys = registry.getTotalSigningKeyCount(node_operator_id).call().await?.try_into()?;
325-
292+
async fn collect_registry_keys<F, Fut>(
293+
total_keys: u64,
294+
mut fetch_batch: F,
295+
) -> eyre::Result<Vec<BlsPublicKey>>
296+
where
297+
F: FnMut(u64, u64) -> Fut,
298+
Fut: std::future::Future<Output = eyre::Result<Bytes>>,
299+
{
326300
if total_keys == 0 {
327301
return Ok(Vec::new());
328302
}
329-
330303
debug!("fetching {total_keys} total keys");
331304

332305
const CALL_BATCH_SIZE: u64 = 250u64;
333306

334307
let mut keys = vec![];
335-
let mut offset = 0;
308+
let mut offset: u64 = 0;
336309

337310
while offset < total_keys {
338311
let limit = CALL_BATCH_SIZE.min(total_keys - offset);
339312

340-
let pubkeys = registry
341-
.getSigningKeys(node_operator_id, U256::from(offset), U256::from(limit))
342-
.call()
343-
.await?
344-
.pubkeys;
313+
let pubkeys = fetch_batch(offset, limit).await?;
345314

346315
ensure!(
347316
pubkeys.len() % BLS_PUBLIC_KEY_BYTES_LEN == 0,
@@ -368,6 +337,59 @@ async fn fetch_lido_registry_keys(
368337
Ok(keys)
369338
}
370339

340+
async fn fetch_lido_csm_registry_keys(
341+
registry_address: Address,
342+
rpc_client: RpcClient,
343+
node_operator_id: U256,
344+
) -> eyre::Result<Vec<BlsPublicKey>> {
345+
let provider = ProviderBuilder::new().connect_client(rpc_client);
346+
let registry = get_lido_csm_registry(registry_address, provider);
347+
let total_keys = fetch_lido_csm_keys_total(&registry, node_operator_id).await?;
348+
349+
collect_registry_keys(total_keys, |offset, limit| {
350+
fetch_lido_csm_keys_batch(&registry, node_operator_id, offset, limit)
351+
})
352+
.await
353+
}
354+
355+
async fn fetch_lido_module_registry_keys(
356+
registry_address: Address,
357+
rpc_client: RpcClient,
358+
node_operator_id: U256,
359+
) -> eyre::Result<Vec<BlsPublicKey>> {
360+
let provider = ProviderBuilder::new().connect_client(rpc_client);
361+
let registry = get_lido_module_registry(registry_address, provider);
362+
let total_keys: u64 = fetch_lido_module_keys_total(&registry, node_operator_id).await?;
363+
364+
collect_registry_keys(total_keys, |offset, limit| {
365+
fetch_lido_module_keys_batch(&registry, node_operator_id, offset, limit)
366+
})
367+
.await
368+
}
369+
370+
async fn fetch_lido_registry_keys(
371+
rpc_url: Url,
372+
chain: Chain,
373+
node_operator_id: U256,
374+
lido_module_id: u8,
375+
http_timeout: Duration,
376+
) -> eyre::Result<Vec<BlsPublicKey>> {
377+
debug!(?chain, %node_operator_id, ?lido_module_id, "loading operator keys from Lido registry");
378+
379+
// Create an RPC provider with HTTP timeout support
380+
let client = Client::builder().timeout(http_timeout).build()?;
381+
let http = Http::with_client(client, rpc_url);
382+
let is_local = http.guess_local();
383+
let rpc_client = RpcClient::new(http, is_local);
384+
let registry_address = lido_registry_address(chain, lido_module_id)?;
385+
386+
if is_csm_module(chain, lido_module_id) {
387+
fetch_lido_csm_registry_keys(registry_address, rpc_client, node_operator_id).await
388+
} else {
389+
fetch_lido_module_registry_keys(registry_address, rpc_client, node_operator_id).await
390+
}
391+
}
392+
371393
async fn fetch_ssv_pubkeys(
372394
mut api_url: Url,
373395
chain: Chain,
@@ -421,46 +443,3 @@ async fn fetch_ssv_pubkeys(
421443

422444
Ok(pubkeys)
423445
}
424-
425-
#[cfg(test)]
426-
mod tests {
427-
use alloy::{primitives::U256, providers::ProviderBuilder};
428-
use url::Url;
429-
430-
use super::*;
431-
432-
#[tokio::test]
433-
async fn test_lido_registry_address() -> eyre::Result<()> {
434-
let url = Url::parse("https://ethereum-rpc.publicnode.com")?;
435-
let provider = ProviderBuilder::new().connect_http(url);
436-
437-
let registry =
438-
LidoRegistry::new(address!("55032650b14df07b85bF18A3a3eC8E0Af2e028d5"), provider);
439-
440-
const LIMIT: usize = 3;
441-
let node_operator_id = U256::from(1);
442-
443-
let total_keys: u64 =
444-
registry.getTotalSigningKeyCount(node_operator_id).call().await?.try_into()?;
445-
446-
assert!(total_keys > LIMIT as u64);
447-
448-
let pubkeys = registry
449-
.getSigningKeys(node_operator_id, U256::ZERO, U256::from(LIMIT))
450-
.call()
451-
.await?
452-
.pubkeys;
453-
454-
let mut vec = vec![];
455-
for chunk in pubkeys.chunks(BLS_PUBLIC_KEY_BYTES_LEN) {
456-
vec.push(
457-
BlsPublicKey::deserialize(chunk)
458-
.map_err(|_| eyre::eyre!("invalid BLS public key"))?,
459-
);
460-
}
461-
462-
assert_eq!(vec.len(), LIMIT);
463-
464-
Ok(())
465-
}
466-
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
pub mod types;
2+
pub mod utils;
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
use alloy::sol;
2+
3+
sol! {
4+
#[allow(missing_docs)]
5+
#[sol(rpc)]
6+
LidoRegistry,
7+
"src/abi/LidoNORegistry.json"
8+
}
9+
10+
sol! {
11+
#[allow(missing_docs)]
12+
#[sol(rpc)]
13+
LidoCSMRegistry,
14+
"src/abi/LidoCSModuleNORegistry.json"
15+
}

0 commit comments

Comments
 (0)