Skip to content
Draft
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
3 changes: 3 additions & 0 deletions Cargo.lock

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

59 changes: 59 additions & 0 deletions crates/cluster/tests/prototype_mixed_reconstruction.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
//! PROTOTYPE (not yet runnable): reconstruct a group signature from a mix of
//! Pluto- and Charon-generated key shares.
//!
//! This file is a forward specification, not a working test. It is `#[ignore]`d
//! so it never runs in CI; it pins the target and the missing fixture.
//!
//! ## Why this matters
//!
//! Charon compatibility must hold at the *cryptographic* layer, not just for
//! JSON parsing. After a mixed DKG ceremony (some operators on Charon, some on
//! Pluto), every node holds a private share of the **same** distributed
//! validator key. The cluster is only interoperable if a threshold of those
//! shares — regardless of which implementation produced each one — reconstructs
//! a group signature that verifies against the DV public key.
//!
//! ## What already exists
//!
//! - Pure-Pluto t-of-n reconstruction is tested: partials from a threshold of
//! Pluto shares aggregate to a valid group signature
//! (`crates/dkg/src/frostp2p_integ_test.rs:502-536`, via
//! `BlstImpl::threshold_aggregate` + `BlstImpl::verify`).
//! - A mixed 2 Charon + 2 Pluto DKG ceremony runs in CI
//! (`.github/workflows/dkg-runner.yml`). But its semantic check only asserts
//! the lock's `signature_aggregate` has the right byte length
//! (`scripts/dkg-runner/ci/verify-output-semantic.sh`), not that a fresh
//! threshold reconstruction from the mixed shares verifies cryptographically.
//!
//! ## What is missing (the gap)
//!
//! Committed test fixtures containing the **private** key shares produced by
//! Charon for a known group key. The reconstruction primitive
//! (`BlstImpl::threshold_aggregate`) exists and works for Pluto shares; what is
//! absent is access to Charon-generated shares inside a Rust test. A real test
//! needs either captured per-node keystores from a mixed `dkg-runner` ceremony
//! (group pubkey + at least a threshold of secret shares, some authored by
//! Charon) or a committed interop fixture. Running Charon itself from a unit
//! test is out of scope.
//!
//! ## Target scenario (to assert once fixtures exist)
//!
//! 1. Load a known distributed-validator group public key and a threshold of
//! private shares, where at least one share was generated by Charon and at
//! least one by Pluto.
//! 2. Sign a fixed message with each share to produce partial signatures.
//! 3. `BlstImpl::threshold_aggregate` the partials and assert the result
//! verifies against the group public key — proving Charon and Pluto shares
//! are cryptographically interchangeable, not merely format-compatible.

/// Forward spec for reconstructing a group signature from mixed Charon/Pluto
/// shares. Ignored and intentionally unimplemented: it needs committed Charon
/// private-share fixtures (see the module docs above).
#[test]
#[ignore = "blocked: no committed Charon-generated private share fixtures to reconstruct from"]
fn prototype_test_reconstruct_group_signature_from_mixed_pluto_charon_shares() {
unimplemented!(
"specification only — implement once a mixed-ceremony fixture provides Charon-generated \
private shares for a known group key (see module docs)"
);
}
65 changes: 65 additions & 0 deletions crates/core/tests/prototype_duty_runtime.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
//! PROTOTYPE (not yet runnable): validator duty execution over the full
//! runtime pipeline.
//!
//! Forward specifications, all `#[ignore]`d so they never run in CI. They pin
//! the most important runtime proofs and the single blocker.
//!
//! ## The blocker
//!
//! There is no `pluto run` command (`crates/cli/src/cli.rs` exposes only `Enr`,
//! `Create`, `Version`, `Relay`, `Dkg`, `Alpha`). The duty pipeline cannot be
//! assembled or exercised until it exists.
//!
//! ## What already exists (the building blocks)
//!
//! - `DutyDB` (`crates/core/src/dutydb/memory.rs`)
//! - `ValidatorAPI` component (`crates/core/src/validatorapi/component.rs`) —
//! but its submit handlers are dead-code, awaiting the runtime
//! - `ParSigEx` (`crates/parsigex/`), `ParSigDB`
//! (`crates/core/src/parsigdb/memory.rs`), `SigAgg`
//! (`crates/core/src/sigagg.rs`), `AggSigDB`
//! (`crates/core/src/aggsigdb/memory.rs`)
//! - `Tracker` and `Deadliner` (`crates/core/src/{tracker,deadline}/`)
//! - QBFT state machine (`crates/core/src/qbft/`) and libp2p transport (#448)
//! - `BeaconMock` and `ValidatorMock` (`crates/testutil/src/{beaconmock,
//! validatormock}/`) — the simnet doubles are ready
//!
//! ## What is missing (the gap)
//!
//! The runtime glue that wires the blocks into a pipeline: a **scheduler**
//! (duty poller), a **fetcher** (unsigned duty data), a **consensus runner**
//! (spawns `qbft::run()` over the transport), a **broadcaster** (submits
//! aggregated signatures), and the ValidatorAPI **submit handlers**. None of
//! these exist.
//!
//! ## Target pipeline (each test, once `pluto run` exists)
//!
//! scheduler → fetch unsigned duty → QBFT consensus over the `UnsignedDataSet`
//! → store in `DutyDB` → VC signs → ParSigEx exchange → ParSigDB threshold
//! match → SigAgg aggregate → broadcast to the beacon node.

/// Full attestation duty over the pipeline on a small simnet cluster:
/// scheduler → consensus → VC sign → parsig → aggregate → submit, asserting one
/// aggregated attestation is submitted per validator/slot.
#[test]
#[ignore = "blocked on `pluto run`: no scheduler/fetcher/consensus-runner/broadcaster pipeline"]
fn prototype_test_attestation_duty_full_pipeline() {
unimplemented!("specification only — needs the assembled duty runtime (see module docs)");
}

/// Block proposal duty over the pipeline, asserting exactly one valid block is
/// produced and broadcast per validator/slot.
#[test]
#[ignore = "blocked on `pluto run`: no proposer pipeline / broadcaster"]
fn prototype_test_block_proposal_duty_produces_one_block() {
unimplemented!("specification only — needs the assembled duty runtime (see module docs)");
}

/// Multi-slot simnet with `BeaconMock` + `ValidatorMock` and 3-4 nodes,
/// asserting attester and proposer duties pass across several slots (parity
/// with Charon's `TestSimnetDuties`).
#[test]
#[ignore = "blocked on `pluto run`: simnet doubles exist but nothing drives the pipeline"]
fn prototype_test_multi_slot_simnet_duties() {
unimplemented!("specification only — needs the assembled duty runtime (see module docs)");
}
59 changes: 59 additions & 0 deletions crates/core/tests/prototype_qbft_over_libp2p.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
//! PROTOTYPE (not yet runnable): QBFT consensus over a real libp2p network.
//!
//! This file is a forward specification, not a working test. It documents the
//! end-to-end QBFT scenario we want to prove and the single piece of production
//! code that is missing to make it real. The test is `#[ignore]`d so it never
//! runs in CI; it exists to pin the target and give the future test a home.
//!
//! ## What already exists
//!
//! - The QBFT state machine: `pluto_core::qbft::run()`
//! (`crates/core/src/qbft/mod.rs:327`). Multi-node consensus reaching a
//! single decided value — including degraded and adversarial cases — is
//! already covered against an **in-memory** transport with a fake clock in
//! `crates/core/src/qbft/internal_test.rs` (`happy`, `stagger_start`,
//! `dropped_messages`, `fuzzed`, `chain_split`).
//! - The libp2p QBFT transport and sniffer landed in #448:
//! `crates/core/src/consensus/qbft/transport.rs` and `.../sniffer.rs`. Both
//! carry `#![allow(dead_code)]` with `TODO: Remove once the consensus runner
//! wires this transport.`
//! - The per-instance plumbing: `pluto_core::consensus::instance::InstanceIo`
//! (`maybe_start()`, `take_recv_rx()`, …) buffers inbound messages until a
//! runner starts.
//!
//! ## What is missing (the only gap)
//!
//! A **consensus runner**: the glue that, for one duty/instance, adapts the
//! libp2p `consensus::qbft::transport` to the state machine's abstract
//! `qbft::Transport<T>`, drains `InstanceIo` once `maybe_start()` returns true,
//! and drives `qbft::run()` — pumping messages between the swarm and the state
//! machine. Nothing spawns `qbft::run()` over the network today, so this test
//! cannot be written yet. This is the highest-leverage runtime work available
//! before `pluto run`.
//!
//! ## Target scenario (to assert once the runner exists)
//!
//! 1. Start N (e.g. 4) Pluto nodes, each running the consensus runner over the
//! libp2p transport, connected in a full mesh over loopback TCP.
//! 2. Feed each honest node the same proposed value for one instance.
//! 3. Assert every honest node decides, and they all decide the **same** value
//! in the same instance — proving wire serialization + transport + runner
//! interoperate, not just the in-memory state machine.
//!
//! Follow-on variants (own prototypes/commits later): one crashed node with the
//! remaining majority still deciding, and a Byzantine proposer that must never
//! cause two honest nodes to decide different values.

/// Forward spec for QBFT reaching a single decision across nodes over libp2p.
///
/// Ignored and intentionally not implemented: it requires a consensus runner
/// that wires `consensus::qbft::transport` + `consensus::instance::InstanceIo`
/// into `qbft::run()`. See the module docs above.
#[test]
#[ignore = "blocked: no consensus runner wires the libp2p QBFT transport into qbft::run()"]
fn prototype_test_qbft_reaches_single_decision_over_libp2p() {
unimplemented!(
"specification only — implement once a consensus runner drives qbft::run() over the \
consensus::qbft libp2p transport (see module docs)"
);
}
39 changes: 39 additions & 0 deletions crates/core/tests/prototype_runtime_charon_parity.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
//! PROTOTYPE (not yet runnable): mixed Pluto + Charon runtime interoperability.
//!
//! Forward specifications, all `#[ignore]`d so they never run in CI. These
//! prove real interoperability: a cluster of mixed Charon and Pluto nodes
//! executing live validator duties together, not just exchanging static files.
//!
//! ## The blocker
//!
//! No `pluto run` command / assembled duty runtime (see
//! `prototype_duty_runtime.rs`). Static-artifact Charon parity is already
//! covered — Charon-created locks parse and verify in Pluto
//! (`crates/cluster/src/lock.rs`, V1.0-V1.10 fixtures) and a mixed 2 Charon +
//! 2 Pluto DKG ceremony runs in CI (`.github/workflows/dkg-runner.yml`). What
//! cannot be reproduced is a mixed cluster running the *runtime*: agreeing on
//! duty data over QBFT, exchanging partials over ParSigEx, and aggregating —
//! across both implementations.
//!
//! ## Target scenarios (once `pluto run` exists)
//!
//! Wire-level parity of QBFT (`/charon/consensus/...`) and ParSigEx
//! (`/charon/parsigex/2.0.0`) must let Charon and Pluto nodes participate in
//! the same consensus game and partial-signature exchange, then all agree on
//! the duty hash and produce a valid aggregated signature.

/// Mixed runtime, 3 Charon + 1 Pluto: duties pass and all nodes agree on the
/// duty hash (minimum real interoperability).
#[test]
#[ignore = "blocked on `pluto run`: no runtime for a mixed cluster to execute duties"]
fn prototype_test_mixed_runtime_three_charon_one_pluto() {
unimplemented!("specification only — needs `pluto run` + QBFT/ParSigEx wire parity");
}

/// Mixed runtime, 2 Charon + 2 Pluto: attestation and proposer duties pass
/// (stronger parity).
#[test]
#[ignore = "blocked on `pluto run`: no runtime for a mixed cluster to execute duties"]
fn prototype_test_mixed_runtime_two_charon_two_pluto() {
unimplemented!("specification only — needs `pluto run` + QBFT/ParSigEx wire parity");
}
82 changes: 82 additions & 0 deletions crates/core/tests/prototype_runtime_safety.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
//! PROTOTYPE (not yet runnable): runtime safety / anti-slashing guarantees.
//!
//! Forward specifications, all `#[ignore]`d so they never run in CI. These are
//! the safety properties a distributed validator exists to provide; none can be
//! exercised yet.
//!
//! ## The blocker(s)
//!
//! - No `pluto run` command / assembled duty runtime (see
//! `prototype_duty_runtime.rs`). Without scheduler → consensus → VC sign →
//! parsig → aggregate → broadcast, none of these scenarios can be reproduced.
//! - No **slashing-protection database**. There is no persistent record of what
//! a validator has already signed anywhere in the tree (the `*_slashings`
//! symbols in `crates/core/src/{signeddata,dutydb}` are block-body fields,
//! not slashing protection). The surround/double-vote-across-restart case
//! additionally needs this DB to be built.
//! - `privkeylock` itself exists (`crates/app/src/privkeylock.rs`); only the
//! run-level enforcement test is missing.
//!
//! ## Why consensus alone is not enough
//!
//! Threshold aggregation is only safe if every node signs the *same* data.
//! Consensus over the `UnsignedDataSet` plus `DutyDB` persistence plus slashing
//! protection together prevent a malicious beacon node or a compromised VC from
//! inducing a double vote / double proposal. Each test below pins one such
//! guarantee.

/// A malicious beacon node serves conflicting attestation data to different
/// nodes; the cluster must sign one root or nothing — never two.
#[test]
#[ignore = "blocked on `pluto run`: no consensus-backed duty pipeline to defend"]
fn prototype_test_malicious_bn_conflicting_attestation_data() {
unimplemented!("specification only — needs the assembled duty runtime + consensus");
}

/// A malicious beacon node attempts to induce a double block proposal; at most
/// one block signature must be produced/broadcast per validator/slot.
#[test]
#[ignore = "blocked on `pluto run`: no proposer pipeline to defend"]
fn prototype_test_malicious_bn_double_proposal_is_prevented() {
unimplemented!("specification only — needs the assembled duty runtime");
}

/// A compromised validator client submits a partial that does not match the
/// consensus-decided duty; it must be rejected, never exchanged or aggregated.
#[test]
#[ignore = "blocked on `pluto run`: no consensus/DutyDB cross-check on submitted partials"]
fn prototype_test_compromised_vc_submission_is_rejected() {
unimplemented!("specification only — needs the assembled duty runtime + DutyDB cross-check");
}

/// Network partition 2/2: with no majority, neither side may sign or broadcast
/// (safety over liveness).
#[test]
#[ignore = "blocked on `pluto run`: no runtime to partition"]
fn prototype_test_network_partition_2_2_signs_nothing() {
unimplemented!("specification only — needs the assembled duty runtime");
}

/// Network partition 3/1: the majority side continues; the isolated minority
/// cannot reach threshold and must not sign.
#[test]
#[ignore = "blocked on `pluto run`: no runtime to partition"]
fn prototype_test_network_partition_3_1_majority_continues_minority_safe() {
unimplemented!("specification only — needs the assembled duty runtime");
}

/// A surround/double vote attempted across a restart must be blocked by
/// persistent slashing history.
#[test]
#[ignore = "blocked on `pluto run` AND a slashing-protection DB (neither exists)"]
fn prototype_test_surround_vote_blocked_across_restart() {
unimplemented!("specification only — needs the duty runtime and a persistent slashing DB");
}

/// A second runtime started on the same validator keys must not be able to
/// start/sign (`privkeylock` enforcement at the run level).
#[test]
#[ignore = "blocked on `pluto run`: privkeylock primitive exists, run-level enforcement does not"]
fn prototype_test_privkeylock_blocks_second_runtime() {
unimplemented!("specification only — needs `pluto run` to enforce privkeylock at startup");
}
Loading
Loading