Skip to content
Open
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
2 changes: 1 addition & 1 deletion Cargo.lock

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

38 changes: 38 additions & 0 deletions bin/ethlambda/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,36 @@ fn main() -> eyre::Result<()> {
}
}

/// What `/eth/v1/node/identity` reports beyond the peer id: the ENR discv5
/// publishes, and the multiaddrs peers reach this node on. The addresses need
/// the node's externally reachable IP, which only `--discovery.advertise-ip`
/// supplies (a node bound to `0.0.0.0` does not know it), so without the flag
/// they are left empty rather than guessed.
fn beacon_identity(
enr: &str,
advertise_ip: Option<IpAddr>,
gossipsub_port: u16,
discovery_port: u16,
peer_id: &str,
) -> ethlambda_rpc::BeaconIdentity {
let Some(ip) = advertise_ip else {
return ethlambda_rpc::BeaconIdentity {
enr: enr.to_string(),
..Default::default()
};
};
let family = if ip.is_ipv4() { "ip4" } else { "ip6" };
ethlambda_rpc::BeaconIdentity {
enr: enr.to_string(),
// QUIC first, matching the order this node dials in.
p2p_addresses: vec![
format!("/{family}/{ip}/udp/{gossipsub_port}/quic-v1/p2p/{peer_id}"),
format!("/{family}/{ip}/tcp/{gossipsub_port}/p2p/{peer_id}"),
],
discovery_addresses: vec![format!("/{family}/{ip}/udp/{discovery_port}/p2p/{peer_id}")],
}
}

/// Node logging: INFO and above, on stdout.
fn init_node_logging() -> eyre::Result<()> {
let filter = EnvFilter::builder()
Expand Down Expand Up @@ -727,6 +757,13 @@ async fn run_node(options: Options) -> eyre::Result<()> {
let rpc_sync_status = sync_status.clone();
let rpc_events = events.clone();
let rpc_p2p = p2p.actor_ref().to_rpc_to_p2p_ref();
let rpc_identity = beacon_identity(
p2p.local_enr().unwrap_or_default(),
common.discovery.advertise_ip,
common.gossipsub_port,
common.discovery.port,
&local_peer_id,
);
// Block production builds its payloads with the same execution client the
// chain actor validates them with.
let rpc_engine = match &setup.chain {
Expand All @@ -751,6 +788,7 @@ async fn run_node(options: Options) -> eyre::Result<()> {
p2p: rpc_p2p,
attestation_pool: attestation_pool.clone(),
engine: rpc_engine,
identity: rpc_identity,
},
local_peer_id,
rpc_shutdown,
Expand Down
3 changes: 3 additions & 0 deletions bin/ethlambda/src/validator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ pub(crate) struct ValidatorOptions {
pub(crate) beacon_nodes: Vec<String>,

/// Directory holding the EIP-2335 keystores and `validator_definitions.yml`.
/// Without that file, keystores in the Lighthouse layout
/// (`<0xpubkey>/voting-keystore.json`, password in
/// `--secrets-dir/<0xpubkey>`) are discovered and the file is written.
#[arg(long)]
pub(crate) validators_dir: PathBuf,

Expand Down
13 changes: 12 additions & 1 deletion crates/net/p2p/src/discovery/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,15 @@ pub async fn spawn_discovery(
};
let local_node = params.local_node();
let local_record = build_local_enr(&params)?;
let local_enr = local_record.enr_url().map_err(DiscoveryError::EncodeEnr)?;
// EIP-778's text form is URL-safe base64 *without* padding. ethrex's
// `enr_url` pads, and lighthouse refuses a padded record outright ("Not
// valid as ENR nor Multiaddr"), which is the form this string is handed to
// peers in: `/eth/v1/node/identity`, and through it devnet bootnode lists.
let local_enr = local_record
.enr_url()
.map_err(DiscoveryError::EncodeEnr)?
.trim_end_matches('=')
.to_string();

// `spawn` rather than `spawn_with_filter` would install ethrex's own filter,
// which wants an EIP-2124 `eth` entry compatible with an execution chain lean
Expand Down Expand Up @@ -304,6 +312,9 @@ mod tests {
.expect("discovery spawns");

assert!(handle.local_enr.starts_with("enr:"));
// EIP-778's text form carries no base64 padding, and lighthouse
// refuses a record that does.
assert!(!handle.local_enr.contains('='), "{}", handle.local_enr);
let record = decode_enr(&handle.local_enr);

// With discovery_port: 0 the OS picks the real port, and the published
Expand Down
23 changes: 19 additions & 4 deletions crates/net/p2p/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1020,6 +1020,9 @@ pub fn build_swarm(config: SwarmConfig) -> Result<BuiltSwarm, SwarmBuildError> {
/// Public handle to the P2P actor.
pub struct P2P {
handle: ActorRef<P2PServer>,
/// This node's ENR as published at startup, for the Beacon API's
/// `/eth/v1/node/identity`. `None` when discovery is disabled.
local_enr: Option<String>,
}

impl P2P {
Expand All @@ -1044,11 +1047,15 @@ impl P2P {
discovery: Option<DiscoverySpawnConfig>,
attestation_pool: SharedAttestationPool,
) -> Result<P2P, DiscoveryError> {
let discovery = match discovery {
Some(config) => Some(spawn_discovery(config).await?),
let (discovery, local_enr) = match discovery {
Some(config) => {
let discovery = spawn_discovery(config).await?;
let local_enr = discovery.local_enr.clone();
(Some(discovery), Some(local_enr))
}
None => {
info!("discv5 discovery disabled; peering from the static bootnode list only");
None
(None, None)
}
};
let (swarm_stream, swarm_handle) =
Expand Down Expand Up @@ -1118,12 +1125,20 @@ impl P2P {
);
}
spawn_listener(handle.context(), swarm_stream.map(WrappedSwarmEvent));
Ok(P2P { handle })
Ok(P2P { handle, local_enr })
}

pub fn actor_ref(&self) -> &ActorRef<P2PServer> {
&self.handle
}

/// This node's ENR, `enr:`-prefixed, as published at startup. discv5 may
/// re-sign it later with a higher sequence number if IP voting changes the
/// external address; this is the startup record. `None` when discovery is
/// disabled, since then no ENR is published.
pub fn local_enr(&self) -> Option<&str> {
self.local_enr.as_deref()
}
}

/// Message wrapper for swarm events. Not part of the protocol because
Expand Down
59 changes: 46 additions & 13 deletions crates/net/rpc/src/beacon/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,12 @@ pub(crate) fn routes(version: &'static str, peer_id: String) -> Router<Store> {
.route("/eth/v1/node/version", get(move || get_version(version)))
.route(
"/eth/v1/node/identity",
get(move || get_identity(peer_id.clone())),
get(move |identity: Option<Extension<crate::BeaconIdentity>>| {
get_identity(
peer_id.clone(),
identity.map(|Extension(identity)| identity),
)
}),
)
}

Expand Down Expand Up @@ -69,18 +74,21 @@ async fn get_version(version: &'static str) -> Response {
crate::json_response(serde_json::json!({ "data": { "version": version } }))
}

async fn get_identity(peer_id: String) -> Response {
// `enr` and the two address lists are empty, which is not spec-valid: the
// ENR is built for discv5 and owned by the P2P actor, and `BuiltSwarm`
// hands `run_node` only a `local_peer_id`. Serving the record means
// widening the `ethlambda-p2p` surface and threading it through startup,
// which is a change of its own. Recorded in docs/spec_deviations.md.
/// `GET /eth/v1/node/identity`: the peer id, the node's ENR, and the
/// multiaddrs it listens on, which is how a peer that reads this endpoint (a
/// devnet orchestrator wiring bootnodes, say) dials it.
///
/// The addresses are only as good as `--discovery.advertise-ip`: without it the
/// node does not know the address peers reach it on, so the lists are empty and
/// the ENR carries no IP. `metadata` is still a placeholder.
async fn get_identity(peer_id: String, identity: Option<crate::BeaconIdentity>) -> Response {
let identity = identity.unwrap_or_default();
crate::json_response(serde_json::json!({
"data": {
"peer_id": peer_id,
"enr": "",
"p2p_addresses": [],
"discovery_addresses": [],
"enr": identity.enr,
"p2p_addresses": identity.p2p_addresses,
"discovery_addresses": identity.discovery_addresses,
"metadata": {
"seq_number": "0",
"attnets": "0x0000000000000000",
Expand Down Expand Up @@ -158,13 +166,38 @@ mod tests {
}

#[tokio::test]
async fn identity_carries_the_peer_id_and_empty_network_fields() {
async fn identity_without_an_identity_carries_the_peer_id_and_empty_network_fields() {
let json = body_json(get("/eth/v1/node/identity").await).await;
assert_eq!(json["data"]["peer_id"], "test-peer");
// Deliberately empty, and not spec-valid; see docs/spec_deviations.md.
assert_eq!(json["data"]["enr"], "");
assert_eq!(json["data"]["p2p_addresses"], serde_json::json!([]));
assert_eq!(json["data"]["discovery_addresses"], serde_json::json!([]));
assert_eq!(json["data"]["metadata"]["seq_number"], "0");
}

#[tokio::test]
async fn identity_reports_the_enr_and_listen_addresses() {
let fixture = beacon_fixture(ANCHOR_SLOT);
let identity = crate::BeaconIdentity {
enr: "enr:-abc".to_string(),
p2p_addresses: vec!["/ip4/10.0.0.1/tcp/9001/p2p/test-peer".to_string()],
discovery_addresses: vec!["/ip4/10.0.0.1/udp/9000/p2p/test-peer".to_string()],
};
let app = routes("ethlambda/test", "test-peer".into())
.with_state(fixture.store)
.layer(Extension(identity));
let request = Request::builder()
.uri("/eth/v1/node/identity")
.body(Body::empty())
.unwrap();
let json = body_json(app.oneshot(request).await.unwrap()).await;
assert_eq!(json["data"]["enr"], "enr:-abc");
assert_eq!(
json["data"]["p2p_addresses"][0],
"/ip4/10.0.0.1/tcp/9001/p2p/test-peer"
);
assert_eq!(
json["data"]["discovery_addresses"][0],
"/ip4/10.0.0.1/udp/9000/p2p/test-peer"
);
}
}
16 changes: 15 additions & 1 deletion crates/net/rpc/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,17 @@ pub fn build_beacon_api_router(store: Store, version: &'static str, peer_id: Str
.with_state(store)
}

/// How peers reach this node, as `/eth/v1/node/identity` reports it.
#[derive(Debug, Clone, Default)]
pub struct BeaconIdentity {
/// The node's ENR, `enr:`-prefixed.
pub enr: String,
/// libp2p multiaddrs the node listens on, each ending in `/p2p/<peer id>`.
pub p2p_addresses: Vec<String>,
/// discv5 multiaddrs, likewise.
pub discovery_addresses: Vec<String>,
}

/// What the Beacon API's validator endpoints reach beyond the store.
pub struct BeaconApiHandles {
/// Through which the pool, aggregate and block endpoints gossip what a
Expand All @@ -195,6 +206,8 @@ pub struct BeaconApiHandles {
/// The execution client block production builds payloads with; `None`
/// makes it answer 503.
pub engine: Option<ethlambda_engine::EngineClient>,
/// What `/eth/v1/node/identity` reports beyond the peer id.
pub identity: BeaconIdentity,
}

/// Start the HTTP servers for a beacon node.
Expand All @@ -216,7 +229,8 @@ pub async fn start_beacon_rpc_server(
.layer(Extension(handles.p2p))
.layer(Extension(handles.attestation_pool))
.layer(Extension(beacon::validator::FeeRecipients::default()))
.layer(Extension(handles.engine));
.layer(Extension(handles.engine))
.layer(Extension(handles.identity));
start_http_servers(config, Some(api_router), shutdown).await
}

Expand Down
Loading
Loading