Add CAP-0089: VRF-Based Protocol Randomness and Fair Leader Selection - #2005
Add CAP-0089: VRF-Based Protocol Randomness and Fair Leader Selection#2005EslaM-X wants to merge 53 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
Adds draft CAP-0089, proposing VRF-based protocol randomness and leader election.
Changes:
- Defines VRF transcript, beacon, sub-seed, and upgrade semantics.
- Adds proposed XDR changes and CAP registry entry.
- Adds conformance-vector fixtures, generator, and checker.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 23 comments.
Show a summary per file
| File | Description |
|---|---|
core/README.md |
Registers CAP-0089 as Draft. |
core/cap-0089.md |
Defines the protocol proposal. |
contents/cap-0089/xdr-diff.md |
Records proposed XDR changes. |
contents/cap-0089/gen_vectors.py |
Generates derivation vectors. |
contents/cap-0089/fixture_vectors.json |
Stores pinned fixture values. |
contents/cap-0089/check_vectors.py |
Checks selected conformance properties. |
Suppressed comments (1)
contents/cap-0089/check_vectors.py:110
- These checks only assert that freshly computed values differ from each other; none are compared with
V1_…,V4_…,V5_…, orV6_…in the JSON. As a result the checker returns success even when its slot calculation disagrees with every pinned fixture digest, so fixture corruption or a derivation change is undetected. Assert equality to each expected field in addition to the adversarial inequalities.
("V4 cross-network replay", H(NETWORK_ID_TESTNET, ledger_seq_next, 1) != H(NETWORK_ID_MAINNET, ledger_seq_next, 1)),
("V5 cross-slot replay", H(NETWORK_ID_TESTNET, ledger_seq_next, 1) != H(NETWORK_ID_TESTNET, ledger_seq_next + 1, 1)),
("V6 purpose separation", subseed(beta, NETWORK_ID_TESTNET, ledger_seq_next, 2).hex() != subseed(beta, NETWORK_ID_TESTNET, ledger_seq_next, 3).hex()),
💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
|
Superseded by later exact-head work. This correctly identified reveal/withhold optionality, but “bounded abort bias” is no longer the acceptance bar. Current invariant: one exact locked semantic close under one predecessor-fixed epoch yields one verified root, or remains UNKNOWN/wait. Participation may affect availability only, never select another root. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated 11 comments.
Suppressed comments (4)
core/cap-0089.md:151
- The VRF input is ambiguous:
T(s)requires apurpose, but the commit phase defines onebeta_vand one shared beacon while the only legal purpose values are the three consumer labels. Implementations cannot know whether to evaluate purpose0x01, produce three contributions, or use another beacon-specific label. Pin one beacon-generation transcript (preferably with its own label), then keep consumer separation insubseed(p), or define separate beacons consistently.
2. **Commit phase (ledger `s-2`).** Each validator `v`
evaluates `beta_v = VRF_prove(sk_v, T(s))`, computes the hiding commit
`C_v = SHA-256(beta_v)`, and publishes `C_v` while `s-2` is being decided.
core/cap-0089.md:315
- A preprocessor guard does not make this top-level struct member conditional on ledger protocol version. In a schema generated with
VRF_RANDOMNESS, everyStellarValueincludes these 32 bytes, so that schema cannot decode historical values that lack them; without the guard, it cannot decode new values. CAP-0088 avoids this by adding union discriminants while preserving old arms. This needs a runtime-versioned XDR shape that represents both legacy and VRF values, not an unconditional trailing field.
+ Hash vrfBeaconNext; // 32-byte SHA-256 (see "Hash" in Stellar-types.x)
contents/cap-0089/check_vectors.py:172
- As with V4, this only proves that hashing a different slot produces a different transcript digest; it does not test replay rejection. The beacon itself is computed from the same context-free placeholders and is unchanged for
slot + 1. Add a proof bound to slotsand assertVRF_verifyrejects it under thes+1transcript.
# --- V5: cross-slot replay ---
th_next_slot = transcript_hash(net_id, slot + 1, 1, anchor)
check("V5 cross-slot replay detected (beacon differs by slot)",
th_next_slot.hex() == t["V5_transcript_hash_cross_slot"]
and th_next_slot.hex() != th_leader.hex())
core/cap-0089.md:294
- The PR description says this change adds
STELLAR_VALUE_VRF = 5and aStellarValue.extarm, but the submitted CAP instead leaves the enum/union unchanged and adds a top-level field. These are materially different wire formats and transition models. Update the description if the top-level design is intended, or change the CAP/XDR to the advertised union design.
The randomness beacon is carried as a **new orthogonal top-level field** of
`struct StellarValue`, not as a new arm of the `ext` union. This is deliberate:
`StellarValue.ext` is a single discriminated union whose arms enumerate
value-shape variants (including CAP-0088's `MS_CLOSE_TIME` arms). Adding the
beacon as another union arm would make it mutually exclusive with those
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated 15 comments.
Suppressed comments (2)
contents/cap-0089/xdr-diff.md:17
- This “canonical” companion diff is not the same change as the authoritative embedded diff: it omits
VRFCommit,VRFReveal,vrfCommits, andvrfReveals. Replace it with the exact embedded diff (or stop presenting it as canonical), otherwise implementers following this file produce an incompatible schema.
## `Stellar-ledger.x` — add `vrfBeaconNext` to `struct StellarValue`
contents/cap-0089/check_vectors.py:167
- For this fixture
Q = 3, the CAP's stated threshold ist = 3; after dropping one contribution,k = 2, so the specified result is the LCL fallback, notbeacon(remainder). This check never applies the threshold or fallback and therefore does not implement V2. Add pinned fallback context and assert the threshold branch's expected beacon.
remainder = contribs[1:] + contribs[:0]
dropped = beacon(remainder)
check("V2 withholding one reveal excludes it and recomputes the aggregate "
"deterministically by NodeID order",
dropped != beacon(contribs)
|
The problem is canonical replay: every node and catchup path must derive the same accepted randomness from consensus-persisted evidence, without local quorum observations or a minority-selectable fallback. That requires one protocol object that fixes contributor membership, authentication/uniqueness, omission behavior, replay evidence, and fallback semantics. Can Stellar’s FBA model supply that object cleanly? If not, I’d keep the VRF primitive reviewable on its own and leave protocol randomness Draft until the missing membership/reconstruction primitive is specified. — Noot’s Raven 🐦⬛ — once the participant set is truly canonical, every later layer gets firmer ground to build on. |
|
Superseded exploratory mechanism. The participantless delayed-beacon/VDF direction is not the current CAP-0089 path. Sequential delay also does not by itself defeat parallel candidate evaluation. Retained invariant: validator role, hardware speed, identity count, or timing must create zero additional value-selection privilege. Current direction is a Core-owned externalized-close lock plus an irreducible unique-threshold crypto primitive. |
|
The problem is identity-neutrality: splitting one economic controller across more validator identities must not increase its randomness choice while underlying consensus influence stays the same. The current per-
Can the proposed beacon satisfy that property without inventing a canonical real-world controller registry? If not, that is strong evidence to change the entropy model rather than reward identity proliferation. Also correcting my earlier VDF direction: sequential delay alone does not solve parallel candidate evaluation. The invariant is role privilege → 0 additional bias. — Noot’s Raven 🐦⬛ — equal influence should stay equal however many validator keys an operator happens to use. |
|
Superseded as a source architecture. The useful invariant remains: routing, identity splitting, retries, or ledger construction must not create extra root choice. But the current direction no longer depends on an independent future public-randomness source or consumer-specific reveal timing. Current target is one Core-owned pulse from the exact externalized semantic close under a predecessor-fixed epoch, with labeled APPLY/PRNG/NOMINATION derivations and UNKNOWN/wait until one unique proof verifies. |
|
One more acceptance issue: the test itself should not have a favorable-path escape hatch. For a fixed committed state, enumerate every spec-valid continuation across reveal subsets, the threshold edge, fallback, retries, timing boundaries, and the three consumer derivations. Score the worst reachable choice window, not one selected trace. Then perturb each protocol knob around its real boundary. If a one-step change in reveal count or a small timing shift sharply increases reachable roots or time-to-lock, that cliff belongs in the vectors. Could — Noot’s Raven 🐦⬛ — make every valid route clear the same safety bar, and the measurement becomes as trustworthy as the beacon. |
|
Superseded by the later one-close Core-native design. The useful finding here remains: domain separation cannot repair randomness revealed before the protected input is locked. But the current target no longer uses separate Current shape: one verified |
|
One stronger neutrality rule: the randomness source should be causally neutral across the whole dependency graph, not just unbiased at the instant it appears. For a committed event
Acceptance target: none may change which root is valid, create a second valid root, or let someone discard an observed result for another one.
The compute term matters even for tiny windows: if faster CPUs, GPUs, accelerators, or finer clocks let an actor test more candidate futures before lock, that extra search belongs inside the bias budget. The target should remain invariant as that capability scales. This also sharpens the two-clock model: each protected decision gets a future event committed before the decision, revealed only after that decision can no longer move, and downstream logic cannot choose whether the revealed root counts. Could the harness model these as counterfactual equivalence classes and require — Noot’s Raven 🐦⬛ — if no amount of timing, routing, or faster hardware buys a second valid future, neutrality becomes a property of the system rather than the clock. |
|
Problem: the beacon can be neutral while its use is still gameable. The current draft makes The last-revealer I think the stronger invariant is causal sealing, per consumer:
After reveal, retry/recovery must reproduce the same value or fail closed. Security should not depend on the choice window being “too short”; faster hardware must never create another valid future. That may mean one beacon/source family with different reveal horizons for nomination, Soroban PRNG, and apply order rather than one public Could the CAP add lifecycle vectors for pre-lock shaping, last-reveal/fallback, post-reveal submit/retry, and future key/algorithm migration? — Noot’s Raven 🐦⬛ — seal every choice before the reveal, and faster machines only reach the same answer sooner. |
|
Problem: one shared beacon is revealed too early to be the safest seed for all three consumers. The CAP finalizes A stronger acceptance target may be one precommitted randomness stream with consumer-specific reveal horizons:
Then the harness can require Would this be a cleaner top-level target than forcing one already-public — Noot’s Raven 🐦⬛ — one clock can serve three doors, as long as each door opens only after its choices are sealed. |
|
Problem: the security model still exists in more than one form, and the current V2 checker proves a residual choice rather than removing it. At head
The CAP itself correctly acknowledges the last-revealer in/out choice. That means RFC 9381 uniqueness for a fixed key/input is not yet protocol neutrality: after commitment, a valid actor path can still select between two accepted roots. A stronger acceptance surface is:
And the test should be source-neutral: summary, CAP, XDR, fixtures, and checker are peers. If they disagree, fail the consistency check rather than choosing one as the baseline. Could V2 be changed from “withholding deterministically produces a different beacon” to “every allowed reveal/recovery path after lock produces the same root, or explicitly stops” — with threshold/fallback behavior actually covered by the fixture? — Noot’s Raven 🐦⬛ — make the last choice disappear, then let every surface prove the same future. |
|
Problem: V2 still lets participation select the root. The current checker requires the dropped-contributor beacon to differ from the full beacon, and the fallback selects a different entropy source. The stronger acceptance property is simpler:
Below threshold, liveness handling must not create a second random root. This construction is practical, not hypothetical: unique threshold-signature beacons reconstruct the same group signature regardless of which valid threshold subset supplies the shares. drand documents that property directly, and DFINITY’s beacon work defines uniqueness the same way. https://docs.drand.love/docs/cryptography/ Could — Noot’s Raven 🐦⬛ — let the quorum decide whether the door opens, never what is behind it. |
|
Problem: subset-invariant randomness needs the threshold group fixed before the round, but Stellar consensus is configured from each node's local quorum set while this CAP defines contributors from validators that signed the prior externalized value. That leaves a prerequisite to pin: what consensus object fixes the randomness group key, member/share set, threshold, and activation epoch before those members can influence the target round? Without that object, a unique threshold primitive can still inherit ambiguity from how its group was chosen. Could the CAP make a — Noot’s Raven 🐦⬛ — freeze the team before the draw, then let every valid subset recover the same future. |
|
Problem: The acceptance property I’d add is:
We should not assume accounts map 1:1 to people, organizations, machines, or independent operators. If the protocol cannot establish a Sybil-resistant weighting source, raw identity count should not be treated as independent security weight. Could the randomness-epoch definition make its membership/weight source explicit and require an identity-splitting test: clone one controller into N validator identities while holding its underlying consensus influence constant, and prove the reachable random roots do not increase? — Noot’s Raven 🐦⬛ — count independent influence, not usernames. |
|
Problem: even a unique, split-resistant beacon can leak influence through transport. SCP envelopes are broadcast over the overlay, so peers naturally observe valid data at different times; delivery state must not become randomness authority. For each consumer I’d require:
Delay, reorder, duplicate, selective relay, partition, rebroadcast, timeout, or invalid alternatives may change availability/latency only. None may select another root. Could the vectors permute those delivery schedules and assert both — Noot’s Raven 🐦⬛ — let the network move the message, never the future. |
|
Problem: even a unique beacon can lose neutrality if consensus metadata is allowed back into the pulse identity. I’d make the boundary one-way:
Only after that value exists may consensus notarize it. Quorum path, proposer, signer count, committee shape, proof layout, arrival order, retries, partitions, or later consensus changes may affect availability, but none may be an input to That gives a stronger acceptance test: for one locked event and one accepted source root, vary every valid consensus/notarization path and require exactly one pulse id (or explicit no-pulse before notarization). Could the CAP define the beacon so consensus consumes a pre-existing pulse rather than participating in its identity? — Noot’s Raven 🐦⬛ — let the network agree on the draw without ever becoming part of the draw. |
|
Problem: consensus still participates in constructing the random value. Contributor eligibility comes from the externalized A stronger boundary is:
Then consensus may only attest Acceptance vector: hold the locked event and valid source evidence fixed, vary every consensus/notarization path, and require one identical Could this be added as a conformance vector before deciding whether commit/reveal aggregation belongs inside the consensus value at all? — Noot’s Raven 🐦⬛ — let consensus seal the envelope without ever choosing what is inside. |
|
Updated after the full exact-head recheck. The governing invariant still stands, but the current target is stricter and simpler:
There is no value-bearing fallback and no current The neutrality requirements remain: subset, identity, transport, compute, recovery, migration, implementation, and observation differences may change availability/knowledge time only; they must never create another valid root. |
…ntext vectors - check_vectors: V4/V5 now isolate PROOF-context binding. The prior cross-network/cross-slot tests rejected at the commit-signature scope; they never exercised proof binding. Each target context (mainnet-s, testnet-s+1) is built fully self-consistent (VRFCommit.sig re-signed under the target scope, target transcript, target-derived proofs) and ACCEPTED as a positive control; substituting only the wrong-context proof bytes into the identical rows is REJECTED -- valid signature, valid commit, valid transcript, correct roster, wrong-context proof fails at the PROOF gate, never the signature gate. Non-vacuous checks confirm target-context proofs differ from source-context proofs. checker now 44 PASS. - cap-0089: new 'Security, Correctness and Conformance Evidence (Review Package)' annex: design-freeze statement, 14 precise invariants I1-I14 each mapped to real executable check labels, property x evidence matrix, explicit threat model (assumptions A1-A6, may/may-not), 16-row attack catalog with fail-closed mechanisms and tests, runtime-decidable state machine with failure/disambiguation paths, why-this-mechanism table, resource/cost analysis with canonical wire widths, and a reviewer response ledger mapping 17 review rounds to root-cause/resolution/regression. Ends with the frozen design statement: remaining work is conformance + independent review.
Final audit commit: security/correctness evidence package + isolated proof-context vectorsBuilding on the last two review rounds (12 threads resolved, 0 outstanding), I treated the deep-review findings as a packaging and semantics problem, not a prose problem. Summary of what changed and why each item directly answers a reviewer's claim: 1. The V4/V5 concern is fixed at the semantics level, not the sentence level.
The same rows are then verified under the target context and ACCEPTED (positive control), while substituting only the wrong-context proof bytes into those identical rows is REJECTED. Because the sole differing field is the proof bytes, the failure is at the proof-context gate — valid signature, valid commit, valid transcript, correct roster, wrong-context proof — never a signature-scope failure. Non-vacuity is asserted separately (target-context proofs differ from source-context proofs). 2. A consolidated evidence package is now in the CAP annex ("Security, Correctness and Conformance Evidence"), so the review history doubles as the audit surface:
3. Design freeze. I consider the protocol design frozen at this point. No new mechanism, state, consumer, or fallback is added except in response to a reviewer showing the existing surface insufficient. I therefore do not claim this is "fully secure" or "production ready" — no protocol proposal should. The remaining work is implementation and conformance: a second, independent native Core/Rust implementation cross-checked against the same deterministic vectors, an independent cryptographic review of the construction, and the measured acceptance benchmarks listed under the CAP's Exit Criteria. That is where the residual risk sits, and it is reviewable there. Reproduce everything deterministically (no live network): I would especially welcome an attempt to construct two distinct valid roots, bias the root through subset selection, or make a proof validate under a different (network, slot, epoch, roster, close) context — that is the concentrated risk surface of this design, and the point of the isolated V4/V5 harness is to state plainly which failures the tests exercise and which they do not. — Eslam |
…er/qualification/reconstruction, full-register dealer pool, committed E_j vector and release registry, one consensus-bound nomination branch per slot, pre-activation liveness bound (Copilot 3936620188,3936620224,3936620244,3936620265,3936620296,3936620332,3936620357,3936620399,3936620432,3936844061,3936844117,3936844169,3936844242,3936844284,3936844318,3936844352)
…alidation), secret-free public AVSS record with proportionate ElGamal + public NIZK and decrypted-shares reconstruction, public release-capability binding, single canonical V_s/C_s encoding, reconciled no-draw/wait-for-root consumer semantics, refreshed check counts (Copilot 3939109826,3939109841,3939109855,3939109864,3939109883)
…ied plaintext, reconstructable scalar constants) feeding the distributed FROST proof assembly, fail-closed public AVSS parsing (I13), consensus-carrier ReleaseRegistry admitted only from decoded committed state, committed+consensus-validated roster-complete CONFIRM certificate, ledger-state (not self-hashed value) registry persistence, refreshed check counts (Copilot 3939264478,3939264494,3939264509,3939264532,3939264544,3939264557)
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 7 changed files in this pull request and generated 10 comments.
Suppressed comments (1)
contents/cap-0089/check_vectors.py:262
- Correct the typo in the RFC 9381 description.
# cofactor 8 (identicate dimensions: suite 0x03, TAI)
|
Round 24 closes a lot — especially the scalar-AVSS and registry-cycle fixes. Nice progress. One liveness implication still doesn’t hold: a roster-complete certificate with Concrete admitted case: This matters directly to Core because the CAP makes APPLY mandatory while current SCP externalizes as part of Can CAP-0089 either prove |
|
Correction: my last counting fix still assumed Commit the epoch’s roster and the exact SCP qset graph it relies on (pin those qset hashes for the epoch; a relevant qset change rolls the epoch), then have Core’s Rust SAT checker reject activation unless, for every admitted fault pattern
and every admitted Byzantine set Then any SCP-live close has |
… review of the global n/f/t vs local quorum-slice objection
Round-26 (tacticalnoot 5549653283): epoch COMMITS the flat SCP quorum-set graph (scp_qset=(q,members), scp_qset_hash in epoch.hash; relevant qset change rolls the epoch); activation SAT gate C0/C1/C2 enumerates every admitted Byzantine set B (SCP_FINALIZABLE(B) => |Roster n Intact(B)| >= t, |Roster n B| < t, no Byzantine-own quorum) and rejects malformed graphs at construction; flat roster-CONFIRM certificate-counting liveness claim discontinued (liveness now from committed topology + gate). Model 108 PASS, vectors 47 PASS (re-pinned EXP_B3/EXP_COMP).
Round-27 (0xEmpty, Discord, CAP-0089): formal security review section -- implicit global n/f/t vs SCP local quorum-slice model ('the set that finalizes a ledger can differ from the set the shares were dealt to'; 't CONFIRM voters can include f Byzantine ones, so you'd need t+f voters to guarantee t honest'): component x assumption x SCP-reality x risk x closure matrix (n/f/t/RandomnessEpoch/DKG/certificate/t+f rows), repairable-not-abandoned decision (committed topology repair adopted; leader-VRF V1 recorded as fallback, not adopted). Executable checks R27-a (finalizer != share-dealt roster neutralized at activation: 4-of-8 subset quorum still leaves >= t intact roster; widened fault class fires C1; honest q=t graph C1-immune), R27-b (threshold-sized cert externalizes but f Byzantine cert-voters withhold release: recover_proof(t-f) is None -> UNKNOWN, never a root; liveness from committed topology not cert count), R27-c (committed member-list mutation rolls epoch; permutation canonicalizes to same graph). Model 111 PASS, vectors 47 PASS (pins stable)
|
@tacticalnoot — you are right on both counts, and 5549653283 supplies exactly the The committed topology contract (round-26). The epoch now COMMITS the exact
The executable proof (model, rounds 26–27).
A dedicated "Security review: the implicit global n/f/t model vs SCP's local — Eslam |
|
@tacticalnoot — your 5549584433 counterexample and 0xEmpty's independent Discord The Your — Eslam |
… encryption with honest-dealer reconstructibility bound (R28-a), consensus-deterministic closed-value registry presence with blocking construction, strict canonical registry decode, C1 min-quorum activation gate over actual finalizing quorums, corrected primitive dependency status (Copilot 3942590381,3942590393,3942590403,3942590407,3942590416,3942590421)
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 7 changed files in this pull request and generated 5 comments.
Suppressed comments (3)
contents/cap-0089/check_vectors.py:262
- Correct the typo “identicate” to “identical.”
# cofactor 8 (identicate dimensions: suite 0x03, TAI)
contents/cap-0089/check_vectors.py:1362
- This describes “no-pulse” as a Layer-B exit criterion, but the CAP explicitly prohibits a
NO_PULSEoutcome and usesUNKNOWN/stall instead. Remove the stale term so the checker documents the same state machine as the proposal.
check("Layer B gate: subset-invariance/no-pulse is a Draft exit criterion, "
"not satisfied by -- and not asserted for -- the Layer A aggregate",
True)
contents/cap-0089/check_vectors.py:1110
- This does not isolate network-context binding as claimed:
vrf_seedscontains testnet-derived keys, while the target rows and verifier use mainnet-derived public keys. The substituted proof is therefore invalid under the target key regardless of its transcript, so this test can pass even ifalpha_stringis ignored. Generate the wrong-alpha proof with the mainnet secret key so the public key remains fixed and only the transcript differs.
test_proofs = {nid: _ecvrf_prove(vrf_seeds[nid],
transcript(net_id, slot, 1, anchor))
for nid in commit_auth}
…once leaks e_d while the secret-nonce form defeats recovery (ALL PASS 114)
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 7 changed files in this pull request and generated 4 comments.
Suppressed comments (1)
contents/cap-0089/check_vectors.py:1112
- The claimed proof-context isolation also changes the VRF key:
vrf_seedscontains the testnet-derived keys, whilemain_rowsis verified under_main_vp's mainnet-derived keys. Rejection can therefore be caused by the wrong public key rather than the network embedded inalpha. Produce the wrong-alpha proof with the same mainnet secret key so only the transcript context differs.
test_proofs = {nid: _ecvrf_prove(vrf_seeds[nid],
transcript(net_id, slot, 1, anchor))
for nid in commit_auth}
main_bad = [(nid, beta, th, test_proofs[nid])
for nid, beta, th, _pi in main_rows]
…4, foqqj, foqrG): C1 sharpened to every quorum leaving |Q n Intact(B)| >= t intact releasers with flat default qset (t+f, all n) -- rejects 4-of-8, 5-of-8 containing Byzantine, and (10,7,3,7) at activation; per-member AVSS per-close nonce/share availability (Byzantine nonce dealer inert, skip-not-stall) with member-owned round-1 nonce commitments; Verifier binds the round-1 aggregate nonce (committed_R) rejecting a Schnorr-valid wrong-R proof; registry presence is a field of slot-s's OWN balloted value XDR via the deterministic canonical carrier (absent carrier is a different ballot value) (ALL PASS 116)
…ned, fpkED committed-graph authority, alpha-isolated V4/V5 negatives + genuine V6 ECVRF vectors, fpkD5 topology-gate None-path fix - Distribution-close gate (fos67/foqrG/foqqv): the AVSS distribution CLOSES (epoch committed+activated) only if >= t HONEST members hold valid share+nonce; a QUALIFIED garbage-everyone Byzantine dealer (well-formed NIZKs, same Cks/r1/points, stays in Q*) yields 0/7 honest valid -> close WITHHELD, epoch rolled BEFORE any externalized close (no post-commit stall reachable); honest set == t so even one honest garble withholds (6 < 7). ALL PASS 116 -> 117. - fos67: |P| >= n-f with f exclusions only gives |Q*| >= n-2f never n-f; the '|Q*| >= t' claim is DEAD and replaced by the close-gate (n=10,t=7,f=3 -> floor 4 < t). - fpkED: committed flat (q,members) made AUTHORITATIVE for epoch finality by a Section-5 deployment contract (Core nested SCPQuorumSet at one flat level), qset mutation rolls the epoch (R27-c), a foreign-qset node leaves the epoch not forks. - check_vectors: V4/V5 rewritten as ALPHA-ISOLATED negatives under target mainnet/testnet keys (only ECVRF_verify rejects; old key/beta-confounded and beta-swap tautology gone); new V6 genuine RFC 9381 acceptance vectors under the fixture's own committed testnet keys (fos6s). Still ALL PASS 47. - fpkD5: _avss_gate_flat_ok None path (n-|B| closed form) accepted unlive topologies e.g. (8,5,1,6,1..6); now routed through exhaustive per-B SAT and that case rejected; R27-c partial graph rebuilt sound (t=5,f=1,q=6,members 1..7, m-f >= q).
…es a PUBLIC authenticated-certificate count -- the distribution CLOSES iff >= t+f distinct roster members hold publicly verified material-OK certificates (Schnorr sigs under epoch-committed K_i issued exactly on each member's OWN envelope+nonce recovery; NO honesty oracle, NO decryption; certified - f >= t lattice = the same q >= t+f flat rule as foqq9); slot-s ballots carry NO presence/absence -- the pulse-vs-fallback branch is a PROOF-INDEPENDENT read of the already-closed SOURCE C_{s-1}'s OWN registry, one balloted value per target slot; both nomination branches derive from C_{s-1}/H(value_{s-1}) with the target slot number only as a domain separator -- fallback never references C_s (ALL PASS 120)
…22 PASS)
fw_fL: ReleaseRegistry admits only internally-verified threshold proofs
(UniqueThresholdProof.verify derives the root from the carried 64-byte P_s);
the forgeable �uthenticator callback is removed and bare 32-byte
root-shaped payloads are rejected.
fw_fU: dealer-set digest (_avss_dealer_digest over full canonical qualified
record bytes) is bound into every material-OK certificate and the
distribution-close gate closes only under ONE committed digest, so batches
certified against divergent local dealer sets cannot combine (RB
canonicality no longer relied on).
fw_ff: release registry is read from the CLOSED SOURCE's frozen post-close
LedgerHeader.ext register (the canonical_proof section), never from any
balloted value - P_{s-1} exists only after the s-1 CONFIRM/EXTERNALIZE
boundary (temporal-cycle revision of foQhh/foqq4 ballot-value phrasing).
fw_fl: RandomnessSource gains require_distribution_close+admit_distribution;
admit() refuses a close whose AVSS distribution close was not publicly
certified (activation seam, real-path rejection test).
| Ki = _validator_confirm_pub(GROUP_SK, i) | ||
| return _avss_material_cert_verify(Ki, i, epoch, cl_hash, | ||
| cert_records.get(i), dealer_digest) |
| function `f(committed register digest)`, and every material-OK certificate | ||
| BINDS that digest** — the certificate statement is | ||
| `(epoch, C_s, dealer_set_digest, member_index)`, PURELY publicly verified | ||
| under the epoch-committed `K_i`. The distribution-close gate closes only | ||
| when `≥ t + f` certificates verify under **ONE committed digest**, so: |
| toward `C_{s-1}`, the ENTIRE target slot `s` uses the root pulse | ||
| `KDF(R_{s-1}, "NOMINATION")`; otherwise the ENTIRE slot uses the single | ||
| closed-form fallback `NominationFallback(epoch_hash, target_slot=s, | ||
| C_{s-1}, H(value_{s-1}))` — in BOTH branches the inputs are exclusively | ||
| epoch + the TARGET slot number (domain separator) + the CLOSED SOURCE | ||
| `C_{s-1}` and its value hash, and **the target's own value `C_s` does not |
| if len(value_bytes) == 32: | ||
| return value_bytes # V_s as-is -- no double hash | ||
| return sha256(b"ProvenanceExcludedValue/v1" | ||
| + provenance_free_value(value_bytes)) |
| both honest branches are pure functions of that close. The registry list — | ||
| the unique `P_{s-1}` if the edge was crossed, else the canonical absent | ||
| marker — is written into the source close's post-close `LedgerHeader.ext` | ||
| as the edge completes, with a deterministic, strict-sorted encoder, so the |
|
Hey @tacticalnoot, I wanted to get your take before I make any changes to the current PR. I was discussing the contribution process with Tupui on Discord, and he pointed out that the current workflow is discussion-first rather than PR-first, and that the updated guidelines don't accept unsolicited work. That made me wonder whether I approached CAP-0089 from the wrong entry point by developing the implementation and opening the PR before going through the protocol discussion stage. From your perspective, would the right move now be to stop pushing #2005 and take the proposal back to a discussion first, gather protocol-level feedback, and only then determine whether it should become a CAP/implementation PR? I also want to be transparent about my broader goal here: I'm planning to apply for SCF Build Round 46 and would like to use the funding to take CAP-0089 through the remaining Core/Rust implementation, conformance testing, benchmarks, security review, and testnet work — assuming the protocol direction is something the ecosystem actually wants. So I'd rather get the protocol process right first than keep building in the wrong direction just for the sake of the grant application. If you think there's a better path for validating the proposal before I take it further, I'd really appreciate your guidance. |
CAP-0089: VRF-Based Protocol Randomness and Fair Leader Selection
This PR adds the initial Draft of CAP-0089, which brings protocol-level,
single-pulse, one-future randomness to Stellar's consensus -- hardening the
leader-influencable per-ledger randomness derived from the last-closed-ledger
(LCL) hash that today seeds nomination priority, the Soroban PRNG, and
transaction apply order.
This PR presents an accurate Layer A / Layer B split, with **one deployment
path** (aligned with the CAP body and the review that resolved the earlier
deployment ambiguity):
Layer A (research evidence only -- no deployable transition): the
ECVRF-EDWARDS25519-SHA512-TAIper-reveal aggregate. It is kept as evidenceand as a first-class primitive, not pitched as the shipped
protocol-randomness wire format. The trailing
StellarValuefields in the XDRdiff are the conformance/evidence reference and are **not frozen as a
shipping format** -- Stellar never migrates through an intermediate
trailing-field wire representation.
Layer B (the single deployment contract): the precommitted
RandomnessEpochone-future construct. Each closed ledger has exactly onecanonical close commitment `C_s = H("CloseLock" ||
RandomnessCloseInputV1(network_id, epoch_hash, ledgerSeq, previousLedgerHash,
H(externalized_value_bytes)))
, and thatC_s` is the protected challenge --the only threshold-signed and only verified domain, with no separate
"Challenge"preimage and no caller-selected purpose/class/object (onepulse per locked ledger close). All three consumers derive labeled sub-seeds from the one
source root
R_s = root(P_s):KDF(R_s, "PRNG")(Soroban PRNG),KDF(R_s, "APPLY")(transaction apply order), andKDF(R_s, "NOMINATION")(next-ledger nomination priority). There is **no dual
R_pre/R_posttimingmodel** and no separate nomination path.
Per the review, the authority boundary is the **CONFIRM -> EXTERNALIZE ->
valueExternalized** transition (neveracceptedCommit/setConfirmPrepared,which are not safe-to-act finality), and a share is released only for the
exact externalized value bytes, only once that value is **locally fully
validated**. The executable architecture is:
Core owns the canonical close-commitment / challenge derivation from
the externalized close (no caller choice), epoch activation, the replayable
lock witness, and replay.
Rust (behind the existing bridge) owns **proof verification and root
derivation**, with a proof/root split (dnFVg): the source emits an
unforgeable proof for the exact externalized value bytes, and the root is
derived only after that proof verifies.
The threshold share is generated in the CONFIRM->EXTERNALIZE emit path
(before
emitCurrentStateStatement, so it rides the envelope Core emits;not at
valueExternalized, which Core calls after emission) and carried withexisting EXTERNALIZE propagation (no extra consensus phase, no
pre-finality root oracle). Missing proof means apply is stalled (UNKNOWN),
never an alternate entropy branch and never a timeout-derived no-pulse -- there
is no
NO_PULSEoutcome.It is the protocol-tier completion of the work reviewed on
stellar-core issue #4388
and builds directly on the reviewed cryptographic building block in
stellar-core PR #5409
(
ECVRF-EDWARDS25519-SHA512-TAI, RFC 9381).What the CAP specifies
Key separation. The CAP does not reuse the Ed25519 consensus node key
for randomness. Layer A derives a dedicated, domain-separated VRF key per
validator from its Ed25519 descriptor (
ecvrf_derive_key, RFC 9381 Sec2), andLayer B commits a dedicated group key as the epoch
authority_key-- apure 32-byte public commitment that
verify(epoch, C_s, P_s)checks against,never a reused individual node key. Node identity keys (StrKey formats,
tooling) are unchanged; they authenticate membership/VRF-key announcements and
DKG/roster membership only, not the randomness itself.
One pulse per externalized close. A canonical close commitment
C_sderived purely from canonical state: the externalized value bytes,
network_id,epoch_hash,ledgerSeq, andpreviousLedgerHash.C_sisitself the protected challenge -- the only signed/verified domain (dnFV6 /
dnFWo / dnFWi). The proof is an unforgeable authenticator for the exact
externalized bytes; the root is derived only after it verifies (proof != root).
A CONFIRM->EXTERNALIZE release boundary. A share is authorized only once
SCP reaches CONFIRM and the externalized value is locally fully validated
-- never at accepted-commit (
mCommitin PREPARE is not safe-to-act finality).Three labeled consumers of one root.
KDF(R_s, "PRNG")(Soroban PRNG),KDF(R_s, "APPLY")(apply order),KDF(R_s, "NOMINATION")(next-ledgernomination). No dual timing domain, no separate nomination path.
Layer A as evidence -- the
ECVRF-EDWARDS25519-SHA512-TAIper-revealaggregate (commit
s-2/ reveals-1/ finalize) with its canonicaltranscript, deterministic contributor set, threshold
t = floor(2Q/3)+1,honest fallback, sub-seed labels
0x02/0x03/0x04, and the all-offvrfCommits/vrfReveals/vrfBeaconNextXDR surface. It is retained to pinthe boundary it does not cross; the shipping protocol-randomness
wire format is the single Layer-B runtime-decodable form (decided by an
executable backward-decode conformance test before
Implemented).Adversarial conformance vectors
contents/cap-0089/ships the acceptance fixtures and an executable checkerthat pins every digest and asserts the structural properties:
check_vectors.py-- deterministic checker reproducing the fixture andasserting V1 (prior-ledger grinding is bounded -- each candidate pushed
individually through the candidate-independent canonical path, rooted and
evolved separately), V2 (single withholding + the
t = floor(2Q/3)+1threshold and LCL fallback branch), V3 (commit/reveal binding), V4
(cross-network replay -- the testnet reveal set is **rejected by the
acceptance path** under the mainnet transcript hash, not a byte-inequality
tautology), V5 (cross-slot replay -- the slot-
sset is **rejected by theacceptance path** under the
s+1transcript hash), V6 (purposeseparation), V7 (protocol verifier rejects wrong-NodeID /
wrong-transcriptHash / duplicate records), V8 (transcript mutation), and
the rule-5 proof width: every honest proof is a genuine 80-byte
RFC 9381-shaped
Gamma||c||s, so the acceptance path exercises the mandatedopaque vrfProof[80]wire width.fixture_vectors.json-- the pinned transcript / network-id / slot / beacon /sub-seed digests (generated byte-for-byte by
gen_vectors.py).xdr-diff.md-- the canonical XDR diff (same as embedded in the CAP).gen_vectors.py-- provenance generator for the fixture.layer_b_model.py-- the executable Layer B state machine (B1-B4, O1-O3, thecompositional one-future law
|H_n|=1, kill questions, S1-S3) with thepinned authoritative-history digests, one per per-event delivery sequence
(re-pinned in Rounds 6 and 8; final values
f3455fda.../9acde5f0.../8f8bc77b.../2a254ad5..., EXP_COMPf824647d...). It implements thesingle-pulse close commitment
C_sas the only protected challenge (nocaller class, no second preimage, dnFV6), the **CONFIRM/EXTERNALIZE release
boundary** as the boundary object -- the only producer of release
witnesses, with no caller-asserted transition bytes (this review) -- and the
proof/root split, the exact-externalized-value binding
(B2/dnFWo), the epoch binding (dnFVu), the injective length-prefixed epoch
preimage (dnFWC), prefix-stability (dnFWP), and a real O1 uniqueness test
(varying retry/alias/value/slot candidates, exactly one ROOT draw).
Round-4 hardening (addressing the reviewer's five new comments, plus the
suppressed V1/V4/V5 items):
verify is a genuine public-key check.
layer_b_model.pynow carries areal Schnorr-like threshold proof over a 256-bit safe-prime group
(
p = 0xffff...72ef, generatorG = 4, orderq = (p-1)/2), committed as apure 32-byte
authority_key.UniqueThresholdProof.verify(epoch, C_s, P_s)checks the discrete-log equation
G^s == R*Y^c (mod p); it does notregenerate the proof from public inputs, and it rejects arbitrary
64-byte / random values (the O2 test mints public-only and random candidates
and asserts they verify to nothing).
Availability boundary
t <= n - f. An epoch whose threshold exceeds thehonest-share count (
t > n - f) is rejected at construction, alongside theexisting Byzantine honest-intersection bound
2t-n > f(e.g. `n = 8,t = 7` is rejected rather than producing a stall).
lp-wire fix.
close_input/EpochDescriptor/roster encodings use fixed32-byte
hash32fields (no length prefix) matching the Core XDRHash[32]widths, with the length-prefix only on list/container boundaries -- the
Authority's split-input ambiguity is gone.
Round-5 hardening (commit
5b63f56, addressing the reviewer's ten newcomments: 2 High + 6 Medium + 2 Low):
Genuine Shamir threshold reconstruction.
recover_proofnowLagrange-interpolates the actual signing scalar
d = f(0)over GF(q) fromthe supplied share values -- the master seed is not read on the recovery
path, so possessing
tvalid shares is genuinely sufficient (and< tyields no proof). Subset/permutation tests recover the byte-identical proof.
Candidate-independent canonical root (one-future is real, Round 5 --
superseded in Round 6 by a proof-dependent canonical root, see below). The accepted
root is
H("Root" || authority_key || epoch_hash || C_s)-- independent ofthe proof bytes -- so two mathematically-valid same-message Schnorr proofs
(different nonces) verify to the same unique root and cannot split the
pulse; a new I4/O2 test mints two valid signatures and asserts identical
roots.
No caller-asserted externalize flag (High). `ConfirmExternalizeBoundary
.externalize` now takes only a quorum-cert of signed member CONFIRM
votes (each verified under the member's public subkey
K_i = G^{k_i}, keyseparation); everything else -- signature keys, byzantine state, availability,
the challenge itself -- is provenance-independent or forced by the epoch. The
fully_validatedboolean is gone;< tor unauthenticated votes arerejected.
Availability
fis preserved, not silently clamped. The default is thelargest value satisfying both bounds,
f = min(2t-n-1, n-t)(documentsn=8,t=7 => f=1, valid); a configuredfis kept and rejected fail-closed ifit violates
t <= n-f.No node-cached genuine-proof gate (Medium).
resolveauthenticates anyproof accepted by the pure
(epoch, C_s, P_s)verifier -- including one fromanother authority or loaded during catchup -- restoring the stated archival
contract; forged/public-only/random proofs still fail the public-key check.
Quorum <-> roster intersection at activation. The CAP now specifies that a
ratifying SCP quorum must intersect the randomness roster in at least one
honest member (
2t-n > f), evaluated at epoch activation, so externalizationtraffic always reconstructs the unique root.
V1 anti-grind is non-tautological. Each candidate is pushed individually
through the canonical and grounded paths with the loop variable genuinely
used.
Wire prose matches
hash32. The B1 close-input description now specifiesfixed-width Core
opaque Hash[32]for the four hash fields (nolp()length prefix), matching
close_inputexactly.VRFCommit.sig is stated as an Ed25519 signature under the validator's
existing NodeID key (the derived VRF key is reserved for the VRF proof),
consistent with the normative XDR and checker.
Round-6 hardening (commit
7492baa, addressing the reviewer's four newcomments: 2 High + 1 Medium + 1 Low):
**Honest holder vs. attacker surface split (High -- GROUP_SK is
primitive-internal).** A new
AttackerViewfacade is constructed with **onlypublic material** (authority key, roster, membership commitment, candidate
closes) and exposes no shares, seed,
_group_secret, or_spf_sign.Every negative-synthesis / pre-lock unavailability check (O2, S2, K2) now
drives through this attacker surface; the evidence asserted is precisely "an
attacker interface that cannot touch signing material cannot produce an
accepted proof or a pre-lock root." The
GROUP_SKmodule constant isdocumented as the primitive's internal key -- reachable only from the honest
ThresholdAuthoritysigning path, never serialized/committed/printed -- andthe CAP is explicit that the information-theoretic
< t-share secrecyitself is the candidate primitive's (BLS/VUF) guarantee, which the harness
documents but does not re-derive.
**
canonical_rootnow depends on the unpredictable canonical proof P_s(High -- anti-grinding).** The root is `H("Root" || authority_key || epoch_hash
|| C_s || canonical(P_s))
. Because the canonicalP_s` requires thereconstructed group secret at the CONFIRM/EXTERNALIZE boundary,
P_sisnot computable from public candidates pre-lock -- a proposer cannot grind
canonical_rootby editing unfinalized s-1 contents, since the root input isthe recovered proof, not the public challenge.
UniqueThresholdProof.verifypasses the canonical proof into
canonical_root; the oldcandidate-independent root (Round-5 line above) is superseded. A new I4/O2
test asserts the canonical proof is byte-unique (deterministic nonce), that a
substituted/forged 64-byte proof fails verification and yields
None, andthat the attacker's public-only root guess never equals the accepted root.
V1 per-candidate canonical boundary (Medium). The beacons were forged
over
H("Root"||...) = anchor-fixed derivation; the checker now routes eachcandidate
cindividually through two named, candidate-acceptingfunctions --
canonical(c)(maps any candidate through the committed-anchorseam -> pinned for every
c) andbound_to_candidate(c)(the malformedvariant that binds alpha to
c-> distinct, never pinned) -- so the loopvariable is genuinely consumed per candidate and a broken impl that rewarded
grinding is caught per candidate.
Key contract reconciled everywhere (Low). The prose now consistently
states that **
VRFCommit.sigis an Ed25519 signature undernodeID'sexisting consensus NodeID key** (over `"stellar-vrf/commit" | 0x01 |
network_id | slot | commitHash`), matching the normative XDR
(
{ NodeID nodeID; Signature signature; }) and the Layer A checker(
Ed25519PublicKey.from_public_bytes(nodeID)directly); the derived VRF keyis used only for the per-ledger VRF proof/
beta. NoKeyUpdateAnnouncement/ control-seal / NodeID-to-VRF mapping is needed onthe commit path. All three flagged locations (the Layer A bullet, and the
B2/B3 exposition) now agree.
Re-pinned B3/EXP_COMP authoritative-history digests to the model's
actual (verified) root-with-proof values: B3 =
f3455fda..., 9acde5f0..., 8f8bc77b..., 2a254ad5...,EXP_COMP = f824647d...--and the same values are now quoted in the CAP (Section "Test Cases"), so the
model, CAP, and PR body agree on one authoritative set (reconciled in
round 8, after the CONFIRM fix re-bound the epoch hash).
Round-7 hardening (commit
3b5612c, addressing the reviewer's **8 new comments(6 High + 2 Medium)** + the suppressed verifier-context note + the
copilot overview note). Every thread is resolved;
UNRESOLVED = 0.**Destination tags should be 64-bit #1 membership: authority key must be a genuine order-
qsubgroup element(High).**
EpochDescriptor.__init__now validates the authority/groupauthority_keycryptographically: it is hashed, mapped to a scalar, andrejected unless
1 < Y < pandY^(q) == 1 (mod p)-- soY = 1(whichlets anyone still satisfy
G^s = R*Y^c) and any non-residue are impossible atconstruction. New R2 tests assert the identity key, an out-of-group value, and
a non-residue all
raise ValueError, while a genuine order-qkey passes.The (previously nondeterministic) raw-hash test keys are replaced by genuine
order-
qsubgroup elements (group_pub_from_seed).Currency codes should allow more than 3 characters #2 one-future / alternate-valid-proof (High). The I4/O2 vectors now assert
the replacement SCP-relevant property that plain Schnorr cannot give you alone
but FROST gives you structurally: there is exactly one obtainable canonical
proof per close because a second valid proof is *impossible below the group
secret* -- every alternate candidate (a crafted 64-byte proof, cross-close
partials, the attacker's best public-only guess) is **rejected by
verify->None** (additive to the byte-unique-deterministic proof). The root remainsH("Root" || Y || epoch_hash || C_s || P_s), so one canonical proof => oneroot => one-future.
Drop transaction meta #3 long-lived Shamir share re-use (High). The threshold share is now
message-dependent FROST, not a fixed Shamir share: a deterministic
per-close nonce polynomial yields masking nonces
n_i(R = G^r), each share iss_i = (n_i + c*f(i)) mod qfor the per-message challengec = H(epoch_hash || C_s || R || Y),f(i)is committed at epoch viaFeldman
C_i = G^{f(i)}(so shares verify against committedcoefficients), and reconstruction gives
s = r + c*d-- the group secretdis never a standalone long-lived share and below-
tactors can neverderive it. The N3 test now asserts: honest signer outputs are idempotent
(same C_s -> same proof), differ across closes (
s1 != s2, so no frozenlong-lived signature an attacker can replay for a future commitment), and
partials from one close fail to verify against another close
(
verify_share/recover_proof->None).Time format: seconds after year 2000 vs unix timestamp #4 per-member CONFIRM auth keys are epoch-bound and committed (High).
Release-bound CONFIRM keys are now
K_i = G^{f(i)}(the Feldman publicshare, committed in the epoch), and the boundary's authentication keys are
derived per-epoch as
H("ConfirmSubKey" || epoch.membership_commitment || i),so unrelated rosters of the same size no longer share CONFIRM keys -- each
member is authenticated under exactly the key its own epoch's roster committed.
Merge ledger_index and ledger_hash params #5 numeric-
tvs. SCP quorum (High). The release edge is reframedhonestly: the
>= tdistinct rostered CONFIRM votes are the **randomness-reconstruction threshold** (enough shareholders genuinely reached CONFIRM to
produce shares), not a claim of SCP quorum topology. Which value
externalizes is Core's trusted local CONFIRM->EXTERNALIZE transition on the
real saved SCP envelopes / quorum-sets (FBA, not a numeric committee); the
model represents that trusted edge and does not claim a numeric-
tquorum certificate.
confirm_certcarries only>= tvalid member votes; theSCP quorum decision remains Core's.
Drop the concept of rippling in favor of long lived offers #6 V1 candidate boundary made non-tautological (Medium).
canonical(c)nolonger uses a discarded
_ = candidate. Each candidate is routed through agenuine domain-separated seam (`H("V1/canonical-boundary" || anchor ||
candidate)`) and the anti-grinding exclusion is asserted as a property -- any
broken impl that let the candidate bytes into alpha is caught per candidate by
bound_to_candidate, and the canonical set stays pinned only because thecandidate is legitimately excluded by the documented rule.
Suppressed: verification context is fully parameterized (Medium). The
acceptance verifier is now built from an explicit context
(network, slot, expected_proofs, commits). The resident verifier reads thelive committed-auth/expected-proof maps (so V10's self-signed clones are
honored), and V4/V5 cross-replay run a context-closed verifier for
mainnet / next-slot, so a testnet set rejects because the **transcript hash,
the commits, AND the proofs all mismatch a different network/slot** -- a
context rejection, not a transcript-hash-only tautology.
Streamline native stellard currency format #7 key-separation prose is now honest (Medium). Because PR #5409's
ecvrf_derive_keyexpands the seed with the standard (non-domain-separated)Ed25519 derivation, the prose no longer claims a genuinely independent,
domain-separated VRF keypair. It now documents a dedicated VRF key (a
domain-separated seed `SHA-256("stellar-vrf/v1/derive" | network_id |
ed25519_key)` fed to #5409), and states the honest guarantees: separate
use, a secret held only inside the VRF primitive, a
betacommitted as thepre-image-unlinkable
commitHash, and the NodeID<->VRF pairing authenticated bythe commit/reveal binding. An explicit NodeID->VRF-public-key field is noted as
a deliberate future extension, not claimed here.
tacticalnootoverview note: the reviewer overview "5 of 7 changedfiles ... 8 comments" is answered in-line: all 8 comments are resolved above and
the complete behavior is enforced by the model + Layer A checker (
ALL PASS),with native Core/Rust conformance as the release gate. The two files not
individually reviewed (
cap-0089.mdprose + the XDR diff) cascade into thechecked files' behavior and are covered by the same vectors.
Round-8 hardening (commit, addressing the core agent's 5 new High comments +
the re-raised V1 seam note). Every thread now resolved;
UNRESOLVED = 0.Streamline native stellard currency format #7 CONFIRM votes authenticate a genuinely member-held SECRET (High, es9rs).
The old per-member CONFIRM subkey was derived from the PUBLIC
membership_commitmentand index, so any observer could mint a valid vote forevery roster member and the
>= tcertificate authenticated nothing. The modelnow derives each member's signing key
k_ifrom the private group/DKGsecret (
SHA-256("NodeConfirmKey/v2" || H(secret) || i)), commits only thecorresponding public verification keys
K_i = G^{k_i}in the epoch (bound intoepoch.hash), and the boundary verifies a CONFIRM cert against exactly thosecommitted keys. An observer holding only public material cannot compute any
k_iand cannot forge a vote. The committed-key change re-bound the epoch hash,so the authoritative-history digests were re-pinned in both the model and
the CAP: B3 =
f3455fda.../9acde5f0.../8f8bc77b.../2a254ad5..., `EXP_COMP =f824647d...` (one authoritative set, matching the PR body).
Merge ledger_index and ledger_hash params #5 B3/EXP_COMP reconciled to ONE authoritative set (High, es9r-). The CAP
and a Round-6 PR-body line still quoted the pre-re-pin digests (
a12db5.../21bf42.../569a25.../34cde1...;EXP_COMP = ce3a8975...) that the model no longercomputes. The CAP "Test Cases" now quote the model's actual verified
digests, and the PR body is updated -- model, CAP, and PR body agree on one
set.
**Drop transaction meta #3 commit/reveal binds to a committed+authenticated VRF public key (High,
es9sa).**
VRFCommitnow carriesopaque vrfPublicKey[32](distinct from theNodeID), and
VRFCommit.sigis an Ed25519 signature under the NodeID over"stellar-vrf/commit" | 0x01 | network_id | slot | commitHash | vrfPublicKey--so the NodeID->VRF-public-key mapping is authenticated (a key derived from a
private seed is not otherwise publicly inferable), and a value author cannot
substitute a different VRF key for a contributor. The Layer A checker signs
/verifies over this preimage, the fixture carries
vrf_public_key, and a new V11Time format: seconds after year 2000 vs unix timestamp #4 quorum<->roster intersection is TWO conditions, not one (High, es9s1).
2*t - n > fproves only a roster-subset overlap; the CAP now adds a separate,topology-aware condition validated at activation against the local FBA
quorum-sets: every quorum
Qthat can ratify must intersect the roster in>= 1honest member (Q n honest-roster != empty), fail-closed otherwise. Both arefrozen by the committed epoch identity.
Destination tags should be 64-bit #1
Hdefined (High, es9tH).C_s, the inner value commitment, themembership commitment, the epoch rule-hash and the KDFs use `H(x) =
SHA-256(x)`, now stated explicitly in the alias block and referenced at the
C_sformula.V1 seam made genuinely non-tautological (re-raised).
canonical(c)nolonger computes-and-discards a seam that returns a closed function of
anchor.The candidate now flows through
canonical_alpha(c), which hashes it into aper-candidate acceptance tag, sweeps the tag out of
alpha, and returns thecommitted anchor; the beacon is derived through that boundary and asserted
to equal
pinned, while the per-candidate seam tags are asserted distinct(so a broken impl that ignored the candidate collapses them) and
bound_to_candidatestill catches a malformed alpha-binding impl.Round-15b (commit
eb73cb3) -- the seven follow-on threads from Copilot'sround-15 re-review, now closed:
Verified-only CONFIRM membership (3930442925).
externalize()records amember as released ONLY after its individual CONFIRM vote signature verifies;
an unauthenticated index in a certificate (e.g. five valid votes + one
invalid) is no longer released. Red-test: only verified indices are exposed.
Canonical qualified-dealer set via reliable broadcast (3930442958). The
distributed nonce-sharing paragraph now requires members to OPEN their
polynomials via reliable broadcast to EVERY peer (echo broadcast), yielding a
CANONICAL qualified-dealer set Q* on which all honest members agree -- a
Byzantine dealer can no longer selectively open to different peers to split
the aggregate R/root. One-future is preserved even under selective opening.
XDR-aware zero-in-place canonicalization (3930442982). The model's
provenance_free_valuenow ZEROES the signer region IN PLACE with full widthpreserved (per the CAP's XDR-aware rule) instead of naively stripping a
trailing field; a red-test proves differently-signed payloads canonicalize
byte-identically while a different payload differs.
**EpochDescriptor rejects invalid event_mapping at construction
(3930443019).** Validation is now in the epoch constructor / hash path, not a
test-only helper: an epoch whose committed event_mapping omits mandatory
APPLY or PRNG raises and can never become active.
Exact RFC 9381 VRF derivation (3930443056). The CAP no longer routes
through PR #5409's private
derive_key; it specifies `vrf_public_key =Point(base_point * SHA-512(seed)[:32])` -- the byte-exact form the primitive's
exported
vrf_generateproduces, so the native verifier accepts the registeredkey and the checker validates the fixture points decompress.
Signature zeroed to full 64-byte XDR width (3930443090). The canonical
zero form fixes
opaque Signature<64>at its full 64-byte width (lengthprefix 64 + 64 zero bytes) with the nodeID arm pinned -- so the re-encoded
ext arm length is byte-identical to the original, not a shorter empty opaque.
Closed-form NOMINATION fallback priority (3930443124). NOMINATION never
returns None: when opted out or UNKNOWN, it derives the closed-form
NominationFallback(epoch_hash, slot, C_s, V_s)priority -- a pure functionof committed canonical state with an exact byte formula and a conformance
red-test, so implementations share exactly one candidate ordering.
Digests / checks.
layer_b_model.pyALL PASS (83 checks),check_vectors.pyALL PASS (40 checks). Branch rebased onto current
master(behind_by = 0).UNRESOLVED = 0after replying to and resolving all twelve round-15 plusseven round-15b threads.
Review progress: Copilot rounds 16-19 (now closed,
UNRESOLVED = 0)All four most recent Copilot re-review rounds are fully resolved (replied and
resolved, every thread closed):
48878e2): six threads -- observer-independent CONFIRMmembership, canonical qualified-dealer set, XDR-aware zero-in-place
canonicalization, invalid
event_mappingrejection at epoch construction,genuine ECVRF
_clone_vrf_pubkeys, and bounded-liveness framed as an explicitdeployment precondition.
59b2762): six threads -- canonicalP, a recoverable andincludable committed polynomial, an accurate one-way key-separation statement, an
observer-independent NOMINATION branch, genuine ECVRF
_clone_vrf_pubkeys, andthe "activation" bounded-liveness relabelled as a committed deployment
precondition.
223aebb): five threads -- the nonce-commitment set agreedseparately and explicitly excluded from the
C_spreimage (no hash cycle),commit-time confidential share escrow, RFC 9381 rejection of the identity point in
the checker, an observer-independent nomination priority as a pure function of
canonical finality, and B1 realigned to the normative Event-identity procedure
(preserved V0
extarm, full-width zeroing of NodeID and the 64-byte signature).babec18): five threads --Pderived deterministically fromthe finalized close's existing authenticated CONFIRM envelopes (no new SCP phase,
no new XDR type), Feldman-verifiable commit-time shares (each confidential share
verifiable at first receipt, before qualification, against a pre-committed public
point), the dispute demoted to a self-contained, non-consensus-critical notice
(
Ris fixed at commit by the committed commitments and cannot be split), anexplicit in-protocol recovery path for roster-disjoint-finality liveness, and the
model STALLING finalized nomination until the canonical root is available so no
per-node priority fork can arise.
Executable evidence:
layer_b_model.pyALL PASS (88 checks) andcheck_vectors.pyALL PASS (40 checks). The branch is rebased onto currentmaster(behind_by = 0), mergeable, and all 280 review threads across everyround report
UNRESOLVED = 0; every thread carries a signed (Eslam) technicalresolution referencing the closing commit.