Skip to content

Commit 76e0134

Browse files
feat(mux): support SSV network (#223)
Co-authored-by: ltitanb <163874448+ltitanb@users.noreply.github.com>
1 parent 4c370ea commit 76e0134

File tree

3 files changed

+91
-1
lines changed

3 files changed

+91
-1
lines changed

config.example.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,9 +114,11 @@ validator_pubkeys = [
114114
# Path to a file containing a list of validator pubkeys or details of a registry to load keys from.
115115
# Supported registries:
116116
# - Lido: NodeOperatorsRegistry
117+
# - SSV: SSV API
117118
# OPTIONAL
118119
loader = "./tests/data/mux_keys.example.json"
119120
# loader = { registry = "lido", node_operator_id = 8 }
121+
# loader = { registry = "ssv", node_operator_id = 8 }
120122
timeout_get_header_ms = 900
121123
late_in_slot_time_ms = 1500
122124
# For each mux, one or more [[mux.relays]] can be defined, which will be used for the matching validator pubkeys

crates/common/src/config/mux.rs

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,8 @@ pub enum MuxKeysLoader {
149149
pub enum NORegistry {
150150
#[serde(alias = "lido")]
151151
Lido,
152+
#[serde(alias = "ssv")]
153+
SSV,
152154
}
153155

154156
impl MuxKeysLoader {
@@ -176,6 +178,7 @@ impl MuxKeysLoader {
176178

177179
fetch_lido_registry_keys(rpc_url, chain, U256::from(*node_operator_id)).await
178180
}
181+
NORegistry::SSV => fetch_ssv_pubkeys(chain, U256::from(*node_operator_id)).await,
179182
},
180183
}
181184
}
@@ -266,6 +269,71 @@ async fn fetch_lido_registry_keys(
266269
Ok(keys)
267270
}
268271

272+
async fn fetch_ssv_pubkeys(
273+
chain: Chain,
274+
node_operator_id: U256,
275+
) -> eyre::Result<Vec<BlsPublicKey>> {
276+
const MAX_PER_PAGE: usize = 100;
277+
278+
let chain_name = match chain {
279+
Chain::Mainnet => "mainnet",
280+
Chain::Holesky => "holesky",
281+
_ => bail!("SSV network is not supported for chain: {chain:?}"),
282+
};
283+
284+
let client = reqwest::Client::new();
285+
let mut pubkeys: Vec<BlsPublicKey> = vec![];
286+
let mut page = 1;
287+
288+
loop {
289+
let response = client
290+
.get(format!(
291+
"https://api.ssv.network/api/v4/{}/validators/in_operator/{}?perPage={}&page={}",
292+
chain_name, node_operator_id, MAX_PER_PAGE, page
293+
))
294+
.send()
295+
.await
296+
.map_err(|e| eyre::eyre!("Error sending request to SSV network API: {e}"))?
297+
.json::<SSVResponse>()
298+
.await?;
299+
300+
pubkeys.extend(response.validators.iter().map(|v| v.pubkey).collect::<Vec<BlsPublicKey>>());
301+
page += 1;
302+
303+
if response.validators.len() < MAX_PER_PAGE {
304+
ensure!(
305+
pubkeys.len() == response.pagination.total,
306+
"expected {} keys, got {}",
307+
response.pagination.total,
308+
pubkeys.len()
309+
);
310+
break;
311+
}
312+
}
313+
314+
let unique = pubkeys.iter().collect::<HashSet<_>>();
315+
ensure!(unique.len() == pubkeys.len(), "found duplicate keys in registry");
316+
317+
Ok(pubkeys)
318+
}
319+
320+
#[derive(Deserialize)]
321+
struct SSVResponse {
322+
validators: Vec<SSVValidator>,
323+
pagination: SSVPagination,
324+
}
325+
326+
#[derive(Deserialize)]
327+
struct SSVValidator {
328+
#[serde(rename = "public_key")]
329+
pubkey: BlsPublicKey,
330+
}
331+
332+
#[derive(Deserialize)]
333+
struct SSVPagination {
334+
total: usize,
335+
}
336+
269337
#[cfg(test)]
270338
mod tests {
271339
use alloy::{primitives::U256, providers::ProviderBuilder};
@@ -304,4 +372,16 @@ mod tests {
304372

305373
Ok(())
306374
}
375+
376+
#[tokio::test]
377+
async fn test_ssv_network_fetch() -> eyre::Result<()> {
378+
let chain = Chain::Holesky;
379+
let node_operator_id = U256::from(200);
380+
381+
let pubkeys = fetch_ssv_pubkeys(chain, node_operator_id).await?;
382+
383+
assert_eq!(pubkeys.len(), 3);
384+
385+
Ok(())
386+
}
307387
}

examples/configs/pbs_mux.toml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ id = "timing-mux"
1919
validator_pubkeys = [
2020
"0x80c7f782b2467c5898c5516a8b6595d75623960b4afc4f71ee07d40985d20e117ba35e7cd352a3e75fb85a8668a3b745",
2121
]
22-
loader = "./mux_keys.example.json"
22+
loader = "./tests/data/mux_keys.example.json"
2323
timeout_get_header_ms = 900
2424
late_in_slot_time_ms = 1500
2525

@@ -38,3 +38,11 @@ loader = { registry = "lido", node_operator_id = 8 }
3838
[[mux.relays]]
3939
id = "relay-3"
4040
url = "http://0x80c7f782b2467c5898c5516a8b6595d75623960b4afc4f71ee07d40985d20e117ba35e7cd352a3e75fb85a8668a3b745@fgh.xyz"
41+
42+
[[mux]]
43+
id = "ssv-mux"
44+
loader = { registry = "ssv", node_operator_id = 200 }
45+
46+
[[mux.relays]]
47+
id = "relay-4"
48+
url = "http://0x80c7f782b2467c5898c5516a8b6595d75623960b4afc4f71ee07d40985d20e117ba35e7cd352a3e75fb85a8668a3b745@fgh.xyz"

0 commit comments

Comments
 (0)