Rust consumer: run the spec vectors against pluto - #13
Open
pinebit wants to merge 27 commits into
Open
Conversation
Restructures unsigned_data_set_hashing to skip and separately track the two cases where prost's map encoding diverges from charon's (empty key/value map-entry presence), so the FAIL finding stays visible without leaving the whole suite permanently red.
The skip-set assert_eq! only checks case names, not divergence status; the comment wrongly claimed it detects a case that stops diverging. Fixed the comment to attribute that detection to the sibling pinned test instead. Also drops line numbers from the charon/prost citations in the Findings entry, per the repo's no-line-anchor convention.
round_timeouts previously only tested with ProposalTimeout enabled, which overstated parity: pluto's default FeatureSet disables it, diverging from the vectors on proposer round-1 cases. Split into an enabled-feature test (matches the vectors) and a default-feature test that pins the divergence explicitly. Also add a real code-level pin for slot-invariance (deadline_nanos ABSENT-OK), replacing a doc-only claim with an assertion that fails if pluto's timers ever grow slot-dependence.
Asserts the published test_vectors/*.json suites match the COVERED table's declared suites, and that each declared test file exists. Mirrors the Go consumer's TestEverySuiteIsCovered.
read_protobuf_with_max_size reports every rejection through the same io::ErrorKind::InvalidData with no structured reason code, so matching on err.to_string() is unavoidable here despite test_vectors/README.md's "the slugs are the contract, wording is not" -- pluto's own p2p.rs tests use the same idiom.
The feature list was copied wholesale from pluto's workspace Cargo.toml. Only noise, tokio, and yamux are needed here; the rest (autonat, dns, identify, mdns, ping, quic, relay, secp256k1, macros, tcp) are unused by this consumer's code.
The Rust consumer's coverage.rs is not compiled here either, so mirror the Go check: parse COVERED out of consumers/rust/tests/coverage.rs and assert it matches the published suites, and that each declared test file exists.
consumers/README.md: expand the Pluto row and section with suite-by-suite verdicts, run instructions, and the pluto commit under test (67088a2). plans/spec-completion.md: mark the pluto consumer bullet done, pointing at plans/pluto-conformance.md's Results table.
The timer_deadlines vectors assume charon's proposal_timeout feature is enabled (stable since v1.9.0, charon #4296), but the behaviour ladder had no entry for it, so an implementation pinned to an older charon (pluto at v1.7.1) had nothing excusing the 9 diverging PROPOSER/round-1 cases. Record it in charon_anchor.json, the README compatibility table, and the consensus.md timer prose, which omitted the proposer round-1 exception the reference implementation and vectors already carried. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Re-validation of every recorded divergence (differential Go probes in the charon checkout, layered serde probes, and a source audit of pluto 67088a2) confirmed all findings. Three documentation defects surfaced in review: - qbft_msg_limits.rs presented a paraphrase of test_vectors/README.md's slug rule as a verbatim quote; reworded as a paraphrase. - qbft_decided_resends.rs claimed pluto's own test reaches quorum COMMIT; that test cancels at the node's own COMMIT broadcast, so the external COMMIT quorum is this harness's extension. Both comments now say so. - The plan attributed DKG's no-op parsigex verifier to Exchanger; the production wiring is in dkg/src/node.rs::setup_p2p (Exchanger's no-op verifiers are cfg(test)-only). Also sharpen the cluster_hashing finding: a layered probe proved the all_empty_lists blocker set is exactly operators/validators null-handling plus the missing timestamp default, and tick the plan's completed step checkboxes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Go's TestEverySuiteIsCovered also asserts each suite ships cases, read from its vendored release manifest. The Rust consumer reads test_vectors/ live with no manifest; each suite test pins its exact case count instead. Say so rather than claiming unqualified equivalence. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The proposal_timeout ladder entry added in ac81aab formally covers the 9 PROPOSER/round-1 cases that diverge under pluto's default FeatureSet, so the plan's Results row, its finding, the consumer README, and the timer test's comments now name that entry per the verdict taxonomy instead of calling it an uncovered known-divergence. Also record the 2026-08-01 re-validation pass in the plan: full suite re-run, both FAIL findings re-proven by differential execution, all pluto citations re-audited. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The bullet's bold lead still read as an open recommendation ("has no
entry") even though the entry landed in ac81aab; retitle it as an
addressed gap and fix the timer finding's cross-reference to it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds
consumers/rust/, a standalone Cargo package that runs all nine published vector suites against an unmodified pluto checkout. It path-depends on a sibling../plutoand is never copied into pluto, so a spec-repo user can test the current pluto implementation independently. This is the Rust counterpart toconsumers/go/, which does the same for charon.Also adds
plans/pluto-conformance.md, the plan this was executed from, which carries the per-suite Results table and the Findings section, and one spec-side change the findings called for:charon_anchor.jsongains an "Extended 1.5s proposer round-1 timeout (proposal_timeout)" behaviours-ladder entry (v1.9.0), with the matching README compatibility-table row and a sentence indocs/dv-spec/consensus.md's timer prose stating the proposer round-1 exception the reference implementation and vectors already carried.Tested against pluto
67088a2. 31 Rust tests, all green. Both FAIL-class findings were re-proven by differential execution (a charonhashProtoprobe for the map-entry divergence, a layered serde probe for the cluster-parsing one), and every pluto file:line citation in the findings was re-audited against source.Verdicts
secp256k1_signaturesbls_thresholdpriority_scoringPrioritiserAPI)qbft_hashingcluster_hashingtimer_deadlinesProposalTimeoutenabled (the vectors' assumption); the default-config divergence on 9 PROPOSER/round-1 cases is ABSENT-OK against the ladder entry this PR adds; deadline ABSENT-OK; start-delay UNREACHABLEqbft_msg_limitsqbft_decided_resendsparsigex_sender_bindingcasesABSENT-OK;peer_mapUNREACHABLEABSENT-OK means the divergence is covered by a
charon_anchor.jsonbehaviours-ladder entry postdating pluto's charon-v1.7.1 parity anchor; those are pinned in code so they fail when pluto catches up. UNREACHABLE means no public API path exists from an external crate — a coverage gap, not a pass.Divergences found
UnsignedDataSetentries with an empty key or empty value therefore encode, and hash, differently. Confirmed by differential execution: a Go probe running charon'shashProto(core/consensus/qbft/msg.go) on both cases reproduces the vector bytes exactly. Reachable only via malformed or adversarial wire input, per code inspection of charon's encode path.operators: nullandvalidator_addresses: null(Go marshals nil slices as null), an absentpartial_deposit_datakey (Go omits it viaomitempty), and an absenttimestamp. A layered probe proved this blocker set complete for the shipped cases (nameanddeposit_amountsare already handled); charonomitemptyfields the cases don't exercise may hide more instances of the same missing-#[serde(default)]root cause.Feature::ProposalTimeoutoff by default, matching charon v1.7.1; charon at the spec anchor has it on, promoted to stable in v1.9.0. Default-configured pluto diverges from the vectors on all 9 PROPOSER/round-1 cases — ABSENT-OK against the ladder entry this PR adds, and pinned by a test that names it.4 * node_count(), which is neither charon v1.7.1's absence of a cap nor the spec's2n. Now pinned at its exact boundary.dkg/src/node.rs::setup_p2p), and the aggregation-time check that validates share indices lives in a private module, so the lock-hash exchange verifies nothing per message.Notes for this repo
tests/test_release.pyenforces.real_keys_3_of_4lock's operator EIP-712 signatures are placeholders, soverify_signaturesshort-circuits before the BLS-aggregate and node-signature checks that case exists to exercise. An external consumer cannot reach them; the Go consumer only can because they are in-package in charon. Recorded in the plan, not acted on.tox.inigains one skip socodespelldoes not scan the gitignored Cargo build directory.