Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
dd41593
Initial wiring
emlautarom1 Jun 25, 2026
b8ae7b2
feat(app): wire core duty workflow
emlautarom1-agent[bot] Jun 25, 2026
0efaac4
Remove reexport comment
emlautarom1 Jun 25, 2026
3d81f25
Remove lifetimes from slice
emlautarom1 Jun 29, 2026
8640ec0
Inline arguments
emlautarom1 Jun 29, 2026
508fffe
Rename `setup_p2p` to `wire_p2p` to better match Charon naming
emlautarom1 Jun 29, 2026
f1339ee
Code cleanup
emlautarom1 Jun 29, 2026
3fee8a9
Add TODO regarding `Arc<OnceLock<Handle>>` pattern
emlautarom1 Jun 29, 2026
7ed55b2
Small refactor
emlautarom1 Jun 30, 2026
5942052
feat(app): wire eth2 sigagg verifier and consensus feature set
emlautarom1-agent[bot] Jul 1, 2026
31ba7de
feat(app): wire real duty gater, deadlines, graffiti and fetch flags
emlautarom1-agent[bot] Jul 1, 2026
59482fb
feat(app): register validator API duty-query back-edges
emlautarom1-agent[bot] Jul 1, 2026
6c62073
feat(app): verify inbound partial signatures with the eth2 verifier
emlautarom1-agent[bot] Jul 1, 2026
402d4f4
feat(app): verify the cluster lock on startup
emlautarom1-agent[bot] Jul 1, 2026
57e7ff2
docs(app): clarify submission-client and pending gas-limit/fallback T…
emlautarom1-agent[bot] Jul 1, 2026
f0bc80c
feat(app): encode all duty types for consensus via the core codec
emlautarom1-agent[bot] Jul 1, 2026
4592989
feat(app): finish Part A — privkey lock, sign-path test coverage, cle…
emlautarom1-agent[bot] Jul 2, 2026
9e2ff54
Cleanup docs
emlautarom1 Jul 2, 2026
bce13aa
fix(app): seed the shared validator cache into both beacon clients
emlautarom1-agent[bot] Jul 7, 2026
b142300
fix(app): align no_verify, parsigex errors and p2p-key checks with Ch…
emlautarom1-agent[bot] Jul 7, 2026
fe431d1
feat(app): wire relay support into the core node
emlautarom1-agent[bot] Jul 7, 2026
61a3806
refactor(app): load and verify the cluster lock via pluto-cluster
emlautarom1-agent[bot] Jul 7, 2026
5c98b0c
feat(featureset): add case-insensitive string parsing for Status
emlautarom1-agent[bot] Jul 7, 2026
920397c
refactor(app): drop dead AppConfig data_dir and target_gas_limit fields
emlautarom1-agent[bot] Jul 7, 2026
62ab52f
chore(cli): add pluto-featureset dependency
emlautarom1-agent[bot] Jul 7, 2026
3a63a06
feat(cli): add featureset and app error variants
emlautarom1-agent[bot] Jul 7, 2026
57a44c4
feat(cli): bridge RunConfig into pluto-app and wire App::run
emlautarom1-agent[bot] Jul 7, 2026
f05569f
test(cli): cover the RunConfig to AppConfig bridge
emlautarom1-agent[bot] Jul 7, 2026
223d691
fix(app): fail the run when a lifecycle task errors
emlautarom1-agent[bot] Jul 8, 2026
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
7 changes: 7 additions & 0 deletions Cargo.lock

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

9 changes: 8 additions & 1 deletion crates/app/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,15 @@ axum.workspace = true
backon.workspace = true
chrono.workspace = true
futures.workspace = true
libp2p.workspace = true
pluto-core.workspace = true
pluto-consensus.workspace = true
pluto-eth1wrap.workspace = true
pluto-featureset.workspace = true
pluto-eth2api.workspace = true
pluto-p2p.workspace = true
pluto-parsigex.workspace = true
pluto-peerinfo.workspace = true
tokio.workspace = true
tokio-util.workspace = true
vise.workspace = true
Expand All @@ -38,7 +45,6 @@ pluto-cluster.workspace = true
pluto-k1util.workspace = true
pluto-crypto.workspace = true
pluto-ssz.workspace = true
pluto-p2p.workspace = true

[build-dependencies]
pluto-build-proto.workspace = true
Expand All @@ -49,6 +55,7 @@ pluto-testutil.workspace = true
wiremock.workspace = true
test-case.workspace = true
tower = { workspace = true, features = ["util"] }
rand.workspace = true

[lints]
workspace = true
5 changes: 5 additions & 0 deletions crates/app/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,8 @@ pub mod utils;
/// fixed set of checks over a rolling window, and publishes per-check pass/fail
/// state as the `app_health_checks` gauge.
pub mod health;

/// Distributed-validator node wiring: constructs and connects the core duty
/// workflow components (scheduler, fetcher, consensus, dutydb, validatorapi,
/// parsigdb, parsigex, sigagg, aggsigdb, broadcaster) into a runnable node.
pub mod node;
166 changes: 166 additions & 0 deletions crates/app/src/node/behaviour.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
//! Composed P2P [`NetworkBehaviour`] for the core duty workflow.
//!
//! This is the Rust analog of Charon's `wireP2P`: it composes the relay
//! transport behaviours (relay client, [`RelayManager`], force-direct) with
//! the three core protocol behaviours (partial-signature exchange, QBFT
//! consensus, peerinfo) into a single libp2p behaviour and builds a [`Node`]
//! driving it. Relay reservation + routing is the cluster's only
//! peer-discovery path (lock ENRs carry no addresses), mirroring Charon's
//! `NewRelays` / `NewRelayReserver` / `NewRelayRouter` /
//! `ForceDirectConnections` (`app.go:341-400`).
//!
//! Routing is push-based inside the individual behaviours (the QBFT p2p
//! [`Handler`](pluto_consensus::qbft::p2p) holds an `Arc<Consensus>` and calls
//! `consensus.handle()`; parsigex's [`Handle::subscribe`] dispatches inbound
//! partial signatures), so the swarm drive loop body can be empty for
//! correctness — see [`crate::node::drive_network`].

use std::{collections::HashMap, sync::Arc};

use libp2p::{relay, swarm::NetworkBehaviour};
use pluto_consensus::qbft;
use pluto_core::{gater::DutyGaterFn, types::PubKey};
use pluto_crypto::types::PublicKey;
use pluto_eth2api::EthBeaconNodeApiClient;
use pluto_p2p::{
bootnode,
force_direct::ForceDirectBehaviour,
gater,
p2p::{Node, NodeType},
p2p_context::P2PContext,
peer::{self, Peer},
relay::RelayManager,
};
use pluto_parsigex as parsigex;
use pluto_peerinfo::{self as peerinfo, LocalPeerInfo};
use tokio_util::sync::CancellationToken;

use crate::node::AppError;

/// Composed network behaviour for the core duty workflow.
#[derive(NetworkBehaviour)]
pub(crate) struct CoreBehaviour {
/// Relay client transport (circuit reservations and relayed dials).
pub relay: relay::client::Behaviour,
/// Relay reservation lifecycle + relay-circuit peer routing (Charon's
/// `NewRelayReserver` + `NewRelayRouter`).
pub relay_manager: RelayManager,
/// Upgrades relay-routed connections to direct ones (Charon's
/// `ForceDirectConnections`).
pub force_direct: ForceDirectBehaviour,
/// Partial signature exchange between cluster peers.
pub parsigex: parsigex::Behaviour,
/// QBFT consensus message transport.
pub consensus: qbft::p2p::Behaviour,
/// Peer metadata exchange.
pub peerinfo: peerinfo::Behaviour,
}

/// Async handles for driving the composed behaviour from the core workflow.
pub struct CoreHandles {
/// Outbound partial-signature broadcast + inbound subscription handle.
pub parsigex: parsigex::Handle,
/// Outbound QBFT broadcast handle.
pub consensus: qbft::p2p::Handle,
}

/// Composes the core behaviours and builds the libp2p [`Node`].
// TODO(#402 part B): QUIC transport (featureset-gated off at v1.7.1) and
// bandwidth metrics.
#[allow(
clippy::too_many_arguments,
reason = "wireP2P aggregates independent inputs; a config struct is deferred to part B when priority inputs are added"
)]
pub(crate) async fn wire_p2p(
key: k256::SecretKey,
p2p_config: pluto_p2p::config::P2PConfig,
peers: Vec<Peer>,
consensus: Arc<qbft::Consensus>,
duty_gater: DutyGaterFn,
eth2_cl: EthBeaconNodeApiClient,
pub_shares_by_key: HashMap<PubKey, HashMap<u64, PublicKey>>,
lock_hash: Vec<u8>,
builder_enabled: bool,
nickname: String,
cancellation: CancellationToken,
) -> Result<(Node<CoreBehaviour>, CoreHandles), AppError> {
let peer_ids = peers.iter().map(|peer| peer.id).collect::<Vec<_>>();
let local_peer_id = peer::peer_id_from_key(key.public_key())?;

// Relay endpoints resolve in the background; the `Charon-Cluster` header
// carries the lock hash hex (Charon v1.7.1 `p2p.NewRelays`). Post-#4130
// Charon additionally sends a `Cluster-Uuid` header for relay-side load
// balancing — a pending follow-up in pluto-p2p's `new_relays`.
let relay_addrs = bootnode::relay_addrs_for_resolution(&p2p_config.relays);
let relays =
bootnode::new_relays(cancellation.clone(), &relay_addrs, &hex::encode(&lock_hash)).await?;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

lockHashHex := Hex7(cluster.GetInitialMutationHash()) instead of &hex::encode(&lock_hash)


// Closed gater: only cluster peers and the resolved relays may connect.
let conn_gater = gater::ConnGater::new_conn_gater(peer_ids.clone(), relays.clone());

let p2p_context = P2PContext::new(peer_ids.clone());
p2p_context.set_local_peer_id(local_peer_id);

// Keeps one circuit reservation alive per relay and continuously routes
// cluster peers via relay circuit addresses; force-direct then upgrades
// relayed connections to direct ones.
let relay_manager = RelayManager::new(relays, p2p_context.clone());
let force_direct = ForceDirectBehaviour::new(p2p_context.clone(), local_peer_id);

// Partial signature exchange. Inbound partial signatures are verified
// against the sender's public share for the duty via the eth2 verifier
// (Charon `parsigex.NewEth2Verifier`).
let parsigex_config = parsigex::Config::new(
local_peer_id,
p2p_context.clone(),
parsigex::new_eth2_verifier(eth2_cl, pub_shares_by_key),
duty_gater,
);
let (parsigex_comp, parsigex_handle) = parsigex::Behaviour::new(parsigex_config);

// QBFT consensus transport. `Behaviour::new` errors if the local peer id is
// not present in the configured cluster peer list.
let (consensus_comp, consensus_handle) = qbft::p2p::Behaviour::new(qbft::p2p::Config {
consensus,
p2p_context: p2p_context.clone(),
local_peer_id,
cancellation,
})?;

// Peer metadata exchange.
let (git_hash, _) = pluto_core::version::git_commit();
let peerinfo_config = peerinfo::Config::new(LocalPeerInfo::new(
pluto_core::version::VERSION.to_string(),
lock_hash,
git_hash,
builder_enabled,
nickname,
))
.with_peers(peer_ids.clone());
let peerinfo_comp = peerinfo::Behaviour::new(local_peer_id, peerinfo_config);

let node = Node::new(
p2p_config,
key,
NodeType::TCP,
false,
p2p_context,
|builder, _keypair, relay_client| {
builder.with_gater(conn_gater).with_inner(CoreBehaviour {
relay: relay_client,
relay_manager,
force_direct,
parsigex: parsigex_comp,
consensus: consensus_comp,
peerinfo: peerinfo_comp,
})
},
)?;

let handles = CoreHandles {
parsigex: parsigex_handle,
consensus: consensus_handle,
};

Ok((node, handles))
}
74 changes: 74 additions & 0 deletions crates/app/src/node/config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
//! Configuration for the distributed-validator node.

use std::{net::SocketAddr, path::PathBuf, sync::Arc, time::Duration};

use pluto_featureset::FeatureSet;
use pluto_p2p::config::P2PConfig;

/// Application configuration for running a distributed-validator node.
///
/// This is the Rust analog of Charon's `app.Config` (`app/app.go`), reduced to
/// the minimal set required to wire and run the core duty workflow.
/// Observability (monitoring/debug API, tracing/OTLP) and simnet/mock-only
/// fields are intentionally omitted for the minimal-runnable wiring.
// TODO(#402 part B): add monitoring/debug addrs, OTLP/Jaeger tracing config,
// simnet (beacon/validator mock) and `TestConfig`-style overrides.
#[derive(Debug, Clone)]
pub struct AppConfig {
/// P2P networking configuration (listen/advertise addresses, relays, ...).
pub p2p: P2PConfig,

/// Path to the cluster lock file (`cluster-lock.json`).
pub lock_file: PathBuf,

/// Path to the node's secp256k1 P2P private key.
pub priv_key_file: PathBuf,

/// Enable private-key file locking. When set, a
/// `<priv_key_file>.lock`sentinel is maintained for the node's lifetime to
/// detect a second node started against the same key.
pub priv_key_locking: bool,

/// Beacon node API endpoints. The first reachable endpoint is used;
/// multiple addresses enable fallback.
pub beacon_node_addrs: Vec<String>,

/// Timeout for general beacon node requests.
pub beacon_node_timeout: Duration,

/// Timeout for beacon node submission (broadcast) requests.
pub beacon_node_submit_timeout: Duration,

/// Address the validator API HTTP server binds to.
pub validator_api_addr: SocketAddr,

/// Whether the builder API (MEV-boost) is enabled.
pub builder_api: bool,

/// Human-readable node nickname, surfaced via the peerinfo protocol.
pub nickname: String,

/// Skip cluster lock hash + signature verification.
pub no_verify: bool,

/// Execution-layer (eth1) JSON-RPC endpoint, used to verify operator
/// signatures (including EIP-1271 smart-contract signatures) in the cluster
/// lock. When `None`, lock verification runs without eth1 and such operator
/// signatures are not checked. Mirrors Charon's
/// `--execution-client-rpc-endpoint`.
pub eth1_endpoint: Option<String>,

/// Graffiti included in proposed blocks. `None` gives every validator the
/// default (client) graffiti; a single value applies to all validators; one
/// value per validator otherwise. Mirrors Charon's `--graffiti`.
pub graffiti: Option<Vec<String>>,

/// Disable appending the client version/codex to graffiti. Mirrors Charon's
/// `--graffiti-disable-client-append`.
pub graffiti_disable_client_append: bool,

/// Feature set controlling optional/alpha behaviors (e.g.
/// `FetchOnlyCommIdx0`, `ChainSplitHalt`). Resolved from the CLI
/// feature flags (out of scope here).
pub feature_set: Arc<FeatureSet>,
}
Loading
Loading