Skip to content
Merged
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
21 changes: 15 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@ with `Ctrl+C`; it shuts down gracefully on SIGINT only, so a systemd unit needs
`xattr -d com.apple.quarantine <file>` lifts; a `curl` download is not.
- [`RELEASE.md`](./RELEASE.md#verifying-a-release) shows how to verify an
ethlambda binary's provenance attestation.
- A release binary is the tested release candidate's build, so `--version`
names that candidate (`vX.Y.Z-rc.N-<sha>`), while the same release's Docker
image reports `vX.Y.Z-stable-<sha>`. Both run the same code; see
[`RELEASE.md`](./RELEASE.md#version-string).

To skip payload validation, run the node without ethrex: drop it, the JWT
secret and the two `--execution-*` flags. The node then follows the chain and
Expand All @@ -96,8 +100,9 @@ curl -s localhost:5052/eth/v1/node/syncing
```

The node has caught up once `sync_distance` (the chain's current slot minus
`head_slot`) is near 0. Don't rely on `is_syncing` yet: it currently reads
`false` during catch-up as well.
`head_slot`) is near 0. Don't rely on `is_syncing` or `/eth/v1/node/health`
yet: during catch-up `is_syncing` can read `false` or switch back and forth,
and the health endpoint can answer `200` rather than `206`.

For Sepolia or Hoodi, change `--network mainnet` to `--network sepolia` or
`--network hoodi` in both the ethrex and the beacon node arguments, and point
Expand Down Expand Up @@ -150,8 +155,10 @@ docker run -d --name ethrex --network ethereum \
--authrpc.addr 0.0.0.0 \
--authrpc.jwtsecret /jwt.hex

# The node shuts down gracefully on SIGINT only, so this makes `docker stop` flush its state
docker run -d --name ethlambda-beacon --network ethereum --stop-signal SIGINT \
# The node shuts down gracefully on SIGINT only, so `--stop-signal` makes `docker stop` send it.
# Flushing its state can outlast Docker's default 10 s, after which Docker kills it and the
# unflushed state is lost, so `--stop-timeout` gives it a minute.
docker run -d --name ethlambda-beacon --network ethereum --stop-signal SIGINT --stop-timeout 60 \
-p 9000:9000/udp -p 9001:9001/udp -p 9001:9001/tcp \
-p 127.0.0.1:5052:5052 \
-v "$PWD/beacon-data:/data" \
Expand Down Expand Up @@ -182,8 +189,10 @@ openssl rand -hex 32 > beacon-data/node-key
# This host's public address, published in the node's ENR (see above)
PUBLIC_IP=$(curl -s https://ifconfig.me)

# The node shuts down gracefully on SIGINT only, so this makes `docker stop` flush its state
docker run -d --name ethlambda-beacon --stop-signal SIGINT \
# The node shuts down gracefully on SIGINT only, so `--stop-signal` makes `docker stop` send it.
# Flushing its state can outlast Docker's default 10 s, after which Docker kills it and the
# unflushed state is lost, so `--stop-timeout` gives it a minute.
docker run -d --name ethlambda-beacon --stop-signal SIGINT --stop-timeout 60 \
-p 9000:9000/udp -p 9001:9001/udp -p 9001:9001/tcp \
-p 127.0.0.1:5052:5052 \
-v "$PWD/beacon-data:/data" \
Expand Down
18 changes: 11 additions & 7 deletions bin/ethlambda/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ fn beacon_identity(
format!("/{family}/{ip}/tcp/{gossipsub_port}/p2p/{peer_id}"),
],
discovery_addresses: vec![format!("/{family}/{ip}/udp/{discovery_port}/p2p/{peer_id}")],
metadata: None,
}
}

Expand Down Expand Up @@ -768,13 +769,16 @@ async fn run_node(options: Options) -> eyre::Result<()> {
} => ethlambda_rpc::CustodyColumns(custody_columns.clone()),
ChainActor::Lean(..) => ethlambda_rpc::CustodyColumns::default(),
};
let rpc_identity = beacon_identity(
p2p.local_enr().unwrap_or_default(),
common.discovery.advertise_ip,
common.gossipsub_port,
common.discovery.port,
&local_peer_id,
);
let rpc_identity = ethlambda_rpc::BeaconIdentity {
metadata: p2p.local_metadata(),
..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 Down
5 changes: 5 additions & 0 deletions crates/blockchain/state_transition/src/beacon/fork_choice.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1282,6 +1282,11 @@ pub fn get_forkchoice_store(
store
.insert_signed_block(anchor_root, anchor_block)
.expect("insert");
// The anchor heads the canonical chain, but the head-diff that maintains
// `BlockRoots` never writes the block it starts from.
store
.index_beacon_anchor(anchor_root)
.expect("index anchor");
// Only the hash, not a verdict: the specification's `is_execution_block`
// reads the block body, and the anchor is the one block that never goes
// through `on_block`, which is what fills this cache. Without it the first
Expand Down
45 changes: 45 additions & 0 deletions crates/net/api/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use std::sync::{Arc, RwLock};
use std::time::Instant;

use ethlambda_types::{
Expand Down Expand Up @@ -470,6 +471,50 @@ pub trait RpcToP2P: Send + Sync {
) -> Result<(), ActorError>;
}

// --- Shared state: P2P -> RPC ---

/// The beacon `MetaData` this node answers its peers with.
///
/// The two bitfields are kept as their SSZ bytes, which is also what the
/// Beacon API's `/eth/v1/node/identity` reports (hex-encoded), so a reader
/// needs neither the subnet counts nor the bitvector types.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct AdvertisedMetaData {
pub seq_number: u64,
/// `attnets`: the attestation subnets this node serves.
pub attnets: Vec<u8>,
/// `syncnets`: the sync committee subnets this node serves.
pub syncnets: Vec<u8>,
}

/// Shared, runtime-readable copy of the node's [`AdvertisedMetaData`].
///
/// The p2p layer is the sole writer: it publishes whenever the `MetaData` it
/// answers peers with changes, which is also every time it bumps
/// `seq_number`. Read paths outside it, the Beacon API's
/// `/eth/v1/node/identity` in particular, clone the handle and read the
/// current value, so that endpoint reports what peers are told rather than a
/// copy taken at startup.
#[derive(Clone, Debug, Default)]
pub struct MetaDataController {
metadata: Arc<RwLock<AdvertisedMetaData>>,
}

impl MetaDataController {
/// Publish the current `MetaData`.
pub fn set(&self, metadata: AdvertisedMetaData) {
*self.metadata.write().expect("metadata lock poisoned") = metadata;
}

/// Read the current `MetaData`.
pub fn get(&self) -> AdvertisedMetaData {
self.metadata
.read()
.expect("metadata lock poisoned")
.clone()
}
}

// --- Init messages ---
// Used to wire actors together after spawn.

Expand Down
13 changes: 13 additions & 0 deletions crates/net/p2p/src/beacon/handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@
//! [`build_status`] and [`build_metadata`] for the bodies they cannot construct
//! themselves.

use ethlambda_network_api::AdvertisedMetaData;
use ethlambda_storage::Store;
use ethlambda_types::beacon::primitives::Root;
use libp2p::PeerId;
use libssz::SszEncode;
use tracing::debug;

use super::messages::{
Expand Down Expand Up @@ -148,6 +150,16 @@ pub fn build_metadata(wire: &BeaconWire, protocol: &str) -> Option<BeaconMetaDat
}
}

/// The fields of [`build_metadata`]'s answer that the Beacon API's
/// `/eth/v1/node/identity` reports, computed the same way so the two agree.
pub(crate) fn advertised_metadata(wire: &BeaconWire) -> AdvertisedMetaData {
AdvertisedMetaData {
seq_number: wire.metadata_seq_number,
attnets: attnets(wire).to_ssz(),
syncnets: syncnets(wire).to_ssz(),
}
}

/// The `attnets` bitfield for this node's backbone subscription.
///
/// A subnet id past the bitfield's width is dropped rather than wrapped: the
Expand Down Expand Up @@ -254,6 +266,7 @@ mod tests {
genesis_time: 1_606_824_023,
genesis_validators_root: Root::ZERO,
metadata_seq_number: 0,
metadata: Default::default(),
custody_columns: Vec::new(),
attestation_subnets: Vec::new(),
sync_committee_subnets: Default::default(),
Expand Down
24 changes: 22 additions & 2 deletions crates/net/p2p/src/beacon/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ pub mod verdict;

use std::collections::BTreeMap;

use ethlambda_network_api::MetaDataController;
use ethlambda_types::beacon::config::Config;
use ethlambda_types::beacon::fork::ForkName;
use ethlambda_types::beacon::primitives::{Epoch, ForkDigest, Root, Slot};
Expand Down Expand Up @@ -69,8 +70,9 @@ pub struct BeaconWire {
/// epoch, so serving history means computing digests this node never runs
/// on. See [`encoding`]'s module docs for the rule.
pub genesis_validators_root: Root,
/// Advertised in `Ping` responses and in `MetaData`. Bumped when the
/// digest switches, the one advertised value that changes at runtime.
/// Advertised in `Ping` responses and in `MetaData`. Bumped through
/// [`Self::bump_metadata_seq_number`] when the digest switches and when
/// `syncnets` changes.
pub metadata_seq_number: u64,
/// The columns this node custodies, carried alongside `topics` so the
/// gossip handler and the request handlers can check what this node
Expand All @@ -88,9 +90,27 @@ pub struct BeaconWire {
/// `build_metadata` derives `syncnets` from its keys. Never in the ENR,
/// which cannot be replaced at runtime.
pub sync_committee_subnets: BTreeMap<u64, Epoch>,
/// The `MetaData` above as the Beacon API's `/eth/v1/node/identity` reads
/// it, republished by [`Self::bump_metadata_seq_number`].
pub metadata: MetaDataController,
}

impl BeaconWire {
/// Bump `metadata_seq_number` after a change to what `MetaData`
/// advertises, and republish it for the Beacon API.
///
/// The one way the sequence number moves, so the copy the Beacon API
/// reports cannot fall behind what peers are told.
pub fn bump_metadata_seq_number(&mut self) {
self.metadata_seq_number += 1;
self.publish_metadata();
}

/// Publish the current `MetaData` to [`Self::metadata`]'s readers.
pub fn publish_metadata(&self) {
self.metadata.set(handler::advertised_metadata(self));
}

/// Every topic set this node is subscribed to: the current digest's, then
/// the window's.
pub fn held_topics(&self) -> impl Iterator<Item = &topics::BeaconTopics> {
Expand Down
28 changes: 25 additions & 3 deletions crates/net/p2p/src/beacon/sync_committee.rs
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ fn join(server: &mut P2PServer, subnets: Vec<(u64, Epoch)>) -> Vec<String> {
}
if !joined.is_empty() {
// The advertised `syncnets` changed.
wire.metadata_seq_number += 1;
wire.bump_metadata_seq_number();
}
subscribed
}
Expand Down Expand Up @@ -197,7 +197,7 @@ fn leave_expired(server: &mut P2PServer, epoch: Epoch) -> Vec<String> {
}
}
if !expired.is_empty() {
wire.metadata_seq_number += 1;
wire.bump_metadata_seq_number();
}
unsubscribed
}
Expand Down Expand Up @@ -227,7 +227,7 @@ mod tests {
use ethlambda_types::beacon::primitives::Root;

use super::*;
use crate::beacon::handler::build_metadata;
use crate::beacon::handler::{advertised_metadata, build_metadata};
use crate::beacon::messages::BeaconMetaData;
use crate::beacon::protocols;
use crate::beacon::transition::apply;
Expand Down Expand Up @@ -352,6 +352,28 @@ mod tests {
assert_eq!(seq(&server), start + 3);
}

/// The copy the Beacon API reports (`/eth/v1/node/identity`) is
/// republished with every change, so it never lags what a peer's
/// `MetaData` request is answered with.
#[tokio::test]
async fn the_beacon_api_copy_follows_every_change() {
let mut server = unconnected_beacon_server(Config::mainnet(), 0).await;
let in_step = |server: &P2PServer| {
let wire = server.wire.beacon().expect("beacon wire");
wire.metadata.get() == advertised_metadata(wire)
};
assert!(in_step(&server), "published at startup");

join(&mut server, vec![(1, 100)]);
assert!(in_step(&server));
let published = server.wire.beacon().expect("beacon wire").metadata.get();
assert_eq!(published.syncnets, vec![0b0000_0010]);
assert_eq!(published.seq_number, seq(&server));

leave_expired(&mut server, 100);
assert!(in_step(&server));
}

#[tokio::test]
async fn publishing_a_message_marks_each_subnet_seen() {
let mut server = unconnected_beacon_server(Config::mainnet(), 0).await;
Expand Down
2 changes: 1 addition & 1 deletion crates/net/p2p/src/beacon/transition.rs
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ pub(crate) fn apply(server: &mut P2PServer, epoch: Epoch) -> Changes {
let previous = wire.fork_digest;
wire.fork_digest = current.digest;
wire.fork = current.fork;
wire.metadata_seq_number += 1;
wire.bump_metadata_seq_number();
let digest_hex = hex::encode(current.digest);
metrics::set_beacon_fork_digest(&digest_hex);
info!(
Expand Down
25 changes: 21 additions & 4 deletions crates/net/p2p/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ use std::{

use either::Either;
use ethlambda_network_api::{
FetchRequest, InitBlockChain, P2PToBlockChainRef,
FetchRequest, InitBlockChain, MetaDataController, P2PToBlockChainRef,
block_chain_to_p2p::{
CheckDataColumnSidecars, FetchBlock, PublishAggregatedAttestation, PublishAttestation,
PublishBlock,
Expand Down Expand Up @@ -1099,7 +1099,7 @@ pub fn build_swarm(config: SwarmConfig) -> Result<BuiltSwarm, SwarmBuildError> {
&beacon.config,
beacon.genesis_validators_root,
);
Wire::Beacon(Box::new(beacon::BeaconWire {
let wire = beacon::BeaconWire {
fork_digest: beacon.fork_digest,
fork: beacon.fork,
schedule,
Expand All @@ -1112,7 +1112,10 @@ pub fn build_swarm(config: SwarmConfig) -> Result<BuiltSwarm, SwarmBuildError> {
custody_columns: beacon.custody_columns,
attestation_subnets: beacon.attestation_subnets,
sync_committee_subnets: std::collections::BTreeMap::new(),
}))
metadata: MetaDataController::default(),
};
wire.publish_metadata();
Wire::Beacon(Box::new(wire))
}
};

Expand All @@ -1132,6 +1135,9 @@ pub struct P2P {
/// 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>,
/// The beacon `MetaData` this node answers peers with, for the same
/// endpoint. `None` on a lean wire, which has no `MetaData`.
local_metadata: Option<MetaDataController>,
}

impl P2P {
Expand Down Expand Up @@ -1189,6 +1195,7 @@ impl P2P {
.wire
.beacon()
.map(|beacon| beacon::scoring::PeerScoring::new(&beacon.config, beacon.fork_digest));
let local_metadata = built.wire.beacon().map(|beacon| beacon.metadata.clone());

let server = P2PServer {
swarm_handle,
Expand Down Expand Up @@ -1276,7 +1283,11 @@ impl P2P {
.send(p2p_protocol::AdvanceForkSchedule)
.inspect_err(|err| warn!(%err, "Could not schedule the fork schedule"));
spawn_listener(handle.context(), swarm_stream.map(WrappedSwarmEvent));
Ok(P2P { handle, local_enr })
Ok(P2P {
handle,
local_enr,
local_metadata,
})
}

pub fn actor_ref(&self) -> &ActorRef<P2PServer> {
Expand All @@ -1290,6 +1301,12 @@ impl P2P {
pub fn local_enr(&self) -> Option<&str> {
self.local_enr.as_deref()
}

/// A live view of the beacon `MetaData` this node answers peers with,
/// updated whenever it changes. `None` on a lean wire.
pub fn local_metadata(&self) -> Option<MetaDataController> {
self.local_metadata.clone()
}
}

/// Message wrapper for swarm events. Not part of the protocol because
Expand Down
Loading
Loading