Skip to content

Commit 8dcc8ae

Browse files
committed
feat: support lido community staking module
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.
1 parent 9a7dfcd commit 8dcc8ae

File tree

2 files changed

+247
-25
lines changed

2 files changed

+247
-25
lines changed
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: 210 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,18 @@ use std::{
66
};
77

88
use alloy::{
9-
primitives::{Address, U256, address},
9+
primitives::{address, Address, Bytes, U256},
1010
providers::ProviderBuilder,
1111
rpc::{client::RpcClient, types::beacon::constants::BLS_PUBLIC_KEY_BYTES_LEN},
1212
sol,
1313
transports::http::Http,
1414
};
15-
use eyre::{Context, bail, ensure};
15+
use eyre::{bail, ensure, Context};
1616
use reqwest::Client;
1717
use serde::{Deserialize, Deserializer, Serialize};
1818
use tracing::{debug, info, warn};
1919
use url::Url;
20+
use LidoCSMRegistry::getNodeOperatorSummaryReturn;
2021

2122
use super::{MUX_PATH_ENV, PbsConfig, RelayConfig, load_optional_env_var};
2223
use crate::{
@@ -260,6 +261,13 @@ sol! {
260261
"src/abi/LidoNORegistry.json"
261262
}
262263

264+
sol! {
265+
#[allow(missing_docs)]
266+
#[sol(rpc)]
267+
LidoCSMRegistry,
268+
"src/abi/LidoCSModuleNORegistry.json"
269+
}
270+
263271
fn lido_registry_addresses_by_module() -> HashMap<Chain, HashMap<u8, Address>> {
264272
let mut map: HashMap<Chain, HashMap<u8, Address>> = HashMap::new();
265273

@@ -307,46 +315,128 @@ fn lido_registry_address(chain: Chain, lido_module_id: u8) -> eyre::Result<Addre
307315
))
308316
}
309317

310-
async fn fetch_lido_registry_keys(
311-
rpc_url: Url,
312-
chain: Chain,
318+
fn is_csm_module(chain: Chain, module_id: u8) -> bool {
319+
match chain {
320+
Chain::Mainnet => module_id == MainnetLidoModule::CommunityStaking as u8,
321+
Chain::Holesky => module_id == HoleskyLidoModule::CommunityStaking as u8,
322+
Chain::Hoodi => module_id == HoodiLidoModule::CommunityStaking as u8,
323+
_ => false,
324+
}
325+
}
326+
327+
fn get_lido_csm_registry<P>(
328+
registry_address: Address,
329+
provider: P,
330+
) -> LidoCSMRegistry::LidoCSMRegistryInstance<P>
331+
where
332+
P: Clone + Send + Sync + 'static + alloy::providers::Provider,
333+
{
334+
LidoCSMRegistry::new(registry_address, provider)
335+
}
336+
337+
fn get_lido_module_registry<P>(
338+
registry_address: Address,
339+
provider: P,
340+
) -> LidoRegistry::LidoRegistryInstance<P>
341+
where
342+
P: Clone + Send + Sync + 'static + alloy::providers::Provider,
343+
{
344+
LidoRegistry::new(registry_address, provider)
345+
}
346+
347+
async fn fetch_lido_csm_keys_total<P>(
348+
registry: &LidoCSMRegistry::LidoCSMRegistryInstance<P>,
313349
node_operator_id: U256,
314-
lido_module_id: u8,
315-
http_timeout: Duration,
316-
) -> eyre::Result<Vec<BlsPublicKey>> {
317-
debug!(?chain, %node_operator_id, ?lido_module_id, "loading operator keys from Lido registry");
350+
) -> eyre::Result<u64>
351+
where
352+
P: Clone + Send + Sync + 'static + alloy::providers::Provider,
353+
{
354+
let summary: getNodeOperatorSummaryReturn = registry
355+
.getNodeOperatorSummary(node_operator_id)
356+
.call()
357+
.await?;
318358

319-
// Create an RPC provider with HTTP timeout support
320-
let client = Client::builder().timeout(http_timeout).build()?;
321-
let http = Http::with_client(client, rpc_url);
322-
let is_local = http.guess_local();
323-
let rpc_client = RpcClient::new(http, is_local);
324-
let provider = ProviderBuilder::new().connect_client(rpc_client);
359+
let total_u256 = summary.totalDepositedValidators + summary.depositableValidatorsCount;
325360

326-
let registry_address = lido_registry_address(chain, lido_module_id)?;
327-
let registry = LidoRegistry::new(registry_address, provider);
361+
let total_u64 = u64::try_from(total_u256)
362+
.wrap_err_with(|| format!("total keys ({total_u256}) does not fit into u64"))?;
363+
364+
Ok(total_u64)
365+
}
366+
367+
async fn fetch_lido_module_keys_total<P>(
368+
registry: &LidoRegistry::LidoRegistryInstance<P>,
369+
node_operator_id: U256,
370+
) -> eyre::Result<u64>
371+
where
372+
P: Clone + Send + Sync + 'static + alloy::providers::Provider,
373+
{
374+
let total_keys: u64 = registry
375+
.getTotalSigningKeyCount(node_operator_id)
376+
.call()
377+
.await?
378+
.try_into()?;
379+
380+
Ok(total_keys)
381+
}
382+
383+
async fn fetch_lido_csm_keys_batch<P>(
384+
registry: &LidoCSMRegistry::LidoCSMRegistryInstance<P>,
385+
node_operator_id: U256,
386+
offset: u64,
387+
limit: u64
388+
) -> eyre::Result<Bytes>
389+
where
390+
P: Clone + Send + Sync + 'static + alloy::providers::Provider,
391+
{
392+
let pubkeys = registry
393+
.getSigningKeys(node_operator_id, U256::from(offset), U256::from(limit))
394+
.call()
395+
.await?;
396+
397+
Ok(pubkeys)
398+
}
328399

329-
let total_keys = registry.getTotalSigningKeyCount(node_operator_id).call().await?.try_into()?;
400+
async fn fetch_lido_module_keys_batch<P>(
401+
registry: &LidoRegistry::LidoRegistryInstance<P>,
402+
node_operator_id: U256,
403+
offset: u64,
404+
limit: u64
405+
) -> eyre::Result<Bytes>
406+
where
407+
P: Clone + Send + Sync + 'static + alloy::providers::Provider,
408+
{
409+
let pubkeys = registry
410+
.getSigningKeys(node_operator_id, U256::from(offset), U256::from(limit))
411+
.call()
412+
.await?
413+
.pubkeys;
414+
415+
Ok(pubkeys)
416+
}
330417

418+
async fn collect_registry_keys<F, Fut>(
419+
total_keys: u64,
420+
mut fetch_batch: F,
421+
) -> eyre::Result<Vec<BlsPublicKey>>
422+
where
423+
F: FnMut(u64, u64) -> Fut,
424+
Fut: std::future::Future<Output = eyre::Result<Bytes>>,
425+
{
331426
if total_keys == 0 {
332427
return Ok(Vec::new());
333428
}
334-
335429
debug!("fetching {total_keys} total keys");
336430

337431
const CALL_BATCH_SIZE: u64 = 250u64;
338432

339433
let mut keys = vec![];
340-
let mut offset = 0;
434+
let mut offset: u64 = 0;
341435

342436
while offset < total_keys {
343437
let limit = CALL_BATCH_SIZE.min(total_keys - offset);
344438

345-
let pubkeys = registry
346-
.getSigningKeys(node_operator_id, U256::from(offset), U256::from(limit))
347-
.call()
348-
.await?
349-
.pubkeys;
439+
let pubkeys = fetch_batch(offset, limit).await?;
350440

351441
ensure!(
352442
pubkeys.len() % BLS_PUBLIC_KEY_BYTES_LEN == 0,
@@ -373,6 +463,58 @@ async fn fetch_lido_registry_keys(
373463
Ok(keys)
374464
}
375465

466+
async fn fetch_lido_csm_registry_keys (
467+
registry_address: Address,
468+
rpc_client: RpcClient,
469+
node_operator_id: U256,
470+
) -> eyre::Result<Vec<BlsPublicKey>> {
471+
let provider = ProviderBuilder::new().connect_client(rpc_client);
472+
let registry = get_lido_csm_registry(registry_address, provider);
473+
474+
let total_keys = fetch_lido_csm_keys_total(&registry, node_operator_id).await?.try_into()?;
475+
476+
collect_registry_keys(total_keys, |offset, limit| {
477+
fetch_lido_csm_keys_batch(&registry, node_operator_id, offset, limit)
478+
}).await
479+
}
480+
481+
async fn fetch_lido_module_registry_keys (
482+
registry_address: Address,
483+
rpc_client: RpcClient,
484+
node_operator_id: U256,
485+
) -> eyre::Result<Vec<BlsPublicKey>> {
486+
let provider = ProviderBuilder::new().connect_client(rpc_client);
487+
let registry = get_lido_module_registry(registry_address, provider);
488+
let total_keys: u64 = fetch_lido_module_keys_total(&registry, node_operator_id).await?.try_into()?;
489+
490+
collect_registry_keys(total_keys, |offset, limit| {
491+
fetch_lido_module_keys_batch(&registry, node_operator_id, offset, limit)
492+
}).await
493+
}
494+
495+
async fn fetch_lido_registry_keys(
496+
rpc_url: Url,
497+
chain: Chain,
498+
node_operator_id: U256,
499+
lido_module_id: u8,
500+
http_timeout: Duration,
501+
) -> eyre::Result<Vec<BlsPublicKey>> {
502+
debug!(?chain, %node_operator_id, ?lido_module_id, "loading operator keys from Lido registry");
503+
504+
// Create an RPC provider with HTTP timeout support
505+
let client = Client::builder().timeout(http_timeout).build()?;
506+
let http = Http::with_client(client, rpc_url);
507+
let is_local = http.guess_local();
508+
let rpc_client = RpcClient::new(http, is_local);
509+
let registry_address = lido_registry_address(chain, lido_module_id)?;
510+
511+
if is_csm_module(chain, lido_module_id) {
512+
fetch_lido_csm_registry_keys(registry_address, rpc_client, node_operator_id).await
513+
} else {
514+
fetch_lido_module_registry_keys(registry_address, rpc_client, node_operator_id).await
515+
}
516+
}
517+
376518
async fn fetch_ssv_pubkeys(
377519
chain: Chain,
378520
node_operator_id: U256,
@@ -520,6 +662,49 @@ mod tests {
520662
Ok(())
521663
}
522664

665+
#[tokio::test]
666+
async fn test_lido_csm_registry_address() -> eyre::Result<()> {
667+
use alloy::{primitives::U256, providers::ProviderBuilder};
668+
669+
let url = Url::parse("https://ethereum-rpc.publicnode.com")?;
670+
let provider = ProviderBuilder::new().connect_http(url);
671+
672+
let registry = LidoCSMRegistry::new(
673+
address!("dA7dE2ECdDfccC6c3AF10108Db212ACBBf9EA83F"),
674+
provider,
675+
);
676+
677+
const LIMIT: usize = 3;
678+
let node_operator_id = U256::from(1);
679+
680+
let summary = registry
681+
.getNodeOperatorSummary(node_operator_id)
682+
.call()
683+
.await?;
684+
685+
let total_keys_u256 = summary.totalDepositedValidators + summary.depositableValidatorsCount;
686+
let total_keys: u64 = total_keys_u256.try_into()?;
687+
688+
assert!(total_keys > LIMIT as u64, "expected more than {LIMIT} keys, got {total_keys}");
689+
690+
let pubkeys = registry
691+
.getSigningKeys(node_operator_id, U256::ZERO, U256::from(LIMIT))
692+
.call()
693+
.await?;
694+
695+
let mut vec = Vec::new();
696+
for chunk in pubkeys.chunks(BLS_PUBLIC_KEY_BYTES_LEN) {
697+
vec.push(
698+
BlsPublicKey::deserialize(chunk)
699+
.map_err(|_| eyre::eyre!("invalid BLS public key"))?,
700+
);
701+
}
702+
703+
assert_eq!(vec.len(), LIMIT, "expected {LIMIT} keys, got {}", vec.len());
704+
705+
Ok(())
706+
}
707+
523708
#[tokio::test]
524709
/// Tests that a successful SSV network fetch is handled and parsed properly
525710
async fn test_ssv_network_fetch() -> eyre::Result<()> {

0 commit comments

Comments
 (0)