Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
b9f5042
Added pluto-conformance plan
pinebit Jul 31, 2026
5e70926
Add Rust consumer scaffold and secp256k1 suite for pluto
pinebit Jul 31, 2026
73cb64b
Prefix suite name in failure labels, drop redundant dev-deps
pinebit Jul 31, 2026
7952ce5
Run qbft_hashing suite against pluto
pinebit Jul 31, 2026
dee5f95
Pin unsigned_data_set map-entry divergence as a named test
pinebit Jul 31, 2026
ef4c82d
Correct skip-set comment and drop line-anchored citations
pinebit Jul 31, 2026
c042616
Run bls_threshold suite against pluto
pinebit Jul 31, 2026
00da5cf
Run cluster_hashing suite against pluto
pinebit Jul 31, 2026
f670b89
Document masked timestamp gap in cluster_hashing findings
pinebit Jul 31, 2026
6e0e6bd
Run priority_scoring suite against pluto
pinebit Jul 31, 2026
27bc6c1
Run timer_deadlines suite against pluto
pinebit Jul 31, 2026
48aab75
Pin timer_deadlines default-config divergence and slot invariance
pinebit Jul 31, 2026
858b41d
Run qbft_msg_limits suite against pluto
pinebit Jul 31, 2026
07bd200
Run qbft_decided_resends suite against pluto
pinebit Jul 31, 2026
5313af7
Run parsigex_sender_binding suite against pluto
pinebit Jul 31, 2026
13e6210
Add coverage guard for the Rust pluto consumer
pinebit Aug 1, 2026
596161a
Note the wire_size_enforcement string-matching tradeoff
pinebit Aug 1, 2026
69f5488
Trim libp2p features to those the Rust consumer uses
pinebit Aug 1, 2026
c3e5594
Extend the consumer coverage guard to the Rust side
pinebit Aug 1, 2026
b2a7b8c
Document the pluto consumer's final verdicts
pinebit Aug 1, 2026
515bc64
Record Results row 10 for the coverage guard and docs task
pinebit Aug 1, 2026
6a96c8d
Skip Cargo build output in the spellcheck gate
pinebit Aug 1, 2026
ac81aab
Add proposal_timeout entry to the behaviour ladder
pinebit Aug 1, 2026
1a5a581
Correct review-found comment inaccuracies, sharpen findings
pinebit Aug 1, 2026
f6dfe27
Clarify what the Rust coverage guard checks vs the Go one
pinebit Aug 1, 2026
f58f1e6
Reclassify the default-config timer divergence as ABSENT-OK
pinebit Aug 1, 2026
998f04a
Mark the ladder-entry finding as addressed in its lead
pinebit Aug 1, 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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ targets `v1.7.1`): some specified behaviors landed after `v1.7.1`:
| ------------------------------------------------------------------ | -------------------- |
| `MsgSync.nickname` field (DKG sync) | `v1.9.0` |
| Deterministic (genesis-derived) eager double linear round deadlines | `v1.9.0` |
| Extended 1.5s proposer round-1 timeout (`proposal_timeout`) | `v1.9.0` |
| Linear round timer subsequent-round timeout fix | unreleased (`v1.11.0` RCs) |
| QBFT DECIDED-resend rate limit and message size/count limits | unreleased (`v1.11.0` RCs) |
| Preferred priority protocol ID `/charon/priority/2.0.0` | unreleased (`main`) |
Expand Down
6 changes: 6 additions & 0 deletions charon_anchor.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,12 @@
"first_charon_release": "v1.9.0",
"spec": "docs/dv-spec/consensus.md"
},
{
"name": "Extended 1.5s proposer round-1 timeout (proposal_timeout)",
"first_charon_release": "v1.9.0",
"spec": "docs/dv-spec/consensus.md",
"note": "The behaviour itself is older (charon #3739) but sat behind the alpha proposal_timeout feature flag; v1.9.0 (charon #4296) enabled it by default. The timer_deadlines vectors assume it is enabled."
},
{
"name": "Linear round timer subsequent-round timeout fix",
"first_charon_release": null,
Expand Down
44 changes: 43 additions & 1 deletion consumers/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ executes them: a suite nobody runs is a document, not a test.
| Consumer | Status | Location |
| --------------------------------- | ---------------------------------------------- | ----------- |
| Charon (Go) | All 9 suites, 314 subtests, verified at anchor `6054bcb2` | [`go/`](go) |
| Pluto (Rust) | Not written yet | — |
| Pluto (Rust) | All 9 suites checked against pluto `67088a2` — 2 FAIL (`qbft_hashing`, `cluster_hashing`), rest PASS, ABSENT-OK, or UNREACHABLE; see plans/pluto-conformance.md | [rust/](rust) |

These files live here rather than in Charon because this repository cannot merge
into Charon. They are laid out to mirror Charon's own tree, so placing them is a
Expand Down Expand Up @@ -81,3 +81,45 @@ produced by Charon and then published, so a failure means Charon has moved away
from the protocol every other implementation was told to implement. The
regeneration path is deliberately not automatic — see the spec's
`test_vectors/README.md`.

## Pluto consumer

`rust/` is a standalone Cargo package that path-depends on a pluto checkout
placed as a sibling of this repository (`../pluto` relative to
`distributed-validator-specs/`, i.e. `../../../pluto` from `consumers/rust/`).
Unlike the Go consumer, nothing is copied into pluto and pluto is never
modified — the harness reads pluto's crates in place.

### Running it

```bash
# pluto checked out as a sibling of this repo (default: ../pluto)
cd consumers/rust && cargo test
```

Vectors are read from `../../test_vectors/` at test time (no vendoring), or
from `SPEC_VECTORS_DIR` when set (same variable as the Go consumer), for
running against an unreleased build without moving files around.

### Verdicts

All nine published suites were run against pluto commit `67088a2`. This is a
snapshot of one pluto commit, not a continuously tracked target: `PASS` means
pluto reproduced every case the vector covers at that commit; `ABSENT-OK` and
`UNREACHABLE` are not passes — they record, respectively, a divergence excused
by pluto's charon-v1.7.1 parity pin, and a coverage gap where no public pluto
API reaches the behaviour at all. Full per-case detail, findings, and pluto
file:line citations are in `plans/pluto-conformance.md`'s Results table and
Findings section.

| Suite | Verdict |
| --- | --- |
| `secp256k1_signatures` | PASS (2/2) |
| `qbft_hashing` | **FAIL** — 23/25 cases agree; 2 pinned divergences (prost omits default-valued protobuf map-entry key/value fields that charon's Go marshaler always emits) |
| `bls_threshold` | PASS (all 5 groups) |
| `cluster_hashing` | **FAIL** — pluto rejects charon-legitimate JSON shapes (`operators: null`, an absent `partial_deposit_data`, and a masked third gap on `timestamp`) |
| `priority_scoring` | PASS (18/18) |
| `timer_deadlines` | `round_timeout_nanos` PASS **only with `ProposalTimeout` explicitly enabled** (9 PROPOSER/round-1 cases diverge under pluto's default config — ABSENT-OK, ladder entry "Extended 1.5s proposer round-1 timeout (proposal_timeout)"); `deadline_nanos` ABSENT-OK; `duty_start_delay_nanos` UNREACHABLE |
| `qbft_msg_limits` | `counts`: 5 MATCH + 6 ABSENT-OK; `wire_size`: 4/4 MATCH |
| `qbft_decided_resends` | ABSENT-OK (pluto has no DECIDED-rebroadcast limiter) |
| `parsigex_sender_binding` | `cases` ABSENT-OK; `peer_map` UNREACHABLE |
2 changes: 2 additions & 0 deletions consumers/rust/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/target
Cargo.lock
40 changes: 40 additions & 0 deletions consumers/rust/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
[package]
name = "spec-vectors-pluto"
version = "0.1.0"
edition = "2024"
publish = false
description = "Runs the spec's test_vectors/ suites against a pluto checkout at ../../../pluto"

[dependencies]
serde_json = "1.0"
hex = "0.4.3"

[dev-dependencies]
crossbeam = "0.8.4"
cancellation = "0.1.0"
pluto-k1util = { path = "../../../pluto/crates/k1util" }
pluto-core = { path = "../../../pluto/crates/core" }
pluto-consensus = { path = "../../../pluto/crates/consensus" }
pluto-crypto = { path = "../../../pluto/crates/crypto" }
pluto-cluster = { path = "../../../pluto/crates/cluster" }
pluto-eth1wrap = { path = "../../../pluto/crates/eth1wrap" }
pluto-priority = { path = "../../../pluto/crates/priority" }
pluto-p2p = { path = "../../../pluto/crates/p2p" }
pluto-featureset = { path = "../../../pluto/crates/featureset" }
pluto-parsigex = { path = "../../../pluto/crates/parsigex" }
pluto-eth2api = { path = "../../../pluto/crates/eth2api" }
pluto-eth2util = { path = "../../../pluto/crates/eth2util" }
pluto-testutil = { path = "../../../pluto/crates/testutil" }
rand = { version = "0.8", features = ["std_rng"] }
k256 = { version = "0.13.4", features = ["ecdsa", "sha256"] }
prost = "0.14"
prost-types = "0.14"
tokio = { version = "1", features = ["macros", "rt", "rt-multi-thread", "time", "sync", "test-util"] }
tokio-util = "0.7.11"
futures = "0.3"
async-trait = "0.1.89"
chrono = "0.4"
libp2p = { version = "0.56", features = ["noise", "tokio", "yamux"] }

[patch.crates-io]
multistream-select = { path = "../../../pluto/third_party/multistream-select" }
3 changes: 3 additions & 0 deletions consumers/rust/rust-toolchain.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[toolchain]
channel = "1.95.0"
components = ["rustfmt", "clippy"]
33 changes: 33 additions & 0 deletions consumers/rust/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
//! Loader for the spec's test_vectors/ suites.
//!
//! Vectors are read from `../../test_vectors/` relative to this package, or from
//! `SPEC_VECTORS_DIR` when set. `load_suite` refuses a file whose `suite` field
//! does not match the requested name, so a stray copy cannot silently substitute.

use std::path::PathBuf;

pub fn vectors_dir() -> PathBuf {
match std::env::var_os("SPEC_VECTORS_DIR") {
Some(dir) => PathBuf::from(dir),
None => PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../test_vectors"),
}
}

pub fn load_suite(name: &str) -> serde_json::Value {
let path = vectors_dir().join(format!("{name}.json"));
let raw =
std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("read {}: {e}", path.display()));
let suite: serde_json::Value =
serde_json::from_str(&raw).unwrap_or_else(|e| panic!("parse {}: {e}", path.display()));
assert_eq!(
suite["suite"],
name,
"suite field mismatch in {}",
path.display()
);
suite
}

pub fn unhex(s: &str) -> Vec<u8> {
hex::decode(s).unwrap_or_else(|e| panic!("bad hex {s:?}: {e}"))
}
Loading
Loading