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).
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
-
GroupKey inheritance. The
GroupKey(which containsAandbTilde) is set at genesis and persists for the lifetime of the group. Every epoch'sEpochKeys.GroupKeypoints to the same underlying object. This is what makesVerifySignatureForEpochwork across epochs without separate per-epoch verification keys and what eliminates the need for "verification key history". -
Lagrange coefficients are recomputed per epoch. In the old
sign.Genflow, each party'sLambdais baked into its share. Reshare produces shares in the standard polynomial-evaluation form, so each party computes itsLambdaon the fly usingprimitives.ComputeLagrangeCoefficientsover 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. -
Pairwise channels.
Reshareoutputs 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
iindependently:- Computes
g_i(X)(one polynomial of degreet_new - 1). - Evaluates at every
j ∈ new_set. - Encrypts
g_i(j)for partyjunder their pairwise key (X25519 + ML-KEM hybrid). - Broadcasts the ciphertexts on a chain-anchored bulletin board.
- Computes
-
Each incoming party
jindependently:- Decrypts the
t_old(or fewer, if somei ∈ Qare unavailable) envelopes addressed to it. - Sums the decrypted
g_i(j)values. - Result is
s'_j.
- Decrypts the
-
-
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.
- Pedersen-style polynomial commitments to
-
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. -
Genesis is special. The initial
smust 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, deletess.
After genesis, no party ever re-derives
s. - Foundation MPC ceremony (recommended): N geographically distinct
parties run a verifiable DKG once, ship transcripts; chain genesis
pins
-
Refresh between rotations. Within a stable validator set, the
pulsar/reshare.Refreshprimitive runs the HJKY97 zero-polynomial update. Recommended cadence: everyMaxEpochDuration(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)