Skip to content

Commit 56a85a1

Browse files
committed
feat: add Stader validator registry support
Add support for loading validator keys from Stader registries. A new `stader` registry is introduced with explicit pool selection for Stader's permissioned or permissionless node registries. This implementation follows the same high-level structure used for the Lido implementation: resolve the registry contract, fetch the total number of keys and load those keys in batches through the shared registry key collection logic.
1 parent fa81a73 commit 56a85a1

9 files changed

Lines changed: 736 additions & 27 deletions

File tree

Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
[
2+
{
3+
"constant": true,
4+
"inputs": [
5+
{
6+
"name": "_operatorId",
7+
"type": "uint256"
8+
}
9+
],
10+
"name": "operatorStructById",
11+
"outputs": [
12+
{
13+
"name": "active",
14+
"type": "bool"
15+
},
16+
{
17+
"name": "optedForSocializingPool",
18+
"type": "bool"
19+
},
20+
{
21+
"name": "operatorName",
22+
"type": "string"
23+
},
24+
{
25+
"name": "operatorRewardAddress",
26+
"type": "address"
27+
},
28+
{
29+
"name": "operatorAddress",
30+
"type": "address"
31+
}
32+
],
33+
"payable": false,
34+
"stateMutability": "view",
35+
"type": "function"
36+
},
37+
{
38+
"constant": true,
39+
"inputs": [
40+
{
41+
"name": "_operator",
42+
"type": "address"
43+
}
44+
],
45+
"name": "operatorIDByAddress",
46+
"outputs": [
47+
{
48+
"name": "",
49+
"type": "uint256"
50+
}
51+
],
52+
"payable": false,
53+
"stateMutability": "view",
54+
"type": "function"
55+
},
56+
{
57+
"constant": true,
58+
"inputs": [
59+
{
60+
"name": "_operatorId",
61+
"type": "uint256"
62+
}
63+
],
64+
"name": "getOperatorTotalKeys",
65+
"outputs": [
66+
{
67+
"name": "_totalKeys",
68+
"type": "uint256"
69+
}
70+
],
71+
"payable": false,
72+
"stateMutability": "view",
73+
"type": "function"
74+
},
75+
{
76+
"constant": true,
77+
"inputs": [
78+
{
79+
"name": "_operator",
80+
"type": "address"
81+
},
82+
{
83+
"name": "_pageNumber",
84+
"type": "uint256"
85+
},
86+
{
87+
"name": "_pageSize",
88+
"type": "uint256"
89+
}
90+
],
91+
"name": "getValidatorsByOperator",
92+
"outputs": [
93+
{
94+
"name": "",
95+
"type": "tuple[]",
96+
"components": [
97+
{
98+
"name": "status",
99+
"type": "uint8"
100+
},
101+
{
102+
"name": "pubkey",
103+
"type": "bytes"
104+
},
105+
{
106+
"name": "preDepositSignature",
107+
"type": "bytes"
108+
},
109+
{
110+
"name": "depositSignature",
111+
"type": "bytes"
112+
},
113+
{
114+
"name": "withdrawVaultAddress",
115+
"type": "address"
116+
},
117+
{
118+
"name": "operatorId",
119+
"type": "uint256"
120+
},
121+
{
122+
"name": "depositBlock",
123+
"type": "uint256"
124+
},
125+
{
126+
"name": "withdrawnBlock",
127+
"type": "uint256"
128+
}
129+
]
130+
}
131+
],
132+
"payable": false,
133+
"stateMutability": "view",
134+
"type": "function"
135+
},
136+
{
137+
"constant": true,
138+
"inputs": [
139+
{
140+
"name": "_operatorId",
141+
"type": "uint256"
142+
},
143+
{
144+
"name": "_index",
145+
"type": "uint256"
146+
}
147+
],
148+
"name": "validatorIdsByOperatorId",
149+
"outputs": [
150+
{
151+
"name": "",
152+
"type": "uint256"
153+
}
154+
],
155+
"payable": false,
156+
"stateMutability": "view",
157+
"type": "function"
158+
},
159+
{
160+
"constant": true,
161+
"inputs": [
162+
{
163+
"name": "_validatorId",
164+
"type": "uint256"
165+
}
166+
],
167+
"name": "validatorRegistry",
168+
"outputs": [
169+
{
170+
"name": "status",
171+
"type": "uint8"
172+
},
173+
{
174+
"name": "pubkey",
175+
"type": "bytes"
176+
},
177+
{
178+
"name": "preDepositSignature",
179+
"type": "bytes"
180+
},
181+
{
182+
"name": "depositSignature",
183+
"type": "bytes"
184+
},
185+
{
186+
"name": "withdrawVaultAddress",
187+
"type": "address"
188+
},
189+
{
190+
"name": "operatorId",
191+
"type": "uint256"
192+
},
193+
{
194+
"name": "depositBlock",
195+
"type": "uint256"
196+
},
197+
{
198+
"name": "withdrawnBlock",
199+
"type": "uint256"
200+
}
201+
],
202+
"payable": false,
203+
"stateMutability": "view",
204+
"type": "function"
205+
},
206+
{
207+
"constant": true,
208+
"inputs": [
209+
{
210+
"name": "_operatorId",
211+
"type": "uint256"
212+
}
213+
],
214+
"name": "getOperatorRewardAddress",
215+
"outputs": [
216+
{
217+
"name": "",
218+
"type": "address"
219+
}
220+
],
221+
"payable": false,
222+
"stateMutability": "view",
223+
"type": "function"
224+
},
225+
{
226+
"constant": true,
227+
"inputs": [
228+
{
229+
"name": "_operAddr",
230+
"type": "address"
231+
}
232+
],
233+
"name": "isExistingOperator",
234+
"outputs": [
235+
{
236+
"name": "",
237+
"type": "bool"
238+
}
239+
],
240+
"payable": false,
241+
"stateMutability": "view",
242+
"type": "function"
243+
}
244+
]

crates/common/src/config/mux.rs

Lines changed: 82 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,10 @@ use tracing::{debug, info, warn};
1919
use url::Url;
2020

2121
use super::{MUX_PATH_ENV, PbsConfig, RelayConfig, load_optional_env_var};
22+
use crate::types::StaderPool;
2223
use crate::{
2324
config::{remove_duplicate_keys, safe_read_http_response},
24-
interop::{lido::utils::*, ssv::utils::*},
25+
interop::{lido::utils::*, ssv::utils::*, stader::utils::*},
2526
pbs::RelayClient,
2627
types::{BlsPublicKey, Chain},
2728
utils::default_bool,
@@ -194,6 +195,8 @@ pub enum MuxKeysLoader {
194195
node_operator_id: u64,
195196
#[serde(default)]
196197
lido_module_id: Option<u8>,
198+
#[serde(default)]
199+
stader_pool: Option<StaderPool>,
197200
#[serde(default = "default_bool::<false>")]
198201
enable_refreshing: bool,
199202
},
@@ -205,6 +208,8 @@ pub enum NORegistry {
205208
Lido,
206209
#[serde(alias = "ssv")]
207210
SSV,
211+
#[serde(alias = "stader")]
212+
Stader,
208213
}
209214

210215
impl MuxKeysLoader {
@@ -240,33 +245,56 @@ impl MuxKeysLoader {
240245
.wrap_err("failed to fetch mux keys from HTTP endpoint")
241246
}
242247

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-
}
248+
Self::Registry {
249+
registry,
250+
node_operator_id,
251+
lido_module_id,
252+
stader_pool,
253+
enable_refreshing: _,
254+
..
255+
} => match registry {
256+
NORegistry::Lido => {
257+
let Some(rpc_url) = rpc_url else {
258+
bail!("Lido registry requires RPC URL to be set in the PBS config");
259+
};
260+
261+
fetch_lido_registry_keys(
262+
rpc_url,
263+
chain,
264+
U256::from(*node_operator_id),
265+
lido_module_id.unwrap_or(1),
266+
http_timeout,
267+
)
268+
.await
268269
}
269-
}
270+
NORegistry::SSV => {
271+
fetch_ssv_pubkeys(
272+
ssv_api_url,
273+
chain,
274+
U256::from(*node_operator_id),
275+
http_timeout,
276+
)
277+
.await
278+
}
279+
NORegistry::Stader => {
280+
let Some(rpc_url) = rpc_url else {
281+
bail!("Stader registry requires RPC URL to be set in the PBS config");
282+
};
283+
284+
let Some(stader_pool) = stader_pool else {
285+
bail!("Stader registry requires `stader_pool` to be set in the mux config");
286+
};
287+
288+
fetch_stader_registry_keys(
289+
rpc_url,
290+
chain,
291+
stader_pool.clone(),
292+
U256::from(*node_operator_id),
293+
http_timeout,
294+
)
295+
.await
296+
}
297+
},
270298
}?;
271299

272300
// Remove duplicates
@@ -390,6 +418,33 @@ async fn fetch_lido_registry_keys(
390418
}
391419
}
392420

421+
async fn fetch_stader_registry_keys(
422+
rpc_url: Url,
423+
chain: Chain,
424+
stader_pool: StaderPool,
425+
node_operator_id: U256,
426+
http_timeout: Duration,
427+
) -> eyre::Result<Vec<BlsPublicKey>> {
428+
let client = Client::builder().timeout(http_timeout).build()?;
429+
let http = Http::with_client(client, rpc_url);
430+
let is_local = http.guess_local();
431+
let rpc_client = RpcClient::new(http, is_local);
432+
433+
let registry_address = stader_registry_address(chain, stader_pool)?;
434+
435+
let provider = ProviderBuilder::new().connect_client(rpc_client);
436+
let registry = get_stader_registry(registry_address, provider);
437+
438+
let operator_address = fetch_stader_operator_address(&registry, node_operator_id).await?;
439+
440+
let total_keys = fetch_stader_keys_total(&registry, node_operator_id).await?;
441+
442+
collect_registry_keys(total_keys, |offset, limit| {
443+
fetch_stader_keys_batch(&registry, operator_address, offset, limit)
444+
})
445+
.await
446+
}
447+
393448
async fn fetch_ssv_pubkeys(
394449
mut api_url: Url,
395450
chain: Chain,

crates/common/src/interop/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
pub mod lido;
22
pub mod ssv;
3+
pub mod stader;
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;

0 commit comments

Comments
 (0)