Files
corona/INTEGRATION.md
T
Hanzo AI c77969ea67 pulsar: lattice threshold kernel with key-era lifecycle + Pulsar-SHA3 + Nebula binding
Forked from github.com/luxfi/corona @ d09b2c2. Pulsar inherits the
2-round signing math byte-equal but adds blockchain-grade key
lifecycle and a SHA3-based domain-separated hash profile for
permissionless validator-set rotation.

Inherits from upstream Corona (byte-equal):
  - Sign1/Sign2/Combine 2-round signing math
  - Lattice ring R_q = Z_q[X]/(X^256+1), q = 0x1000000004A01
  - Module-LWE / Module-SIS hardness
  - NTT, Discrete-Gaussian Ziggurat, MAC layer

Replaces from upstream:
  - Broken Feldman-style DKG (pseudoinverse-recoverable). Replaced
    with: (a) one-time foundation MPC ceremony / trusted-dealer
    Bootstrap; (b) Pedersen DKG over R_q (research, dkg2/) hiding
    under MLWE on B / binding under MSIS on [A | B].
  - Trusted-dealer-per-epoch lifecycle (wrong shape for permissionless
    rotation). Replaced with Verifiable Secret Resharing (VSR) every
    epoch under the persistent group public key.

Key-era lifecycle (KeyEraID / Generation / RollbackFrom):
  - KeyEraID bumps only at Reanchor (rare governance event)
  - Generation bumps every Refresh / Reshare under the same GroupKey
  - RollbackFrom records prior generation reverted from (0 = forward)
  - Activation cert (QUASAR-PULSAR-ACTIVATE-v1) gates new generations
    under the unchanged GroupKey

Pulsar-SHA3 hash profile (NIST FIPS 202 / SP 800-185):
  - Hc:        cSHAKE256("PULSAR-HC-v1", transcript) → challenge
  - Hu:        cSHAKE256("PULSAR-HU-v1", transcript) → XOF stream
  - TranscriptHash: TupleHash256("PULSAR-TRANSCRIPT-v1", parts...)
  - PRF:       KMAC256(key, msg, "PULSAR-PRF-v1")
  - MAC:       KMAC256(key, msg, "PULSAR-MAC-v1")
  - Pairwise:  KMAC256(kex, encode(...), "PULSAR-PAIRWISE-v1")
  - Default suite is Pulsar-SHA3; Pulsar-BLAKE3 retained as optional
    non-normative fast profile. HashSuiteID is bound into transcripts.
  - Implementation at pulsar/hash/ with HashSuite interface.
  - All KATs regenerated under SHA3 profile; vectors in
    luxcpp/crypto/pulsar/test/kat/.

Nebula root binding (TranscriptInputs + ActivationMessage canonical
bytes, all bound under TupleHash256):
    chain_id, network_id, group_id,
    key_era_id, old_generation, new_generation,
    old_epoch_id, new_epoch_id,
    old_set_hash, new_set_hash,
    threshold_old, threshold_new,
    group_public_key_hash,
    nebula_root,
    hash_suite_id,
    implementation_version,
    variant.

Packages:
  primitives/   Shamir over R_q, Lagrange, hash helpers
  sign/         2-round sign math (byte-equal upstream)
  threshold/    GroupKey, KeyShare, Signer types
  reshare/      VSR kernel — Refresh (HJKY97 zero-poly), Reshare
                (Desmedt-Jajodia), commit (Pedersen R_q), complaint,
                transcript, pairwise KEX, activation cert
  hash/         HashSuite interface; PulsarSHA3 (default), PulsarBLAKE3
  dkg2/         Pedersen DKG over R_q (research, reference only)
  keyera/       Lifecycle wrapper: Bootstrap → Reshare → Reanchor
  papers/       LP-073-pulsar paper (sections + bibliography)
  cmd/*_oracle/ KAT generators (Go ↔ C++ byte-equal validation)

DESIGN.md is the single source of truth:
  - The Pulsar metaphor (rotating beam over persistent body)
  - Vocabulary stack per LP-105: Nebula / Photon / Lumen / Beam /
    Pulsar / Pulse / Prism / Horizon / Quasar
  - Pulsar ≠ Lumen (PQ stream is separate, planned)
  - Pulsar / Lens / LSS three-layer architecture
  - Bootstrap Dealer vs Signature Coordinator
  - No-slashing failure ladder (timeout → retry → rollback → reanchor)
  - MVP VSR vs Robust VSR phasing
  - Borrowed-brand terms ("X-Wing" used casually) replaced with
    "hybrid KEM" / "Hybrid Lumen Handshake"; X-Wing cited only as
    the IETF combiner primitive name where exact reference matters.

Tests: full suite green
  hash 5+/5+, keyera 5/5, reshare 45+/45+, threshold, sign, dkg,
  dkg2, primitives, networking, utils, corona_oracle_v2 KAT —
  all pass.

Honest claim: the crypto primitives are not new (HJKY97,
Desmedt-Jajodia97, Wong-Wang-Wing02, Corona, Hermine, Threshold
Raccoon all exist). The novel contribution is the systems
composition: a permissionless-consensus deployment architecture that
turns static two-round PQ threshold signatures into a dynamic,
leaderless, validator-rotation-tolerant finality layer.

Companion docs: papers/lp-073-pulsar/ (academic paper); LP-073 +
LP-103 + LP-105 in github.com/luxfi/lps; threshold/protocols/lss/
lss_pulsar.go (LSS adapter wrapping this kernel).
2026-03-03 12:00:00 -08:00

8.8 KiB

Quasar Consensus Integration: Resharing Replaces Per-Epoch DKG

This document describes how ~/work/lux/consensus/protocol/quasar/epoch.go should be modified to call pulsar/reshare at validator-set rotations instead of running a fresh trusted-dealer GenerateKeys every time.

Status

  • Resharing primitive: complete (pulsar/reshare/reshare.go)
  • Refresh primitive: complete (same-committee proactive update)
  • KAT oracle: complete (16 entries, byte-equal Go↔C++)
  • C++ port: complete (luxcpp/crypto/pulsar/cpp/reshare.{hpp,cpp})
  • Integration test: complete (signature with new committee verifies under unchanged public key b̃)
  • Wire integration into Quasar consensus: PENDING
  • Verifiable Secret Resharing layer (commitments, complaints, activation certs): PENDING (depends on pulsar/dkg2/, parallel track)

Current state of epoch.go

// EpochManager.generateEpochKeysWithThreshold (line 336)
func (em *EpochManager) generateEpochKeysWithThreshold(
    epoch uint64, validators []string, threshold int,
) (*EpochKeys, error) {
    // ... Generate Corona threshold keys ...
    shares, groupKey, err := coronaThreshold.GenerateKeys(threshold, n, nil)
    // ...
}

GenerateKeys runs trusted-dealer Gen internally. Whoever runs generateEpochKeysWithThreshold learns the master secret s and the new public key b is unrelated to the previous epoch's. This is the defect the resharing primitive fixes.

Target state

// genesis only — runs once, before any epoch rotation
func (em *EpochManager) InitializeGenesis(validators []string) (*EpochKeys, error) {
    // First time only: run trusted-dealer Gen (or the eventual MPC
    // ceremony in pulsar/dkg2/). Result: shares + groupKey + master
    // public key b̃. Persist b̃ to chain genesis state — it is a
    // permanent, public, never-rotating value.
    shares, groupKey, err := coronaThreshold.GenerateKeys(
        em.threshold, len(validators), nil)
    // ...
}

// epoch rotation — runs every time the validator set changes
func (em *EpochManager) RotateEpochViaReshare(
    newValidators []string,
) (*EpochKeys, error) {
    // 1. Build the input to pulsar/reshare.Reshare from the OUTGOING
    //    epoch's shares.
    oldEpoch := em.currentKeys
    oldShares := convertToReshareInput(oldEpoch)

    // 2. Compute new committee party IDs (1-indexed).
    newSet := buildPartyIDs(newValidators)

    // 3. Run reshare. The randSource MUST be mixed by the consensus
    //    layer from a per-epoch entropy beacon (e.g. the previous
    //    block's hash + an honest-validator BLS contribution). Each
    //    PARTICIPATING outgoing validator runs Reshare LOCALLY with
    //    its OWN share + its OWN local randomness, then ships
    //    g_i(j) to each new validator. The function below is what
    //    one outgoing validator runs.
    newShares, err := reshare.Reshare(
        ringParams.R,
        oldShares,
        oldEpoch.Threshold,
        newSet,
        em.threshold,
        per_party_rng_source(epoch),
    )

    // 4. Wrap the new shares into Corona KeyShare instances.
    //    GroupKey is INHERITED from the old epoch — same A, same b̃.
    newKeyShares := wrapAsCoronaShares(
        newShares, oldEpoch.GroupKey,
        recomputeLagrangeCoefficients(newValidators, em.threshold),
    )

    // 5. Activation certificate: the new committee threshold-signs
    //    the transcript of the resharing under the SAME group key.
    //    Chain accepts the new epoch only when this cert verifies.
    activationSig, err := signActivationCert(
        newKeyShares, oldEpoch.GroupKey,
        transcriptHash(epoch, oldValidators, newValidators, em.threshold),
    )

    // 6. Build new EpochKeys with the inherited group key.
    return &EpochKeys{
        Epoch:        epoch + 1,
        ValidatorSet: newValidators,
        Threshold:    em.threshold,
        GroupKey:     oldEpoch.GroupKey,  // unchanged!
        Shares:       newKeyShares,
        // ...
    }, nil
}

Critical design points

  1. GroupKey inheritance. The GroupKey (which contains A and bTilde) is set at genesis and persists for the lifetime of the group. Every epoch's EpochKeys.GroupKey points to the same underlying object. This is what makes VerifySignatureForEpoch work across epochs without separate per-epoch verification keys and what eliminates the need for "verification key history".

  2. Lagrange coefficients are recomputed per epoch. In the old sign.Gen flow, each party's Lambda is baked into its share. Reshare produces shares in the standard polynomial-evaluation form, so each party computes its Lambda on the fly using primitives.ComputeLagrangeCoefficients over the active signing set. This is a tiny computation (one Lagrange basis evaluation per signer per signing session) and avoids the need for any committee- specific share preprocessing.

  3. Pairwise channels. Reshare outputs the kernel — the function computes one party's shares from its own old share + fresh randomness, but assumes a magic transport. In the wire integration:

    • Each outgoing party i independently:

      • Computes g_i(X) (one polynomial of degree t_new - 1).
      • Evaluates at every j ∈ new_set.
      • Encrypts g_i(j) for party j under their pairwise key (X25519 + ML-KEM hybrid).
      • Broadcasts the ciphertexts on a chain-anchored bulletin board.
    • Each incoming party j independently:

      • Decrypts the t_old (or fewer, if some i ∈ Q are unavailable) envelopes addressed to it.
      • Sums the decrypted g_i(j) values.
      • Result is s'_j.
  4. VSR layer (out of scope here). The kernel above is correct under honest-but-curious dealers. For full Byzantine resilience the following are required, in pulsar/dkg2/ (parallel track):

    • Pedersen-style polynomial commitments to g_i.
    • Complaint protocol for inconsistent dealings.
    • Deterministic disqualification + re-quorum.
  5. Activation certificate. The chain MUST NOT accept a new epoch unless the new committee can produce a valid threshold signature under the inherited GroupKey. This proves liveness of the new committee. The activation message is the transcript hash of the resharing.

  6. Genesis is special. The initial s must come from somewhere:

    • Foundation MPC ceremony (recommended): N geographically distinct parties run a verifiable DKG once, ship transcripts; chain genesis pins bTilde.
    • Trusted-dealer (acceptable for testnets only): one party runs Gen, publishes bTilde, deletes s.

    After genesis, no party ever re-derives s.

  7. Refresh between rotations. Within a stable validator set, the pulsar/reshare.Refresh primitive runs the HJKY97 zero-polynomial update. Recommended cadence: every MaxEpochDuration (1 hour by default) even when validator set unchanged. This defeats a mobile adversary that compromises < t parties per epoch and tries to accumulate share material over time.

Migration path

Phase 1 (this PR): land the kernel + KAT + C++ port. No consensus changes yet. Resharing is callable from tests but not from production.

Phase 2: add RotateEpochViaReshare to epoch.go behind a feature flag. Existing RotateEpoch (which calls GenerateKeys) remains the default. New method is exercised on devnet only.

Phase 3: wire VSR layer (commitments, complaints, activation) on top of RotateEpochViaReshare. Continue devnet exercise; harden against adversarial validators.

Phase 4: switch testnet default to resharing. Run for at least one month with mixed honest/byzantine drills. Continue to allow GenerateKeys as escape hatch via governance proposal.

Phase 5: switch mainnet default. GenerateKeys removed from epoch code path; only callable for testnet bootstrap.

Proof of public-key invariance (test)

// pulsar/reshare/integration_test.go: TestResharePreservesPublicKey
//
// Demonstrates the load-bearing claim:
//
// 1. Sample s, A, e at genesis. Compute b = A·s + e, b̃ = round(b, ξ).
// 2. Standard-Shamir-share s across 3 parties with threshold 2.
// 3. Run the existing 2-round Sign protocol with the OLD committee.
//    Verify the resulting signature against b̃. Pass.
// 4. Reshare onto a 5-party committee with threshold 3, FRESH PARTY
//    IDs (no overlap with old committee).
// 5. Run Sign again with the NEW committee. Verify against the SAME
//    b̃ from step 1. Pass.
//
// Test status: PASSING.

Test output (current run):

=== RUN   TestResharePreservesPublicKey
    integration_test.go:131: OLD: signature verified
    integration_test.go:213: NEW: signature verified
    integration_test.go:217: PASS: NEW-committee signature verifies against
                                   UNCHANGED public key b̃
--- PASS: TestResharePreservesPublicKey (0.53s)