Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions anchor/client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ name = "client"
path = "src/lib.rs"

[dependencies]
alloy = { workspace = true }
anchor_validator_store = { workspace = true }
beacon_node_fallback = { workspace = true }
clap = { workspace = true }
Expand Down
61 changes: 61 additions & 0 deletions anchor/client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,14 @@ impl Client {
let index_sync_tx =
start_validator_index_syncer(beacon_nodes.clone(), database.clone(), executor.clone());

// Fetch node versions from beacon and execution nodes before moving config fields
let (execution_version, consensus_version) = fetch_node_versions(
&beacon_nodes,
&config.execution_nodes,
&config.execution_nodes_websocket,
)
.await?;

// We create the channel here so that we can pass the receiver to the syncer. But we need to
// delay starting the voluntary exit processor until we have created the validator store.
let (exit_tx, exit_rx) = unbounded_channel();
Expand Down Expand Up @@ -471,6 +479,15 @@ impl Client {
message_validator,
);

// Create NodeMetadata with fetched versions
// (subnets will be set dynamically in Network::try_new)
let node_metadata = network::NodeMetadata {
node_version: version::version_with_platform(),
execution_node: execution_version,
consensus_node: consensus_version,
subnets: String::new(),
};

// Start the p2p network
let mut network = Network::try_new::<E>(
&config.network,
Expand All @@ -480,6 +497,7 @@ impl Client {
outcome_rx,
executor.clone(),
spec.clone(),
node_metadata,
)
.await
.map_err(|e| format!("Unable to start network: {e}"))?;
Expand Down Expand Up @@ -776,6 +794,49 @@ async fn wait_for_genesis(genesis_time: u64) -> Result<(), String> {
Ok(())
}

/// Fetches node version information from beacon and execution nodes
async fn fetch_node_versions<T: SlotClock>(
beacon_nodes: &BeaconNodeFallback<T>,
execution_http_urls: &[SensitiveUrl],
execution_ws_url: &SensitiveUrl,
) -> Result<(String, String), String> {
Comment on lines +797 to +802
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO it is unclear if we actually want to do this. e.g. Go-SSV just leaves the node version in the handshake empty

use alloy::providers::{Provider, ProviderBuilder};

// Get consensus (beacon) node version via GET /eth/v1/node/version
let consensus_version = beacon_nodes
.first_success(|node| async move { node.get_node_version().await })
.await
.map(|version_data| version_data.data.version)
.map_err(|e| format!("Failed to fetch beacon node version: {e}"))?;

// Get execution node version via web3_clientVersion JSON-RPC
let execution_version = if let Some(url) = execution_http_urls.first() {
let provider = ProviderBuilder::new().connect_http(url.full.clone());
provider
.get_client_version()
.await
.map_err(|e| format!("Failed to fetch execution node version: {e}"))?
} else {
return Err("No execution node HTTP URLs configured".to_string());
};

// Verify execution websocket URL is valid
if execution_ws_url.full.scheme() != "ws" && execution_ws_url.full.scheme() != "wss" {
return Err(format!(
"Execution websocket URL must use ws:// or wss:// scheme, got: {}",
execution_ws_url.full.scheme()
));
}

info!(
execution = %execution_version,
consensus = %consensus_version,
"Fetched node version information"
);

Ok((execution_version, consensus_version))
}

pub fn load_pem_certificate<P: AsRef<Path>>(pem_path: P) -> Result<Certificate, String> {
let mut buf = Vec::new();
File::open(&pem_path)
Expand Down
135 changes: 135 additions & 0 deletions anchor/network/src/handshake/node_info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -223,4 +223,139 @@ mod tests {
// We can do the same in Rust using assert_eq.
assert_eq!(old_format, parsed_rec);
}

#[test]
fn test_subnet_encoding_length() {
use subnet_service::SubnetBits;

// Test that hex encoding always produces exactly 32 characters
let empty_subnets: SubnetBits = [0; 16];
assert_eq!(hex::encode(empty_subnets).len(), 32);
assert_eq!(
hex::encode(empty_subnets),
"00000000000000000000000000000000"
);

let full_subnets: SubnetBits = [0xFF; 16];
assert_eq!(hex::encode(full_subnets).len(), 32);
assert_eq!(
hex::encode(full_subnets),
"ffffffffffffffffffffffffffffffff"
);
}

#[test]
fn test_subnet_bit_encoding_matches_go_client() {
use subnet_service::SubnetBits;

// Test that our bit encoding matches the Go client format
// Subnet 0 should set bit 0 of byte 0
let mut subnet_bits: SubnetBits = [0; 16];
subnet_bits[0] |= 1 << 0;
assert_eq!(hex::encode(subnet_bits), "01000000000000000000000000000000");

// Subnet 8 should set bit 0 of byte 1
subnet_bits = [0; 16];
subnet_bits[1] |= 1 << 0;
assert_eq!(hex::encode(subnet_bits), "00010000000000000000000000000000");

// Subnet 127 should set bit 7 of byte 15
subnet_bits = [0; 16];
subnet_bits[15] |= 1 << 7;
assert_eq!(hex::encode(subnet_bits), "00000000000000000000000000000080");
}

#[test]
fn test_set_subscribed_single_subnet() {
use subnet_service::SubnetId;

let mut metadata = NodeMetadata {
node_version: "test".to_string(),
execution_node: "test".to_string(),
consensus_node: "test".to_string(),
subnets: "00000000000000000000000000000000".to_string(),
};

// Subscribe to subnet 5
metadata.set_subscribed(SubnetId::new(5), true).unwrap();

// Verify bit 5 is set in byte 0
let mut expected: [u8; 16] = [0; 16];
expected[0] = 1 << 5;
assert_eq!(metadata.subnets, hex::encode(expected));
}

#[test]
fn test_set_subscribed_multiple_subnets() {
use subnet_service::SubnetId;

let mut metadata = NodeMetadata {
node_version: "test".to_string(),
execution_node: "test".to_string(),
consensus_node: "test".to_string(),
subnets: "00000000000000000000000000000000".to_string(),
};

// Subscribe to subnets 0, 8, and 127
metadata.set_subscribed(SubnetId::new(0), true).unwrap();
metadata.set_subscribed(SubnetId::new(8), true).unwrap();
metadata.set_subscribed(SubnetId::new(127), true).unwrap();

// Verify correct bits are set
let mut expected: [u8; 16] = [0; 16];
expected[0] = 1 << 0; // subnet 0
expected[1] = 1 << 0; // subnet 8
expected[15] = 1 << 7; // subnet 127
assert_eq!(metadata.subnets, hex::encode(expected));
}

#[test]
fn test_count_matching_subnets() {
// Helper to count matching bits (same logic as in network.rs)
fn count_matches(our_subnets: &str, their_subnets: &str) -> usize {
let our_bytes = hex::decode(our_subnets).unwrap();
let their_bytes = hex::decode(their_subnets).unwrap();
our_bytes
.iter()
.zip(their_bytes.iter())
.map(|(a, b)| (a & b).count_ones() as usize)
.sum()
}

// No matches
assert_eq!(
count_matches(
"01000000000000000000000000000000", // subnet 0
"00010000000000000000000000000000" // subnet 8
),
0
);

// One match
assert_eq!(
count_matches(
"01000000000000000000000000000000", // subnet 0
"01000000000000000000000000000000" // subnet 0
),
1
);

// Multiple matches
assert_eq!(
count_matches(
"03000000000000000000000000000000", // subnets 0 and 1
"01000000000000000000000000000000" // subnet 0
),
1
);

// All match
assert_eq!(
count_matches(
"ffffffffffffffffffffffffffffffff",
"ffffffffffffffffffffffffffffffff"
),
128
);
}
}
1 change: 1 addition & 0 deletions anchor/network/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ mod scoring;
mod transport;

pub use config::{Config, DEFAULT_DISC_PORT, DEFAULT_QUIC_PORT, DEFAULT_TCP_PORT};
pub use handshake::node_info::NodeMetadata;
pub use network::Network;
pub use network_utils::listen_addr::{ListenAddr, ListenAddress};
pub type Enr = discv5::enr::Enr<discv5::enr::CombinedKey>;
22 changes: 22 additions & 0 deletions anchor/network/src/metrics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,25 @@ use metrics::*;
pub static PEERS_CONNECTED: LazyLock<Result<IntGauge>> = LazyLock::new(|| {
try_create_int_gauge("libp2p_peers", "Count of libp2p peers currently connected")
});

pub static HANDSHAKE_SUCCESSFUL: LazyLock<Result<IntCounter>> = LazyLock::new(|| {
try_create_int_counter(
"libp2p_handshake_successful_total",
"Total count of successful handshakes",
)
});

pub static HANDSHAKE_FAILED: LazyLock<Result<IntCounter>> = LazyLock::new(|| {
try_create_int_counter(
"libp2p_handshake_failed_total",
"Total count of failed handshakes",
)
});

pub static HANDSHAKE_SUBNET_MATCHES: LazyLock<Result<IntGaugeVec>> = LazyLock::new(|| {
try_create_int_gauge_vec(
"libp2p_handshake_subnet_matches",
"Count of successful handshakes by number of matching subnets",
&["match_count"],
)
});
Comment on lines +9 to +29
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Metrics for the handshake is not really what this PR advertises to do, please open a separate PR

Loading
Loading