@@ -30,6 +30,10 @@ lazy_static! {
3030 MainnetLidoModule :: CommunityStaking as u8 ,
3131 address!( "dA7dE2ECdDfccC6c3AF10108Db212ACBBf9EA83F" ) ,
3232 ) ;
33+ mainnet. insert(
34+ MainnetLidoModule :: CuratedV2 as u8 ,
35+ address!( "Da5F930cE326EB5205085D66c72A4E79d60cB8C1" ) ,
36+ ) ;
3337 map. insert( Chain :: Mainnet , mainnet) ;
3438
3539 // --- Holesky ---
@@ -70,6 +74,10 @@ lazy_static! {
7074 HoodiLidoModule :: CommunityStaking as u8 ,
7175 address!( "79CEf36D84743222f37765204Bec41E92a93E59d" ) ,
7276 ) ;
77+ hoodi. insert(
78+ HoodiLidoModule :: CuratedV2 as u8 ,
79+ address!( "87EB69Ae51317405FD285efD2326a4a11f6173b9" ) ,
80+ ) ;
7381 map. insert( Chain :: Hoodi , hoodi) ;
7482
7583 // --- Sepolia --
@@ -93,11 +101,25 @@ pub fn lido_registry_address(chain: Chain, lido_module_id: u8) -> eyre::Result<A
93101 } )
94102}
95103
96- pub fn is_csm_module ( chain : Chain , module_id : u8 ) -> bool {
104+ /// Lido staking modules expose one of two node operator registry interfaces.
105+ ///
106+ /// The Community Staking Module and the Curated Module v2
107+ /// (`curated-onchain-v2`) share the CSM interface: `getSigningKeys` returns a
108+ /// flat `bytes` blob and the key total is derived from
109+ /// `getNodeOperatorSummary`. Every other module is a `NodeOperatorsRegistry`
110+ /// (curated v1, SimpleDVT, Sandbox), which returns a struct from
111+ /// `getSigningKeys` and exposes `getTotalSigningKeyCount`.
112+ pub fn uses_csm_registry_interface ( chain : Chain , module_id : u8 ) -> bool {
97113 match chain {
98- Chain :: Mainnet => module_id == MainnetLidoModule :: CommunityStaking as u8 ,
114+ Chain :: Mainnet => {
115+ module_id == MainnetLidoModule :: CommunityStaking as u8 ||
116+ module_id == MainnetLidoModule :: CuratedV2 as u8
117+ }
99118 Chain :: Holesky => module_id == HoleskyLidoModule :: CommunityStaking as u8 ,
100- Chain :: Hoodi => module_id == HoodiLidoModule :: CommunityStaking as u8 ,
119+ Chain :: Hoodi => {
120+ module_id == HoodiLidoModule :: CommunityStaking as u8 ||
121+ module_id == HoodiLidoModule :: CuratedV2 as u8
122+ }
101123 _ => false ,
102124 }
103125}
@@ -268,4 +290,87 @@ mod tests {
268290
269291 Ok ( ( ) )
270292 }
293+
294+ #[ test]
295+ fn test_lido_curated_v2_registry_address ( ) -> eyre:: Result < ( ) > {
296+ assert_eq ! (
297+ lido_registry_address( Chain :: Mainnet , MainnetLidoModule :: CuratedV2 as u8 ) ?,
298+ address!( "Da5F930cE326EB5205085D66c72A4E79d60cB8C1" )
299+ ) ;
300+
301+ assert_eq ! (
302+ lido_registry_address( Chain :: Hoodi , HoodiLidoModule :: CuratedV2 as u8 ) ?,
303+ address!( "87EB69Ae51317405FD285efD2326a4a11f6173b9" )
304+ ) ;
305+
306+ // CMv2 shares the CSM read interface, so it must be routed to it
307+ assert ! ( uses_csm_registry_interface( Chain :: Mainnet , MainnetLidoModule :: CuratedV2 as u8 ) ) ;
308+ assert ! ( uses_csm_registry_interface( Chain :: Hoodi , HoodiLidoModule :: CuratedV2 as u8 ) ) ;
309+
310+ // Curated v1 still uses the NodeOperatorsRegistry interface
311+ assert ! ( !uses_csm_registry_interface( Chain :: Mainnet , MainnetLidoModule :: Curated as u8 ) ) ;
312+ assert ! ( !uses_csm_registry_interface( Chain :: Hoodi , HoodiLidoModule :: Curated as u8 ) ) ;
313+
314+ // CMv2 is not deployed on Holesky
315+ assert ! ( lido_registry_address( Chain :: Holesky , 5 ) . is_err( ) ) ;
316+
317+ Ok ( ( ) )
318+ }
319+
320+ /// Reads the first keys of a CMv2 node operator through the same helpers
321+ /// the mux loader uses.
322+ async fn assert_curated_v2_keys (
323+ rpc_url : & str ,
324+ chain : Chain ,
325+ module_id : u8 ,
326+ ) -> eyre:: Result < ( ) > {
327+ let url = Url :: parse ( rpc_url) ?;
328+ let provider = ProviderBuilder :: new ( ) . connect_http ( url) ;
329+
330+ let registry_address = lido_registry_address ( chain, module_id) ?;
331+ let registry = get_lido_csm_registry ( registry_address, provider) ;
332+
333+ const LIMIT : u64 = 3 ;
334+ let node_operator_id = U256 :: ZERO ;
335+
336+ let total_keys = fetch_lido_csm_keys_total ( & registry, node_operator_id) . await ?;
337+
338+ assert ! ( total_keys >= LIMIT , "expected at least {LIMIT} keys, got {total_keys}" ) ;
339+
340+ let pubkeys = fetch_lido_csm_keys_batch ( & registry, node_operator_id, 0 , LIMIT ) . await ?;
341+
342+ assert_eq ! ( pubkeys. len( ) , LIMIT as usize * BLS_PUBLIC_KEY_BYTES_LEN ) ;
343+
344+ let mut vec = Vec :: new ( ) ;
345+ for chunk in pubkeys. chunks ( BLS_PUBLIC_KEY_BYTES_LEN ) {
346+ vec. push (
347+ BlsPublicKey :: deserialize ( chunk)
348+ . map_err ( |_| eyre:: eyre!( "invalid BLS public key" ) ) ?,
349+ ) ;
350+ }
351+
352+ assert_eq ! ( vec. len( ) , LIMIT as usize ) ;
353+
354+ Ok ( ( ) )
355+ }
356+
357+ #[ tokio:: test]
358+ async fn test_lido_curated_v2_mainnet_keys ( ) -> eyre:: Result < ( ) > {
359+ assert_curated_v2_keys (
360+ "https://ethereum-rpc.publicnode.com" ,
361+ Chain :: Mainnet ,
362+ MainnetLidoModule :: CuratedV2 as u8 ,
363+ )
364+ . await
365+ }
366+
367+ #[ tokio:: test]
368+ async fn test_lido_curated_v2_hoodi_keys ( ) -> eyre:: Result < ( ) > {
369+ assert_curated_v2_keys (
370+ "https://ethereum-hoodi-rpc.publicnode.com" ,
371+ Chain :: Hoodi ,
372+ HoodiLidoModule :: CuratedV2 as u8 ,
373+ )
374+ . await
375+ }
271376}
0 commit comments