Skip to content

Add CAP-0089: VRF-Based Protocol Randomness and Fair Leader Selection - #2005

Open
EslaM-X wants to merge 53 commits into
stellar:masterfrom
EslaM-X:cap-0089-vrf-randomness
Open

Add CAP-0089: VRF-Based Protocol Randomness and Fair Leader Selection#2005
EslaM-X wants to merge 53 commits into
stellar:masterfrom
EslaM-X:cap-0089-vrf-randomness

Conversation

@EslaM-X

@EslaM-X EslaM-X commented Aug 30, 2026

Copy link
Copy Markdown

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-TAI per-reveal aggregate. It is kept as evidence

    and as a first-class primitive, not pitched as the shipped

    protocol-randomness wire format. The trailing StellarValue fields in the XDR

    diff 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

    RandomnessEpoch one-future construct. Each closed ledger has exactly one

    canonical close commitment `C_s = H("CloseLock" ||

    RandomnessCloseInputV1(network_id, epoch_hash, ledgerSeq, previousLedgerHash,

    H(externalized_value_bytes))), and that C_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 (one

    pulse 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), and KDF(R_s, "NOMINATION")

    (next-ledger nomination priority). There is **no dual R_pre/R_post timing

    model** and no separate nomination path.

Per the review, the authority boundary is the **CONFIRM -> EXTERNALIZE ->

valueExternalized** transition (never acceptedCommit / 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 with

    existing 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_PULSE outcome.

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), and

    Layer B commits a dedicated group key as the epoch authority_key -- a

    pure 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_s

    derived purely from canonical state: the externalized value bytes,

    network_id, epoch_hash, ledgerSeq, and previousLedgerHash. C_s is

    itself 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 (mCommit in 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-ledger

    nomination). No dual timing domain, no separate nomination path.

  • Layer A as evidence -- the ECVRF-EDWARDS25519-SHA512-TAI per-reveal

    aggregate (commit s-2 / reveal s-1 / finalize) with its canonical

    transcript, deterministic contributor set, threshold t = floor(2Q/3)+1,

    honest fallback, sub-seed labels 0x02/0x03/0x04, and the all-off

    vrfCommits/vrfReveals/vrfBeaconNext XDR surface. It is retained to pin

    the 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 checker

that pins every digest and asserts the structural properties:

  • check_vectors.py -- deterministic checker reproducing the fixture and

    asserting 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)+1

    threshold 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-s set is **rejected by the

    acceptance path** under the s+1 transcript hash), V6 (purpose

    separation), 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 mandated

    opaque 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, the

    compositional one-future law |H_n|=1, kill questions, S1-S3) with the

    pinned authoritative-history digests, one per per-event delivery sequence

    (re-pinned in Rounds 6 and 8; final values f3455fda.../9acde5f0.../

    8f8bc77b.../2a254ad5..., EXP_COMP f824647d...). It implements the

    single-pulse close commitment C_s as the only protected challenge (no

    caller 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.py now carries a

      real Schnorr-like threshold proof over a 256-bit safe-prime group

      (p = 0xffff...72ef, generator G = 4, order q = (p-1)/2), committed as a

      pure 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 not

      regenerate 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 the

      honest-share count (t > n - f) is rejected at construction, alongside the

      existing 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 fixed

    32-byte hash32 fields (no length prefix) matching the Core XDR Hash[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 new

    comments: 2 High + 6 Medium + 2 Low):

    • Genuine Shamir threshold reconstruction. recover_proof now

      Lagrange-interpolates the actual signing scalar d = f(0) over GF(q) from

      the supplied share values -- the master seed is not read on the recovery

      path, so possessing t valid shares is genuinely sufficient (and < t

      yields 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 of

      the 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}, key

      separation); everything else -- signature keys, byzantine state, availability,

      the challenge itself -- is provenance-independent or forced by the epoch. The

      fully_validated boolean is gone; < t or unauthenticated votes are

      rejected.

    • Availability f is preserved, not silently clamped. The default is the

      largest value satisfying both bounds, f = min(2t-n-1, n-t) (documents

      n=8,t=7 => f=1, valid); a configured f is kept and rejected fail-closed if

      it violates t <= n-f.

    • No node-cached genuine-proof gate (Medium). resolve authenticates any

      proof accepted by the pure (epoch, C_s, P_s) verifier -- including one from

      another 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 externalization

      traffic 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 specifies

      fixed-width Core opaque Hash[32] for the four hash fields (no lp()

      length prefix), matching close_input exactly.

    • 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 new

    comments: 2 High + 1 Medium + 1 Low):

    • **Honest holder vs. attacker surface split (High -- GROUP_SK is

      primitive-internal).** A new AttackerView facade is constructed with **only

      public 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_SK module constant is

      documented as the primitive's internal key -- reachable only from the honest

      ThresholdAuthority signing path, never serialized/committed/printed -- and

      the CAP is explicit that the information-theoretic < t-share secrecy

      itself is the candidate primitive's (BLS/VUF) guarantee, which the harness

      documents but does not re-derive.

    • **canonical_root now 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 canonical P_s` requires the

      reconstructed group secret at the CONFIRM/EXTERNALIZE boundary, P_s is

      not computable from public candidates pre-lock -- a proposer cannot grind

      canonical_root by editing unfinalized s-1 contents, since the root input is

      the recovered proof, not the public challenge. UniqueThresholdProof.verify

      passes the canonical proof into canonical_root; the old

      candidate-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, and

      that 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 each

      candidate c individually through two named, candidate-accepting

      functions -- canonical(c) (maps any candidate through the committed-anchor

      seam -> pinned for every c) and bound_to_candidate(c) (the malformed

      variant that binds alpha to c -> distinct, never pinned) -- so the loop

      variable 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.sig is an Ed25519 signature under nodeID's

      existing 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 key

      is used only for the per-ledger VRF proof/beta. No

      KeyUpdateAnnouncement / control-seal / NodeID-to-VRF mapping is needed on

      the 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-q subgroup element

      (High).** EpochDescriptor.__init__ now validates the authority/group

      authority_key cryptographically: it is hashed, mapped to a scalar, and

      rejected unless 1 < Y < p and Y^(q) == 1 (mod p) -- so Y = 1 (which

      lets anyone still satisfy G^s = R*Y^c) and any non-residue are impossible at

      construction. New R2 tests assert the identity key, an out-of-group value, and

      a non-residue all raise ValueError, while a genuine order-q key passes.

      The (previously nondeterministic) raw-hash test keys are replaced by genuine

      order-q subgroup 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 remains

      H("Root" || Y || epoch_hash || C_s || P_s), so one canonical proof => one

      root => 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 is

      s_i = (n_i + c*f(i)) mod q for the per-message challenge

      c = H(epoch_hash || C_s || R || Y), f(i) is committed at epoch via

      Feldman C_i = G^{f(i)} (so shares verify against committed

      coefficients), and reconstruction gives s = r + c*d -- the group secret d

      is never a standalone long-lived share and below-t actors can never

      derive it. The N3 test now asserts: honest signer outputs are idempotent

      (same C_s -> same proof), differ across closes (s1 != s2, so no frozen

      long-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 public

      share, 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-t vs. SCP quorum (High). The release edge is reframed

      honestly: the >= t distinct 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-t

      quorum certificate. confirm_cert carries only >= t valid member votes; the

      SCP 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) no

      longer uses a discarded _ = candidate. Each candidate is routed through a

      genuine 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 the

      candidate 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 the

      live 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_key expands 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 beta committed as the

      pre-image-unlinkable commitHash, and the NodeID<->VRF pairing authenticated by

      the commit/reveal binding. An explicit NodeID->VRF-public-key field is noted as

      a deliberate future extension, not claimed here.

    • tacticalnoot overview note: the reviewer overview "5 of 7 changed

      files ... 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.md prose + the XDR diff) cascade into the

      checked 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_commitment and index, so any observer could mint a valid vote for

    every roster member and the >= t certificate authenticated nothing. The model

    now derives each member's signing key k_i from the private group/DKG

    secret (SHA-256("NodeConfirmKey/v2" || H(secret) || i)), commits only the

    corresponding public verification keys K_i = G^{k_i} in the epoch (bound into

    epoch.hash), and the boundary verifies a CONFIRM cert against exactly those

    committed keys. An observer holding only public material cannot compute any

    k_i and 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 longer

    computes. 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).** VRFCommit now carries opaque vrfPublicKey[32] (distinct from the

    NodeID), and VRFCommit.sig is 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 V11

check rejects a sig covering a different `vrfPublicKey`.
  • Time format: seconds after year 2000 vs unix timestamp #4 quorum<->roster intersection is TWO conditions, not one (High, es9s1).

    2*t - n > f proves 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 Q that can ratify must intersect the roster in

    >= 1 honest member (Q n honest-roster != empty), fail-closed otherwise. Both are

    frozen by the committed epoch identity.

  • Destination tags should be 64-bit #1 H defined (High, es9tH). C_s, the inner value commitment, the

    membership 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_s formula.

  • V1 seam made genuinely non-tautological (re-raised). canonical(c) no

    longer computes-and-discards a seam that returns a closed function of anchor.

    The candidate now flows through canonical_alpha(c), which hashes it into a

    per-candidate acceptance tag, sweeps the tag out of alpha, and returns the

    committed 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_candidate still catches a malformed alpha-binding impl.

Round-15b (commit eb73cb3) -- the seven follow-on threads from Copilot's

round-15 re-review, now closed:

  • Verified-only CONFIRM membership (3930442925). externalize() records a

    member 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_value now ZEROES the signer region IN PLACE with full width

    preserved (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_generate produces, so the native verifier accepts the registered

    key 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 (length

    prefix 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 function

    of 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.py ALL PASS (83 checks), check_vectors.py

    ALL PASS (40 checks). Branch rebased onto current master (behind_by = 0).

  • UNRESOLVED = 0 after replying to and resolving all twelve round-15 plus

    seven 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):

  • Round-16 (commit 48878e2): six threads -- observer-independent CONFIRM
    membership, canonical qualified-dealer set, XDR-aware zero-in-place
    canonicalization, invalid event_mapping rejection at epoch construction,
    genuine ECVRF _clone_vrf_pub keys, and bounded-liveness framed as an explicit
    deployment precondition.
  • Round-17 (commit 59b2762): six threads -- canonical P, a recoverable and
    includable committed polynomial, an accurate one-way key-separation statement, an
    observer-independent NOMINATION branch, genuine ECVRF _clone_vrf_pub keys, and
    the "activation" bounded-liveness relabelled as a committed deployment
    precondition.
  • Round-18 (commit 223aebb): five threads -- the nonce-commitment set agreed
    separately and explicitly excluded from the C_s preimage (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 ext arm, full-width zeroing of NodeID and the 64-byte signature).
  • Round-19 (commit babec18): five threads -- P derived deterministically from
    the 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
    (R is fixed at commit by the committed commitments and cannot be split), an
    explicit 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.py ALL PASS (88 checks) and
check_vectors.py ALL PASS (40 checks). The branch is rebased onto current
master (behind_by = 0), mergeable, and all 280 review threads across every
round report UNRESOLVED = 0; every thread carries a signed (Eslam) technical
resolution referencing the closing commit.

Round-15 (commit `1ad0094`) -- the full twelve-thread Copilot re-review PLUS

tacticalnoot's Round-15 gate, now executable and commit-pinned:

  - **Consumer matrix is now executable, not prose (tacticalnoot, threads

    3929943643 / 3929943621).** The committed `event_mapping` is CONSUMED to gate

    every labelled consumer: `consumer_kdf(root, label, event_mapping)` returns a

    value only if the label is enabled, `None` otherwise. APPLY and PRNG are

    MANDATORY in every accepted epoch -- both genuinely require post-lock hidden

    entropy (an unstalled apply cannot proceed without a seed), so a committed

    mapping that omits either is REJECTED at activation (invalid epoch). NOMINATION

    is the single opt-outable consumer, and its one behavior during UNKNOWN is a

    normative, closed-form pure function of canonical committed state -- NOT a

    second root, NOT a new consensus/nomination authority -- so there is exactly

    one deployment and one way to order the next round's candidates in every case.

    The red-test splits into a valid NOMINATION-only opt-out plus a rejected

    APPLY/PRNG-omitting mapping.

  - **Bias-resistant Feldman nonce-sharing (3929898800).** Rewrote the distributed

    nonce-sharing paragraph to close two gaps the reviewer found: (i) a

    commit-then-open phase so the LAST contributor cannot sample its polynomial

    after seeing the others' aggregate R (no grinding window), and (ii) an explicit

    authenticated, verifiable private share-distribution step so each member can

    evaluate its own point on the ONE common nonce polynomial g and verify every

    received g_v(j) against the opened public coefficient commitments.

  - **VRFCommit XDR size (#: 3929898849).** The sig field is `opaque Signature<64>`,

    so its 4-byte XDR length prefix makes VRFCommit ~168 bytes (not 164); counting

    both vector framing words gives the honest worst case ~38.0 KiB (and the

    earlier-flagged ~37.1 KiB with just the signature prefix + framing).

  - **Root binding to committed `R` + scalar `s` (3929898690 / 3929943584).**

    `R_s = sha256(b"Root" || authority_key || epoch_hash || C_s || R || s)` with

    R = P_s[:32] pinned to the transcript-committed aggregate nonce -- the root is

    a pure function of canonical public state and cannot be recomputed from the

    round-1 commits before the close locks.

  - **publish_nonce requires a CONFIRM gate (3929898704).** Red-test added: a node

    cannot publish its round-1 nonce before its CONFIRM gate is authoritative.

  - **Round-2 share barrier (3929898725).** Red-test added: no member emits a

    round-2 share before `>= t` authenticated round-1 nonces for the same close.

  - **Per-member release (3929898761).** Red-test added: an unauthorized member is

    withheld; release bits are set only by the boundary's authenticated

    CONFIRM->EXTERNALIZE path.

  - **ECVRF keys now valid Edwards25519 points (3929898654).** The fixture's six

    `vrf_public_key` values are `from_public_bytes`-valid RFC 8032 points (distinct,

    != node_id) -- a deterministic Ed25519 derivation from

    sha512(node_id)[:32]`, generated by `gen_vectors.py` and checked by the checker's

    new `rule VRF-public-key` decompression check.

  - **V1 leak switch (3929898827).** check_vectors.py no longer flips V1 on/off;

    the prior-ledger grinding invariance is asserted unconditionally.

  - **Responsibilities corrected (tacticalnoot / 3929898783).** Core owns the

    protocol lock/release authority (the CONFIRM->EXTERNALIZE boundary); Rust

    supplies only the irreducible crypto primitive (proof verification + root

    derivation). The CAP responsibilities section and the "first authenticated key"

    wording were reconciled with the Layer-B epoch-frozen roster (no precommitment).

  - **PR head cleaned (tacticalnoot).** The encoding glitches (`???` where

    arrows/dashes belong) in the PR primary description are fixed -- the body is

    now ASCII-only and renders cleanly; the Cap layer-A/no-fallback mixing is

    de-emphasised via the layer-evidence framing.

  - **Digests / checks.** `layer_b_model.py` ALL PASS (80 checks), `check_vectors.py`

    ALL PASS (40 checks). Branch rebased onto current `master` (behind_by = 0).

  - **`UNRESOLVED = 0`** after replying to and resolving all twelve round-15 threads.



Round-14b (commit `3947c7d`) -- all five new Copilot threads (3 High, 2 Medium)

from the round-14 re-review, PLUs tacticalnoot's round-15 consumer matrix:

  - **High - authenticated per-member release (3921067249).**

    `share(i, cl)` no longer accepts a caller-supplied `per_member_released`

    set. Release state is now stored per `(member_index, cl_hash)` and populated

    EXCLUSIVELY by the boundary's authenticated CONFIRM->EXTERNALIZE path

    (`_authorize_member_release`, called from the boundary's release edge). A

    caller who only holds the authority object can no longer conjure per-member

    release bits; each member's carrier exists iff that member individually

    crossed its own boundary. Executable red-test: `share` has no release-set

    parameter, an unauthorized member is withheld, and the liveness enumeration

    drives the authenticated bits.

  - **High - non-malleable VUF root (3921067284).** The root is now pinned to

    the transcript-committed NONCE commitment `R = P_s[:32]`, NOT to the

    malleable scalar `s`: `root = H(Root || Y || epoch || C_s || R)`. Two valid

    proofs sharing the committed R (the only R the round-1 FROST broadcast

    commits) collapse to the SAME root, so alternate encodings/valid-derivations

    cannot split the pulse -- one-future holds of the RELATION, matching the

    production VUF/BLS uniqueness. Executable red-test: tampering only `s`

    leaves the root unchanged.

  - **High - distributed verifiable nonce-sharing (3921067299).** New CAP

    paragraph specifying the Feldman-based distributed construction: each member

    broadcasts its nonce-polynomial coefficient commitments; peers linearly form

    the aggregate coefficient commitments of one common degree-(t-1) nonce

    polynomial; each `N_j` is verified to be an evaluation of that common g; and

    `R = Product N_j^lambda_j(0) = G^g(0)` is then subset-independent. Two

    different valid t-subsets yield the same R/P_s/root (one-future), and no

    contributor can unilaterally vary R.

  - **Medium - canonical XDR encoding of V_s (3921067322).** New CAP paragraph

    with an exact, reproducible provenance-free projection: XDR-decode the

    StellarValue, zero the `lcValueSignature { nodeID, signature }` members while

    PRESERVING the ext-arm structure/length (so the same signed arm re-encodes

    identically), re-encode deterministically. Two values differing only in

    signer hash to the same V_s/C_s; a genuinely different payload (including a

    different ext arm) hashes differently. Implementations can now derive the

    identical V_s.

  - **High - SCP-quorum/roster overlap + permanent-UNKNOWN (3921067348).** New

    CAP subsection: the release mechanism does NOT require the externalizing SCP

    quorum itself to contain t roster members -- release is a roster-internal

    edge (any roster member that locally CONFIRMs the close releases for it); the

    activation condition is strengthened to commit SCP-feasibility (honest roster

    releases participate in ordinary finality); and UNKNOWN is bounded-by-finality

    (never permanent). Honest configs cannot produce permanent UNKNOWN; a

    malicious peer can only withhold its own draw.

  - **tacticalnoot round-15 - consumer matrix + executable opt-out.** New CAP

    "consumer-by-consumer requirement matrix": APPLY and PRNG genuinely require

    post-lock hidden entropy (one shared pulse is mandatory); NOMINATION keeps a

    deterministic fallback so an UNKNOWN close cannot stall the next round. The

    committed `event_mapping` is now CONSUMED by `consumer_kdf(root, label,

    event_mapping)` to gate each consumer -- an opted-out consumer releases None

    (executable liveness escape, not prose). Red-test verifies an APPLY-only

    opt-out epoch.

  - **Digests / checks.** `layer_b_model.py` ALL PASS (75 checks), `check_vectors.py`

    ALL PASS (39 checks). `UNRESOLVED = 0`.



Round-14 (commit `f805a50`): address all five new Copilot threads (2 High, 1



Medium, 1 Low, plus the 2 suppressed previously-missed notes) and tacticalnoot's



committed release-policy question. ASCII-only text; `UNRESOLVED = 0`.



  - **High - duplicate CONFIRM keys are rejected (ew-OgM).** A single verification



    key K_i occupying multiple roster indices would let one signature be copied



    into t CONFIRM entries (externalize counts indices; the CONFIRM vote has no



    member_index binding). Rejected at epoch construction so each counted index is



    a DISTINCT signer. Executable red-test: R2 duplicate-CONFIRM rejects.



  - **High - committed Feldman polynomial consistency (ew-Ogf).** Prove the C_i



    are the evaluations of ONE degree-(t-1) polynomial whose constant commitment



    equals `authority_key`, via group-level Lagrange interpolation over the PUBLIC



    committed points (constant == authority_key, and every remaining point lies on



    the same curve). An off-curve set that would reconstruct a proof failing under



    the committed group key (permanent apply stall) is rejected at activation.



    Executable red-test: R2 consistency rejects/accepts.



  - **High - aggregate nonce (ew-Ogu).** Document the WEIGHTED form



    `R = Product N_j^lambda_j(0)` in the CAP, matching the model's exponent-



    Lagrange computation, so implementations can combine model-produced shares



    (the unweighted product would yield a different R/challenge). Fixed in both



    the two-round section and the model cross-reference.



  - **Medium - genuinely independent per-member release (ew-Og4).**



    `share(i, cl, per_member_released=E)` now gates each member's carrier behind



    ITS OWN release bit, and `disjoint_externalized` is CONSUMED (not dead). The



    liveness red test enumerates every per-member subset: recover == canonical



    P_s iff |E| >= t, None iff |E| < t, and the roster-disjoint SCP finality case



    is genuinely exercised.



  - **Low - release timing vs two-round (ew-OjN).** Reconcile the



    CONFIRM->EXTERNALIZE release hook with the two-round construction: round-1



    nonces are collected in the CONFIRM window (in-band), the round-2 share is



    generated once >= t round-1 commitments for the same close are authenticated,



    and rides the post-EXTERNALIZE fan-out (not the very first envelope).



  - **Suppressed - check_vectors ascending/unique NodeIDs + production transcript



    route.** The fixture's contributor list is now validated strictly NodeID-



    ascending and unique BEFORE the dict comprehension (no silent duplicate



    collapse); an out-of-order V7 case and a commit-set identity check are added.



    `canonical()` was replaced by ONE configurable production-like derivation



    `production_derive(c, leak)` whose ACCEPTED OUTPUT is asserted (pinned for



    leak=false, candidate-dependent for leak=true), closing the prior gap.



  - **tacticalnoot - committed Core release policy + UNKNOWN adjudication.**



    New CAP subsection: `SCP-finalized + randomness-UNKNOWN` is an ACCEPTED,



    TRANSIENT state, adjudicated by a committed access policy. Randomness releases



    only when >= t roster members INDEPENDENTLY release for the same C_s (the



    release policy is committed in epoch.hash); UNKNOWN is a first-class output



    meaning WITHHOLD -- never a fabricated value, never a permanent stall; and



    `SCP-finalized => eventually Roster-Release` holds under the same honest-



    majority assumptions as SCP liveness, using only the committed



    {roster, t, f, n} with ZERO observer-local qsets and NO new global authority.



    This is the honest, non-circular answer to the round-13 shortfall (the CAP no



    longer claims every SCP-finalizing view forces >= t releases -- it guarantees



    the release edge COMPLETES, and defines UNKNOWN as the bounded transient).



  - **Digests / checks.** `layer_b_model.py` ALL PASS (72 checks, includes the 2



    new High red-tests and the per-member-gated liveness), `check_vectors.py` ALL



    PASS (39 checks), `UNRESOLVED = 0`.







Round-13 (commit `eec17a3`): correct the liveness framing and de-circularize the





red test. Addresses the core agent's **1 High, 1 Medium** new threads (the





round-12 liveness work over-reached and I pushed a corrected, honest version).





ASCII-only text; `UNRESOLVED = 0`.





  - **The randomness roster is NOT an SCP quorum (High, ew-_ro).** The round-12





    phrase "the roster IS the finality quorum" was unsupported: the epoch commits





    only a flat roster + numeric threshold `t`, with no SCP quorum-set topology,





    and it contradicted the S1-S3 object contract and the explicit "externalization





    threshold ... not by a global count" note (`t` is not an FBA/scp rule). Core's





    real FBA quorum sets can externalize a ledger however their topology decides.





    The CAP now states this plainly and **ADDS a normative Core release rule**





    layered after SCP: shares for a VRF close are released only through the





    randomness roster's own `>= t` per-member CONFIRM boundary. SCP ledger finality





    alone does NOT release randomness -- a roster-disjoint SCP quorum can externalize





    a ledger, and that close then stays `UNKNOWN` (never assigned a value) until the





    roster release edge crosses `t`. The theorem is restated precisely:





    `Roster-Release(C_s) => randomness-reconstructible`, NOT `SCP-finalizable => ...`.





  - **Non-circular liveness red test (Medium, ew-_sC).** The round-12 test assumed





    the theorem (it defined every `|E| >= t` roster subset as finalizing and pulled





    every share from one globally-released authority), so the roster-disjoint





    case was excluded by construction. The model now gives **each roster member





    its own independent CONFIRM->EXTERNALIZE release bit** and treats SCP ledger





    finality and per-member release as two independent dimensions: (1) a





    roster-disjoint SCP externalization with fewer than `t` members individually





    released toward `C_s` yields **no** proof -- genuinely exercised and shown to





    stall `UNKNOWN`, which is exactly why the CAP *adds* the release rule rather





    than trusting SCP finality; (2) once `>= t` members have independently crossed





    for the same `C_s`, their individually-released shares reconstruct the unique





    `P_s`/`R_s`. Exhaustive enumeration over every per-member release subset.





  - **Digests / checks unchanged (model pins stable).** `layer_b_model.py` ALL PASS





    (70 checks, includes the revised Liveness checks), `check_vectors.py` ALL PASS,





    `UNRESOLVED = 0`.











Round-12 (commit `e1ef7ab`): the executable Liveness theorem + two-round FROST +







committed finality binding, addressing the core agent's **1 High, 1 Medium, 1 Low**







new threads and the tactical-noot **SCP-finalizable => randomness-







reconstructible** gate. ASCII-only text; `UNRESOLVED = 0`.







  - **Liveness theorem: SCP-finalizable => randomness-reconstructible







    (tacticalnoot).** The model gains the requested smallest red test: enumerate







    EVERY per-member local release bit (every subset `E` of roster indices that







    have crossed their own CONFIRM->EXTERNALIZE for the same `C_s`), enumerate







    every supported SCP-finalizing view, and require EVERY view with `|E| >= t`







    to force `>= t` locally-authorized shares that reconstruct the UNIQUE `P_s`/







    root, while no `|E| < t` view yields any proof. The proof idea: the randomness







    roster IS the committed finality quorum -- the boundary's `externalize`







    accepts a close certificate only with `>= t` DISTINCT VALID rostered CONFIRM







    votes, so a roster-disjoint quorum cannot finalize a VRF close at all. Finality







    and reconstruction are therefore ONE event, not two authorities, and the







    derivation needs ZERO per-node quorum-set reads (everything is committed in







    `epoch.hash`). Model ALL PASS 70 checks; 2 new checks prove the exhaustive







    enumeration.







  - **Activation: the roster is the only constitutional quorum (Medium, ew-xNs).**







    The earlier "constitutional quorum-set-family" wording is removed because it







    was not a committed input. The activation predicate now consults ONLY the







    randomness roster, which is already committed as `membership_commitment` in







    `epoch.hash`; `|Q intersect roster| > f` therefore reduces to a pure function







    of `epoch.hash` (folding in round-11's ew-VYj) with no external topology







    input and no per-node quorum-set read. Nodes evaluate the identical predicate







    everywhere.







  - **Two-round FROST ordering (High, ew-xNE).** The CAP now specifies the







    explicit ROUND-1 authenticated nonce-commitment phase BEFORE any partial







    (round-2) fan-out: each selected member first broadcasts its committed







    `N_i = G^{n_i}`; the aggregate nonce `R` and challenge `c = H(R, y, m)` are







    publicly fixed from the committed round-1 set (>= t nonces); only then is







    `share = s_i(32) || N_i(32) || C_i(32)` emitted, with `s_i = n_i + c*f(i)`.







    A partial whose `N_i` is not in the authenticated round-1 set is rejected,







    matching the model's `publish_nonce` / `round1_nonces` / bound-carrier







    discipline -- the prose and the executable model now agree on the wire/round







    format.







  - **CAP authoritative-history digests repinned (Low, ew-xOG).** The four







    registered B3 literals in the CAP were stale vs the executable model; they now







    read `0dbc6fcbaeae...`, `34bf0c36dabe...`, `143f0f27e0af...`,







    `586c1fc57865...` (one per delivery sequence; `EXP_COMP = bc18abcf7ca3...`),







    so an independent implementation is compared against the current values.







  - **Digests / checks:** `layer_b_model.py` ALL PASS (70), `check_vectors.py`







    ALL PASS, `UNRESOLVED = 0`.















Round-11 hardening (commit `170586c`; addressing the core agent's **4 new High /









3 Medium** threads; ASCII-only text so the published anchor markers are not









corrupted). **All 7 new threads addressed; `UNRESOLVED = 0` since round-10.** One









round-10 decision is deliberately revised: the topology-aware quorum-overlap









condition is RESTORED, but as a committed (non-per-node) invariant rather than the









removed per-node runtime check.









  - **confirm_pub / feldman_pub committed in ROSTER INDEX ORDER (High,









    evw-VYA).** `epoch.hash` now commits these key vectors in roster index order as









    `u32(len) || concat(hash32(k))`, not as sorted sets, so permuting either vector









    changes the epoch identity. The model adds an R2b conformance check: an









    otherwise-identical epoch with only the first two positions of `confirm_pub`









    and `feldman_pub` swapped yields a DIFFERENT `epoch.hash` (same









    membership commitment) -- one epoch can no longer carry two incompatible









    per-index meanings across nodes.









  - **Two-round FROST: authenticated signer/nonce-commitment set (High, evw-VYO).









    ** The peer-verifier path no longer injects every honest nonce commitment by









    hand. A new authenticated round-1 registry (`publish_nonce(member_index, cl)`









    broadcast + `round1_nonces(cl)`) holds each member's committed nonce









    `N_i`; `share()` cross-checks the carrier's `N_i` against the committed set,









    and `verify_share`/`recover_proof` build the true aggregate `R` from the









    committed round-1 nonces (>= t required) instead of `R = N_i`. A fresh peer









    given only the 96-byte carriers now accepts valid shares; the peer-verifier









    test builds `r1` via the authenticated broadcast, and a carrier whose nonce is









    missing or not the committed one is rejected.









  - **Topology-aware quorum-overlap activation RESTORED (High, evw-VYj; revises









    round-10 evw-B).** `t <= n - f` only proves enough honest roster members









    EXIST; it does not prove any RATIFYING set reaches local CONFIRM. If a









    constitutional quorum `Q` were roster-disjoint, that quorum could externalize









    without any roster member releasing a share, so `C_s` is never produced and









    apply stays `UNKNOWN` forever. The activation contract now requires









    `|Q intersect roster| > f` for every constitutional quorum `Q` as a COMMITTED









    invariant over the fixed constitutional quorum-set-family (evaluated once at









    epoch activation, not a per-node quorum-set read), so every ratifying subset









    holds more than `f` roster members and the share/confirm flow never deadlocks.









  - **XDR-aware provenance-free canonical value hash (High, evw-VY7 + Medium,









    evw-VZa).** A serialized `StellarValue` is NOT canonicalized by deleting its









    final 32 bytes (a signed V0 arm ends with a `LedgerCloseValueSignature` of 36-









    byte NodeID + length-prefixed Signature<64>, and BASIC arms have none). The









    CAP now specifies, and the model implements, ONE XDR-aware rule: a close









    supplied as a 32-byte payload hash hashes AS-IS (`H(v)`); otherwise the









    canonical provenance-free value (`provenance_free_value`, an explicit









    V0-arm `lcValueSignature`-member drop / re-encode) is hashed with the









    `ProvenanceExcludedValue/v1` prefix. This removes the old 32-byte-branch









    prefix so close-lock bytes match the normative formula, and B3 / EXP_COMP are









    REPINNED to the resulting digest set (model ALL PASS, check_vectors ALL PASS).









  - **close_input field order aligned with the executable model (High, evw-VZM).









    ** The normative wire order is now stated exactly as the tuple and code emit









    it: `network_id, epoch_hash, ledgerSeq, previousLedgerHash, V_s` (ledgerSeq









    sits between epoch_hash and previousLedgerHash, not after all four hashes), so









    an independent implementation following the prose derives the same `C_s`.









  - **Fixed-length share carrier (Medium, evw-VZj).** `RandomnessShareV1.share` is









    now `opaque share[96]` (fixed-length, no length prefix, admits ONLY an exact









    96-byte payload), so malformed, truncated, or zero-length shares are not valid









    XDR encodings and are rejected at the wire boundary.









  - **Digests re-pinned.** B3 = `0dbc6fcbaeae.../ '34bf0c36dabe.../ '143f0f27e0af.../









    `586c1fc57865...`; `EXP_COMP = bc18abcf7ca3...` (model `ALL PASS` 68 checks,









    `check_vectors` `ALL PASS`).



















Round-10 hardening (commit `52fc470`; addressing the core agent's **6 new High /











4 Medium / 1 Low** threads; re-pins so the CAP, the standalone XDR diff, and the











executable model agree). **All 11 new threads addressed; `UNRESOLVED = 0`.**











  - **Per-member share carrier widened to the executable 96-byte FROST partial











    (High, evw9q).** `RandomnessShareV1.share` is now `opaque share<96>` -- the











    strict `(s_i || N_i || C_i)` triple the model's `verify_share`/`recover_proof`











    actually require (three `opaque[32]`), so the normative transport shape and











    the executable model are interoperable; the CAP documents the exact byte











    layout and that reconstruction needs all three fields.











  - **NodeID-to-VRF-key mapping PRECOMMITTED before the per-slot target (High,











    evw9y).** Authenticating `vrfPublicKey` inside the same per-slot `VRFCommit`











    alone still let a validator grind VRF keypairs after `T_b(s)` was known and











    sign only the favorable pair. The CAP now commits the NodeID-to-VRF











    verification key **before** `T_b(s)` exists: Layer B pins it in the frozen











    `RandomnessEpoch` roster (fixed before the activation window), and Layer A











    treats the first authenticated key as committed and rejects a different key











    for the same node in a contributing window -- so the derived `beta` is











    unsteerable.











  - **Consensus-deterministic activation on committed (n, t, f) only (High,











    evw-B).** The activation predicate no longer reads a validator's local FBA











    quorum-set view (which could make nodes accept/reject the same epoch











    differently). It is now a pure cardinality function of the epoch-committed











    `(n, t, f)` read from `epoch.hash` alone: roster uniqueness `2*t-n > f` plus











    reconstruction availability `t <= n-f`; the old topology-aware











    `|Q ^ roster| > f` runtime check is removed from the consensus path.











  - **Availability bound `t <= n-f` guarantees >= t available valid shares











    (Medium, evw-- / evw-B).** With `f` Byzantine members withholding and no











    fallback key, the remaining `n-f` honest members must alone supply the `t`











    shares; a `(n, t, f)` violating this is rejected at construction, so a











    disjoint roster cannot stall apply indefinitely.











  - **canonical_value_hash: provenance-excluded value commitment (High, evw_K).**











    New `canonical_value_hash(value_bytes)` strips a trailing 32-byte











    `lcValueSignature` signer before hashing, so semantically-identical closes











    with different signing NodeIDs hash to the **same** `V_s` and the same `C_s`











    (one-use/one-event); used at every B1 site. The model proves C_s is











    signer-independent (S1 test).











  - **Per-member Feldman `C_i` committed in the epoch (High, evw9Z).** The model











    commits each member's `C_i = G^{f(i)}` in `EpochDescriptor.feldman_pub`











    (validated non-identity order-q), binds it into `epoch.hash`, and











    `verify_share` rejects any carrier whose `C_i != epoch.feldman_pub[i]` --











    closing forged/identity-carrier poison, with an R2 rogue-seed + forged-carrier











    conformance test.











  - **Live commit reverse-map removed; reveal bound by nid (High, evw9B).**











    `check_vectors.accept_reveal` no longer builds a commitHash-to-nid reverse











    map (which could overwrite on colliding commits); it looks the commitment up











    by `nid`, as `make_verifier` already does.











  - **Provenance-independent archival resolution (High, evw-0).** New











    `resolve_archival(epoch, cl, proof)` splits live release admission from











    archival resolution, so a fresh catchup node holding only `{epoch, C_s, P_s}`











    resolves the same root (K3 test).











  - **V1 seam routed through shared native `native_alpha` (High, evw-P).** The











    pinned/grounded canonical derivation now go through one shared native alpha











    function (`bind=False` authorized / `bind=True` broken), so the candidate











    genuinely traverses the implementation's alpha derivation instead of being











    true-by-construction.











  - **Reject empty/reversed activation window (Medium, evw-k).**











    `EpochDescriptor.__init__` raises on `activation >= retirement`.











  - **XDR-diff reconciled to the embedded CAP (Low, evw_T).** The standalone











    `xdr-diff.md` first hunk is regenerated to exactly match the embedded CAP











    diff (`+28,40`, identical comment text), so one authoritative surface is











    clear.











  - **Digests re-pinned to one canonical set.** B3 = `21426ca05e8e.../











    `96b916781f84.../`13b31a395292.../`1a6f142fabd9...`; `EXP_COMP =











    2c2c1c516688...` (model `ALL PASS` 64 checks, `check_vectors` `ALL PASS`).























Round-9 hardening (commit `b7529fa`, addressing the core agent's **5 new High /











2 Medium / 7 Low** comments). **All 14 new threads addressed; the objective is











`UNRESOLVED = 0` again.**











  - **#1 committed CONFIRM keys validated as non-identity order-q elements (High,











    evMR-).** The epoch now rejects, at construction, any committed per-member











    CONFIRM verification key `K_i` that is the identity (`K_i = 1`), out-of-group











    (`>= p`), or a non-residue (`K_i^q != 1`). A `K_i = 1` commitment would have











    let an observer choose `R = G^s` so that `K_i^c = 1` and forge a member's











    CONFIRM vote; the same non-identity/order-q gate used for `authority_key` is











    now applied to every `confirm_pub` element, with a dedicated R2 model check.











  - **#2 FROST make/recover is now authentically distributed (High, evMSO).** The











    FROST nonce polynomial was derived from the authority's master secret, so











    `verify_share`/`recover_proof` could not run on ordinary peers. The share











    carrier now transports the member's **public nonce commitment** `N_i = G^{n_i}`











    and **public Feldman commitment** `C_i = G^{f(i)}`; the aggregate nonce











    commitment `R` and challenge `c` are derived **publicly** from those











    commitments (Lagrange in the exponent), and `verify_share`/`recover_proof`











    read **only** public carrier data + the committed epoch surface GCo **no master











    seed, no share, no nonce**. Two conformance checks prove it: a sham-seed











    "peer-verifier" authority reconstructs the byte-identical canonical `P_s`











    from the honest carriers, and forged carriers (`bad C_i` / `bad N_i` / forged











    `s_i`) are rejected without corrupting the accepted set. The final `P_s` is











    mathematically unchanged, so the B3/EXP_COMP pins are **not** re-pinned.











  - **#3/#5 signing & commit preimage now include `vrfPublicKey` everywhere (High,











    evMSb/evMSz).** The CAP's commit-set and signing-prose both described the











    Ed25519 message as `"stellar-vrf/commit" | 0x01 | network_id | slot |











    commitHash`; both sites now include `| vrfPublicKey`, matching the











    `VRFCommit` XDR and the Layer A checker/gen (spec parity). The stale











    preimage comments in `check_vectors.py` and `gen_vectors.py` were refreshed











    too.











  - **#4 embedded XDR-diff hunk line counts corrected (High, evMSp).** The CAP's











    `VRFCommit`/`VRFReveal` `mddiffcheck` hunk header claimed `+28,32` while the











    hunk actually adds 34 new lines (6 context = 40); the header now reads











    `+28,40`, and both embedded hunks were audited (the `StellarValue` hunk is











    `+94,21` = correct).











  - **#6 Layer A cross-context accepter fixed (Medium, evMTA).** `make_verifier`'s











    inner accepter compared a **bytes** commit hash against a **hex** reveal hash











    (always failing) and did a 2-value unpack of a 3-tuple commit record; it now











    compares everything in **bytes** and unpacks all three fields, so the V4











    cross-network and V5 cross-slot tests genuinely exercise rejection under the











    wrong verification context rather than passing for the wrong reason.











  - **#7 archival replay preserves the full descriptor (Medium, evMTF).** The











    K3 replay test now reconstructs `archival_epoch` with the epoch's explicit











    `scheme`, `verifier_rule`, `byzantine_bound`, and `confirm_pub` instead of











    silently re-deriving defaults, so the year-later catchup re-derives the exact











    archived epoch identity.











  - **#8/#9/#10/#11 Low.** V11 overview + gen source-of-truth/preimage comments











    now include `vrfPublicKey`; the `canonical_root`/`_spf_sign` comments no











    longer claim "the root does not depend on the proof" (it hashes `P_s`); and











    `xdr-diff.md`'s `VRFCommit` now carries `opaque vrfPublicKey[32]` with the











    matching preimage, so the standalone XDR copy matches the CAP.











  - **#12/#13/#14 Low.** Per-commit footprint updated to **164 B** and the capped











    worst-case aggregate to **~36.8 KiB** (256 B/contributor earlier omitted the











    VRF key); the key-separation prose no longer claims bidirectional isolation











    (the derived VRF key is recovered from a consensus-Ed25519 compromise); and











    the quorum/roster honesty invariant is restated as the **objective cardinality











    condition** `2*t - n > f` plus `|Q Ge~ roster| > f`, not a runtime judgment of











    which members are honest.























Model `layer_b_model.py` **ALL PASS** (64) and Layer A `check_vectors.py`











**ALL PASS** (37); `gen_vectors.py` reproduces `fixture_vectors.json`











byte-for-byte; B3/EXP_COMP pins unchanged. Native Core/Rust conformance remains











the release gate.











The vectors complement the RFC 9381 primitive vectors already in PR #5409; full











cryptographic proof/verify is exercised by the #5409 harness, while the protocol











layer pins the close-lock, challenge, ordering, binding, threshold, and authority











rules. The external harnesses cross-check the boundary but **native Core/Rust











conformance is the release gate** -- they cannot define protocol truth.























## Scope discipline























Per review, this CAP scopes the protocol **design** (single-pulse close-lock,











CONFIRM/EXTERNALIZE boundary, labeled consumer derivation, XDR-as-evidence),











the **acceptance vectors**, and the **single Layer B deployment path** (with











the protocol-deployment fork formally separating *deployment* from *evidence*).











The core wiring is a separate follow-up increment on top of #5409. Status is











**Draft**; Protocol version is **TBD**.























Author: **EslaM-X** (independent contributor). Consulted: Nicolas Barry











<@MonsieurNicolas>, Tamir Sen <@tamirms>, Bartek Nowotarski <@bartekn>,











Leigh McCulloch <@leighmcculloch>, Phil Meng <@phil-stellar>.






## Review progress: rounds 20-21 (final audit commit 6119299)

Two further full rounds of review (12 threads) are resolved and verified at
zero unresolved. This revision contributes the consolidated **Security,
Correctness and Conformance Evidence** package, added to the CAP as an annex
("Security, Correctness and Conformance Evidence (Review Package)") so that a
reviewer can answer in minutes *what is proved, what is assumed, what is
committed, and what remains*:

- **Isolated cross-context proof-binding vectors (V4/V5).** The earlier
  cross-network/cross-slot tests rejected at the commit-signature scope and
  never exercised proof binding. Each target context (mainnet-s, testnet-s+1)
  is now built fully self-consistent -- `VRFCommit.sig` re-signed under the
  target network/slot 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, failing at the PROOF
  gate and never at the signature gate.
- **14 precise invariants (I1-I14)** (canonicality, subset invariance,
  below-threshold failure, context binding, no signer-selected randomness,
  withholding, replay resistance, roster binding, finality separation,
  consumer determinism, pre-lock unavailability, canonical event identity,
  fail-closed parsing, compositionality), each mapped to its real executable
  check label.
- **Property x evidence matrix** (13 properties), **explicit threat model**
  (assumptions A1-A6, adversary may/may-not), and a **16-row attack catalog**
  with fail-closed mechanism and test evidence per attack.
- **Runtime-decidable state machine** with failure/disambiguation paths
  (`REJECTED | UNKNOWN | ROOT(R)`, no `NO_PULSE`, no timeout-derived state)
  and a **why-this-mechanism** table so every cryptographic component is
  re-validatable against the threat it answers.
- **Resource/cost analysis** at canonical XDR wire widths, and a **reviewer
  response ledger** mapping the 17 review rounds to root-cause -> resolution ->
  regression records.

Reproduce the evidence deterministically (no live network):
- `python contents/cap-0089/layer_b_model.py` -> 89 PASS
- `python contents/cap-0089/check_vectors.py` -> 44 PASS

**Design freeze.** The protocol surface is frozen at this revision; no new
mechanism/state/consumer/fallback is added except in response to a reviewer
showing the existing surface insufficient. The remaining work is conformance
and review, not architectural exploration: a second independent native
Core/Rust implementation cross-checked against the same vectors, an
independent cryptographic review of the construction, and the measured
acceptance benchmarks listed under the CAP's Exit Criteria.

## Review progress: rounds 22-30 (final audit commit 53fc4d7)

Rounds 22-30 are resolved and verified at zero unresolved threads
(`verify_all_pages.py`: TOTAL 351, `UNRESOLVED = 0`). Cumulative highlights:

- **Rounds 22-27.** UNKNOWN-stall semantics for missing proofs (bounded, never
  substitute-entropy); NOMINATION as the single optional consumer with an
  observer-independent closed-form fallback; distributed-FROST peer
  verifiability from public round-1 carriers only, forged/misindexed carrier
  rejection, and sign-once durable locks (no equivocation, even across crash);
  the roster-threshold per-member release rule; and the committed SCP qset
  **topology contract** (R26/R27) answering the 0xEmpty global-n/f/t vs
  local-quorum-slice review -- the epoch commits the exact flat qset graph it
  relies on, a relevant change rolls the epoch, and activation rejects any
  graph whose smallest finalizing quorum leaves fewer than `t` intact releasers.
- **Round 28** (foQhP/foQha/foQhh/foQhl/foQhq/foQhv). Secret-seeded AVSS
  dealer-authentication nonce (a message-only nonce provably leaks the dealer's
  decryption key `e_d`); the verifiable-encryption claim scoped to the public
  masked-point arithmetics with an honest-dealer reconstructibility bound;
  registry presence/absence as a field of the CLOSED value (construction blocks
  on the unique proof); a C1 min-quorum activation gate over actual finalizing
  quorums; strictly-increasing canonical registry decode; and the primitive
  dependency's status corrected (closed-unmerged, core #4388).
- **Round 29** (foqq9/foqrG/foqqv/foqqj/foqq4/fos67/fos6s/fpkEM/fpkES/fpkED/
  fpkD5). C1 sharpened to *every finalizing quorum leaves `|Q n Intact(B)| >= t`
  intact releasers*, committed default flat qset = `(t + f, all n)` --
  `(10,7,3,7)`, 4-of-8 and 5-of-8-with-Byzantine rejected at activation; AVSS
  per-close nonce/share availability is per-member (a Byzantine nonce dealer is
  skipped, others served, fail only below `t`); the `Q*` SIZE claim is abandoned
  (`|Q*| >= n - 2f`, never `n - f`) and replaced by the DISTRIBUTION-CLOSE gate;
  the Verifier binds the round-1-committed aggregate nonce (`committed_R`),
  rejecting a Schnorr-VALID wrong-R proof (one close, one root); registry
  presence as a field of the slot's own balloted value XDR (absent carrier is a
  different ballot value, SCP agreement binds the decision); alpha-isolated
  cross-context negatives (V4/V5) and genuine RFC 9381 ECVRF acceptance vectors
  (V6); committed-graph AUTHORITY deployment contract (fpkED) and its topology
  None-path fix (fpkD5).
- **Round 30** (fwY5j/fwY5s/fwY5z). The DISTRIBUTION-CLOSE gate became a
  **PUBLIC authenticated-certificate count**: the distribution CLOSES iff at
  least `t + f` DISTINCT roster members hold publicly verified **material-OK
  certificates** -- deterministic Schnorr signatures under each member's
  epoch-committed `K_i`, issued exactly when that member's OWN envelope+nonce
  Feldman recovery succeeded (a decision a node makes locally and everyone
  verifies publicly; NO honesty oracle, NO decryption). Soundness is the
  adversarial bound `f`: `honest_holders >= certified - f >= t` -- the same
  `q >= t + f` flat lattice as foqq9 (garbage-everyone dealer: `3 < 10`,
  close withheld, pre-activation roll; single honest garble: one certificate
  permanently unmintable, `9 < 10`). Nomination indexing: slot-`s` ballots
  carry NO presence/absence at all -- the pulse-vs-fallback branch is a
  PROOF-INDEPENDENT read of the already-closed SOURCE `C_{s-1}`'s OWN registry
  (exactly ONE balloted value per target slot), and both branches derive from
  `C_{s-1}` / `H(value_{s-1})` with the target slot number only as a domain
  separator -- the fallback never references the value being nominated (`C_s`),
  so no proposer can select a presence branch on async proof delivery.

Reproduce the evidence deterministically (no live network):
- `python contents/cap-0089/layer_b_model.py` -> 120 PASS
- `python contents/cap-0089/check_vectors.py` -> 48 PASS

Branch `cap-0089-vrf-randomness` is pushed (`d52e757..53fc4d7`) to the reviewing
fork; PR head `53fc4d7`, mergeable = true.

Copilot AI balanced review requested due to automatic review settings August 30, 2026 13:16

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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_…, or V6_… 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.

Comment thread core/cap-0089.md Outdated
Comment thread core/cap-0089.md Outdated
Comment thread core/cap-0089.md Outdated
Comment thread core/cap-0089.md Outdated
Comment thread core/cap-0089.md Outdated
Comment thread contents/cap-0089/gen_vectors.py Outdated
Comment thread contents/cap-0089/xdr-diff.md Outdated
Comment thread contents/cap-0089/check_vectors.py Outdated
Comment thread contents/cap-0089/gen_vectors.py Outdated
Comment thread contents/cap-0089/fixture_vectors.json Outdated

tacticalnoot commented Aug 30, 2026

Copy link
Copy Markdown

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.

Copilot AI review requested due to automatic review settings August 30, 2026 14:00
@EslaM-X EslaM-X changed the title Add CAP-0089: VRF-Driven Protocol Randomness and Leader Election Add CAP-0089: VRF-Based Protocol Randomness and Fair Leader Selection Aug 30, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 a purpose, but the commit phase defines one beta_v and one shared beacon while the only legal purpose values are the three consumer labels. Implementations cannot know whether to evaluate purpose 0x01, produce three contributions, or use another beacon-specific label. Pin one beacon-generation transcript (preferably with its own label), then keep consumer separation in subseed(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, every StellarValue includes 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 slot s and assert VRF_verify rejects it under the s+1 transcript.
    # --- 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 = 5 and a StellarValue.ext arm, 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

Comment thread core/cap-0089.md Outdated
Comment thread core/cap-0089.md Outdated
Comment thread core/cap-0089.md Outdated
Comment thread contents/cap-0089/xdr-diff.md Outdated
Comment thread core/cap-0089.md Outdated
Comment thread contents/cap-0089/check_vectors.py Outdated
Comment thread contents/cap-0089/check_vectors.py Outdated
Comment thread contents/cap-0089/check_vectors.py Outdated
Comment thread core/cap-0089.md Outdated
Comment thread contents/cap-0089/check_vectors.py Outdated
Copilot AI review requested due to automatic review settings August 30, 2026 14:44

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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, and vrfReveals. 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 is t = 3; after dropping one contribution, k = 2, so the specified result is the LCL fallback, not beacon(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)

Comment thread core/cap-0089.md Outdated
Comment thread core/cap-0089.md Outdated
Comment thread core/cap-0089.md Outdated
Comment thread core/cap-0089.md
Comment thread core/cap-0089.md
Comment thread core/cap-0089.md
Comment thread core/cap-0089.md Outdated
Comment thread contents/cap-0089/check_vectors.py
Comment thread contents/cap-0089/check_vectors.py Outdated
Comment thread core/cap-0089.md Outdated

tacticalnoot commented Aug 30, 2026

Copy link
Copy Markdown

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.

tacticalnoot commented Aug 30, 2026

Copy link
Copy Markdown

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.

tacticalnoot commented Aug 30, 2026

Copy link
Copy Markdown

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-NodeID reveal model appears to create extra reveal/no-reveal knobs as m controlled identities grows. I’d make this an explicit adversarial vector:

same controller + same consensus power + 1 identity vs same controller + same consensus power + m identities → indistinguishable bias budget.

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.

tacticalnoot commented Aug 30, 2026

Copy link
Copy Markdown

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.

Copy link
Copy Markdown

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 check_vectors.py grow a small exhaustive state-space check around V2/fallback, with the same invariant checked independently from the fixture generator?

— Noot’s Raven 🐦‍⬛ — make every valid route clear the same safety bar, and the measurement becomes as trustworthy as the beacon.

tacticalnoot commented Aug 30, 2026

Copy link
Copy Markdown

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 R_pre / R_post roots.

Current shape: one verified R_s is produced from the exact externalized semantic close for ledger s; APPLY + PRNG consume labeled derivations in s, and NOMINATION consumes a labeled derivation in s+1. Before the close is locked there is no valid complete root; after lock every valid proof path must collapse to the same root or remain UNKNOWN/wait.

tacticalnoot commented Aug 30, 2026

Copy link
Copy Markdown

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 E, vary every protocol-valid action that could affect which result is produced, accepted, selected, replayed, or consumed — even if that choice sits far before or after E:

  • setup/source/key/software state;
  • proposer contents, identity split, routing, retries;
  • reveal order, omission, timeout, threshold edge;
  • fallback, selective use, replay, recovery, migration;
  • compute scale and timing resolution.

Acceptance target: none may change which root is valid, create a second valid root, or let someone discard an observed result for another one.

same committed event + same consensus state -> one reachable root

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 reachable_roots == 1 across the full causal and compute-capability envelope for both timing domains?

— 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.

Copy link
Copy Markdown

Problem: the beacon can be neutral while its use is still gameable.

The current draft makes B(s) public before slot s and derives all three consumer seeds from it. That timing fits nomination, but apply order today depends on the final tx-set hash; exposing its replacement seed before the tx set is frozen creates a look-before-lock surface that needs an explicit test. Soroban PRNG needs the same test against transaction submission/retry.

The last-revealer B_with / B_without branch is also real choice power: two pseudorandom candidates are still two candidates if the revealer can evaluate a downstream predicate and choose which one survives.

I think the stronger invariant is causal sealing, per consumer:

source/round/fallback fixed -> actor-controlled inputs locked -> value revealed -> deterministic use

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 B(s) serving all three at the same time.

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.

Copy link
Copy Markdown

Problem: one shared beacon is revealed too early to be the safest seed for all three consumers.

The CAP finalizes B(s) at s-1, makes it public before slot s, then derives nomination, Soroban PRNG, and apply-order seeds from it. That timing is useful for nomination, but the other two consumers can still have actor-controlled choices later.

A stronger acceptance target may be one precommitted randomness stream with consumer-specific reveal horizons:

  • source key/identity, round schedule, (slot, purpose) -> round mapping, and failure rule are fixed in advance;
  • each purpose gets a unique scheduled round only after the inputs it randomizes are locked;
  • each scheduled round has exactly one valid result;
  • recovery reconstructs that same result or fails closed — it never switches to another observed source/value.

Then the harness can require reachable_roots == 1 across omission, retries, routing, faster hardware, and archival replay for each consumer separately.

Would this be a cleaner top-level target than forcing one already-public B(s) across three different causal boundaries?

— Noot’s Raven 🐦‍⬛ — one clock can serve three doors, as long as each door opens only after its choices are sealed.

tacticalnoot commented Aug 30, 2026

Copy link
Copy Markdown

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 c3a051e:

  • PR summary: signed VRFCommit, t = floor(2Q/3)+1, V2 includes threshold/fallback.
  • Normative CAP/XDR: VRFCommit { NodeID, commitHash }, t = ceil(2Q/3)+1.
  • check_vectors.py V2: drops one reveal and explicitly requires the new beacon to differ from the full beacon; it does not exercise threshold or fallback logic.

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:

authenticated membership -> lock -> scheduled reveal -> one recoverable root OR explicit fail-closed state -> deterministic use

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.

Copy link
Copy Markdown

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:

fixed round + fixed group key + any valid threshold subset -> the same unique output

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/
https://arxiv.org/abs/1805.04548

Could subset invariance become an exit criterion here, so changing who reveals can change availability but never which randomness wins?

— Noot’s Raven 🐦‍⬛ — let the quorum decide whether the door opens, never what is behind it.

Copy link
Copy Markdown

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 randomness epoch first-class — fixed group public key + membership commitment + threshold + activation ledger + transition rule — and then require every round inside that epoch to reconstruct one subset-invariant root?

— Noot’s Raven 🐦‍⬛ — freeze the team before the draw, then let every valid subset recover the same future.

Copy link
Copy Markdown

Problem: NodeID uniqueness is not controller uniqueness. A contributor rule that gives one randomness share or one unit of threshold weight per validator identity can amplify one controller if the same control is split across multiple NodeIDs.

The acceptance property I’d add is:

same underlying consensus influence + more identities -> zero additional randomness privilege

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.

Copy link
Copy Markdown

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:

before lock -> no actionable root

after lock -> every valid observer view is UNKNOWN or the same root R

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 accepted_roots == {R} and prelock_actionable_views == 0 for nomination, contract PRNG, and apply order?

— Noot’s Raven 🐦‍⬛ — let the network move the message, never the future.

Copy link
Copy Markdown

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:

pulse_id = H(locked_event, accepted_source_root)

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 pulse_id.

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.

Copy link
Copy Markdown

Problem: consensus still participates in constructing the random value. Contributor eligibility comes from the externalized s-2 value, the accepted reveal set determines B(s), and the fallback replaces it with an LCL-derived root.

A stronger boundary is:

pulse_id = H(locked_event, accepted_source_root)

Then consensus may only attest pulse_id; quorum path, proposer, signer count, message order, fallback path, or arrival timing may change availability, but cannot change pulse identity.

Acceptance vector: hold the locked event and valid source evidence fixed, vary every consensus/notarization path, and require one identical pulse_id or explicit no-pulse.

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.

tacticalnoot commented Aug 30, 2026

Copy link
Copy Markdown

Updated after the full exact-head recheck.

The governing invariant still stands, but the current target is stricter and simpler:

same canonical locked semantic close + predecessor-fixed epoch -> exactly one verified root R, or UNKNOWN/wait

There is no value-bearing fallback and no current NO_PULSE outcome. Core owns the canonical close/XDR, epoch selection, release/admission, carrier, persistence, replay/catchup, upgrades, and consumer wiring. Rust supplies only the irreducible unique-threshold cryptographic primitive behind the smallest audited seam.

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.

Copilot AI review requested due to automatic review settings August 31, 2026 00:23
…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.
@EslaM-X

EslaM-X commented Sep 4, 2026

Copy link
Copy Markdown
Author

Final audit commit: security/correctness evidence package + isolated proof-context vectors

Building 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 cross-network/cross-slot tests previously rejected at the commit-signature scope — a testnet VRFCommit.sig does not verify under a mainnet scope — so they never exercised the property they claimed to exercise: proof binding to (network, slot). Each target context is now constructed fully self-consistent:

  • VRFCommit signatures re-signed under the target network/slot scope,
  • target transcript,
  • proofs derived for the target context.

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). check_vectors.py now reports 44 PASS (was 40).

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:

  • 14 precise invariants (I1–I14), each mapped to a real executable check label in layer_b_model.py / check_vectors.py — not prose promises;
  • property × evidence matrix (13 properties);
  • explicit threat model: assumptions A1–A6, adversary may / may-not — security claims are conditional on that model, and no "unbiased" claim is left undefined;
  • a 16-row attack catalog with the fail-closed mechanism and test evidence per attack;
  • a runtime-decidable state machine (REJECTED | UNKNOWN | ROOT(R), no NO_PULSE, no timeout-derived subjective state) including the failure/disambiguation paths;
  • a "why this mechanism exists" table so every cryptographic component can be re-validated against the threat it answers;
  • resource/cost analysis at canonical XDR wire widths;
  • a reviewer-response ledger mapping the 17 review rounds to root-cause → resolution → regression.

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):

python contents/cap-0089/layer_b_model.py   # 89 PASS
python contents/cap-0089/check_vectors.py   # 44 PASS

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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 5 out of 7 changed files in this pull request and generated 7 comments.

Comment thread core/cap-0089.md Outdated
Comment thread core/cap-0089.md Outdated
Comment thread contents/cap-0089/check_vectors.py Outdated
Comment thread contents/cap-0089/layer_b_model.py
Comment thread contents/cap-0089/layer_b_model.py Outdated
Comment thread core/cap-0089.md Outdated
Comment thread core/cap-0089.md Outdated
…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)

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 5 out of 7 changed files in this pull request and generated 5 comments.

Comment thread contents/cap-0089/check_vectors.py Outdated
Comment thread core/cap-0089.md Outdated
Comment thread contents/cap-0089/layer_b_model.py Outdated
Comment thread core/cap-0089.md Outdated
Comment thread core/cap-0089.md Outdated
…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)

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 5 out of 7 changed files in this pull request and generated 6 comments.

Comment thread contents/cap-0089/layer_b_model.py Outdated
Comment thread contents/cap-0089/layer_b_model.py Outdated
Comment thread contents/cap-0089/layer_b_model.py Outdated
Comment thread contents/cap-0089/layer_b_model.py
Comment thread core/cap-0089.md
Comment thread core/cap-0089.md Outdated
…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)

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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)

Comment thread contents/cap-0089/layer_b_model.py
Comment thread contents/cap-0089/layer_b_model.py
Comment thread contents/cap-0089/layer_b_model.py
Comment thread contents/cap-0089/layer_b_model.py
Comment thread core/cap-0089.md Outdated
Comment thread contents/cap-0089/check_vectors.py
Comment thread contents/cap-0089/layer_b_model.py Outdated
Comment thread contents/cap-0089/layer_b_model.py
Comment thread contents/cap-0089/layer_b_model.py Outdated
Comment thread core/cap-0089.md Outdated

Copy link
Copy Markdown

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 >= t CONFIRM voters can contain up to f Byzantine voters, so it guarantees only t-f honest post-finality releasers, not t.

Concrete admitted case: n=7, t=5, f=2. Both stated bounds hold (2t-n=3>2 and t<=n-f=5), but a 5-voter certificate can be 2 Byzantine + 3 honest. If the two Byzantine voters withhold after finality, only 3 ReleaseShares appear and a threshold-5 root stays permanently UNKNOWN. The counting condition for a certificate alone to guarantee t honest releasers would be |cert|-f >= t (so |cert| >= t+f), and that is still only a counting condition, not a full SCP-composition proof.

This matters directly to Core because the CAP makes APPLY mandatory while current SCP externalizes as part of setConfirmCommit once the commit is federated-ratified; there is no independent roster-availability authority there. A mandatory post-finality WAIT therefore needs to be proven available from the same finalizing evidence, rather than assumed from permission to release.

Can CAP-0089 either prove SCP_FINALIZES(s) => >=t actual post-lock ReleaseShares(s) for every admitted Byzantine schedule without pre-finality reveal, or keep APPLY non-mandatory until that implication exists?

tacticalnoot commented Sep 5, 2026

Copy link
Copy Markdown

Correction: my last counting fix still assumed nonfaulty => eventually externalizes; SCP does not give that. The missing contract is topology.

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 F:

SCP_FINALIZABLE(F) => |Roster ∩ Intact(F)| >= t

and every admitted Byzantine set B has |Roster ∩ B| < t.

Then any SCP-live close has t eventual local externalizers while no pre-lock coalition can reconstruct. The roster-CONFIRM certificate and flat n,t,f liveness claim can both disappear.

… 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)

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 5 out of 7 changed files in this pull request and generated 6 comments.

Comment thread contents/cap-0089/layer_b_model.py
Comment thread core/cap-0089.md Outdated
Comment thread core/cap-0089.md Outdated
Comment thread contents/cap-0089/layer_b_model.py Outdated
Comment thread contents/cap-0089/layer_b_model.py
Comment thread core/cap-0089.md Outdated
@EslaM-X

EslaM-X commented Sep 6, 2026

Copy link
Copy Markdown
Author

@tacticalnoot — you are right on both counts, and 5549653283 supplies exactly the
missing contract. I have adopted it verbatim as the round-26/27 committed-topology
design, and both the existential (SCP_FINALIZES(s) -> >= t actual post-lock ReleaseShares(s)) and the counting (|cert|-f >= t) fallbacks are now discharged
by a SINGLE mechanism, so the roster-CONFIRM certificate and the flat n,t,f
liveness claim can indeed both disappear — the branch has dropped them.

The committed topology contract (round-26). The epoch now COMMITS the exact
SCP quorum-set graph it relies on: scp_qset = (q, members) is validated and
pinned into epoch.hash as scp_qset_hash (a relevant qset change — different
quorum threshold q or different member list — produces a NEW epoch hash and
therefore a new epoch), and activation is gated by a SAT enumeration over every
admitted fault pattern B (|B| <= f) — the model's _topology_contract_violations
stands in for Core's Rust checker — enforcing exactly your three conditions:

  • SCP_FINALIZABLE(B) => |Roster ∩ Intact(B)| >= t (your implication);
  • |Roster ∩ B| < t (no pre-lock coalition reaches reconstruction);
  • no admitted B is itself an SCP quorum (Byzantine-exclusion / quorum avoidance).

UNKNOWN is therefore bounded-by-topology: any SCP-live close (externalized by
an intact quorum) has >= t INTACT rostered externalizers by the committed graph,
and each independently crosses its release boundary — the mandatory APPLY/PRNG wait
is proven available from the same finalizing evidence (the committed graph), not
assumed from permission to release. The certificate is now an authentication gate
only; it is never the liveness authority.

The executable proof (model, rounds 26–27). layer_b_model.py is ALL PASS
(111 checks)
, check_vectors.py ALL PASS (47 checks), pushed as
34d69ac..39b05b4:

  • "R26 topology: the epoch COMMITS the SCP qset graph" — qset change => different
    epoch.hash (a roll); member-list order canonicalizes to the same committed graph.
  • "R26 topology C0: activation REJECTS a Byzantine-only quorum"; "R26 topology
    C1/C2" — I also ran your n=7, t=5, f=2 admitted case: under the committed flat
    graph every admitted B (|B| <= 2) leaves Intact >= 5 = t, so your
    "2 Byzantine + 3 honest cert, threshold-5 root permanently UNKNOWN" scenario is
    impossible in an activatable epoch — the graph, not a certificate count, is what
    guarantees t honest releasers.
  • "R27-b (0xEmpty / 5549584433)" — exercises your exact withholding counterexample
    on the real machinery: a THRESHOLD-sized cert (t=5 genuine CONFIRM votes)
    externalizes the close, a Byzantine subset of the certified voters WITHHOLDS its
    release share, recover_proof over the surviving t-f=4 honest carriers returns
    None -> resolve() UNKNOWN (apply stalls), NEVER a root — withholding is
    delayed availability, and the >= t eventual honest releasers come from the
    committed C1 topology, not from the certificate.
  • "R27-a" proves your architecture-level point both ways: a finalizing identity that
    differs from the share-dealt roster is allowed but never drops |Intact| below
    t; under the widened roster-subset-of-validators fault class the gate fires at
    activation. "R27-c": qset member-list mutation rolls the epoch.

A dedicated "Security review: the implicit global n/f/t model vs SCP's local
quorum-slice model" section in the CAP formalizes the invariant, the component ×
assumption × SCP-reality × risk × closure matrix, and the decision
(repairable -> repaired; no t+f arithmetic patch).

— Eslam

@EslaM-X

EslaM-X commented Sep 6, 2026

Copy link
Copy Markdown
Author

@tacticalnoot — your 5549584433 counterexample and 0xEmpty's independent Discord
review of CAP-0089 raised the same two points; both are now formally neutralized by
the round-26/27 committed topology and are executable in the model. Since this is
the older comment of the pair, I want to state plainly where it lands after
5549653283 was folded in:

The |cert| - f >= t (i.e. |cert| >= t+f) counting fix is not what I adopted.
I adopted your own sharper correction (5549653283): the missing contract is
topology, not certificate arithmetic. A certificate is a consensus-authentication
fact (used for release/authority), and liveness is a property of the COMMITTED qset
graph + SAT activation gate (SCP_FINALIZABLE(B) => |Roster ∩ Intact(B)| >= t, plus
|Roster ∩ B| < t, evaluated over every admitted B at activation). Under that
gate a roster-complete CONFIRM certificate no longer needs to do liveness work at
all — the CAP's "roster-CONFIRM certificate and flat n,t,f liveness claim can both
disappear" (your words) is exactly what the branch did: dropped them.

Your n=7, t=5, f=2 admitted case is discharged by the gate: every admitted fault
pattern leaves >= 5 INTACT rostered members by the committed graph, so the
"2 Byzantine + 3 honest cert" withholding scenario cannot be the liveness-
determining state in an activatable epoch; if it were ever reachable it would be an
UNKNOWN bounded by topology, not a permanent stall. layer_b_model.py ALL PASS
(111), check_vectors.py ALL PASS (47), push 34d69ac..39b05b4; the three-state
ROOT / UNKNOWN / REJECTED adjudication and the per-member release boundary remain
in force, and the CAP's new Security-review section (round-27) documents the full
argument against the t+f arithmetic patch.

— 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)

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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_PULSE outcome and uses UNKNOWN/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_seeds contains 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 if alpha_string is 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}

Comment thread contents/cap-0089/layer_b_model.py
Comment thread core/cap-0089.md Outdated
Comment thread core/cap-0089.md Outdated
Comment thread contents/cap-0089/layer_b_model.py Outdated
Comment thread contents/cap-0089/layer_b_model.py
…once leaks e_d while the secret-nonce form defeats recovery (ALL PASS 114)

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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_seeds contains the testnet-derived keys, while main_rows is verified under _main_vp's mainnet-derived keys. Rejection can therefore be caused by the wrong public key rather than the network embedded in alpha. 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]

Comment thread contents/cap-0089/check_vectors.py Outdated
Comment thread core/cap-0089.md Outdated
Comment thread core/cap-0089.md
Comment thread core/cap-0089.md Outdated
…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)

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 5 out of 7 changed files in this pull request and generated 4 comments.

Comment thread contents/cap-0089/layer_b_model.py Outdated
Comment thread core/cap-0089.md
Comment thread contents/cap-0089/check_vectors.py Outdated
Comment thread contents/cap-0089/check_vectors.py Outdated
…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).

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 5 out of 7 changed files in this pull request and generated 3 comments.

Comment thread contents/cap-0089/layer_b_model.py Outdated
Comment thread core/cap-0089.md Outdated
Comment thread core/cap-0089.md Outdated
…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)

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 5 out of 7 changed files in this pull request and generated 4 comments.

Comment thread contents/cap-0089/layer_b_model.py Outdated
Comment thread core/cap-0089.md Outdated
Comment thread core/cap-0089.md Outdated
Comment thread contents/cap-0089/layer_b_model.py Outdated
…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).

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 5 out of 7 changed files in this pull request and generated 5 comments.

Comment on lines +1255 to +1257
Ki = _validator_confirm_pub(GROUP_SK, i)
return _avss_material_cert_verify(Ki, i, epoch, cl_hash,
cert_records.get(i), dealer_digest)
Comment thread core/cap-0089.md
Comment on lines +901 to +905
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:
Comment thread core/cap-0089.md
Comment on lines +2783 to +2788
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
Comment on lines +225 to +228
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))
Comment thread core/cap-0089.md
Comment on lines +2753 to +2756
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
@EslaM-X

EslaM-X commented Sep 7, 2026

Copy link
Copy Markdown
Author

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants