mirror of
https://github.com/luxfi/pulsar.git
synced 2026-09-24 00:25:52 +00:00
pulsar-m: drop unnecessary Lux prefix from round identifiers
LuxRoundContext / LuxRoundSessionID / LuxRoundCommitteeRoot / LuxRoundSigShare / LuxRoundQuorumPolicy / DefaultLuxRoundQuorumPolicy → RoundContext / RoundSessionID / RoundCommitteeRoot / RoundSigShare / RoundQuorumPolicy / DefaultRoundQuorumPolicy. Files: luxround.go luxround_math.go luxround_test.go → round.go round_test.go (math helpers folded into round.go). The protocol name "Lux round" stays in prose / spec / paper titles -- only the Go identifiers drop the brand prefix. The cSHAKE customization tag PULSAR-M-LUXROUND-COMMITTEE-V1 is wire-format-stable and stays.
This commit is contained in:
@@ -1,12 +0,0 @@
|
||||
// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package pulsarm
|
||||
|
||||
import "math"
|
||||
|
||||
// logFloat / expFloat are the math.Log / math.Exp pair, pulled
|
||||
// behind named indirection so luxround.go can stub them in unit
|
||||
// tests if needed.
|
||||
func logFloat(x float64) float64 { return math.Log(x) }
|
||||
func expFloat(x float64) float64 { return math.Exp(x) }
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
package pulsarm
|
||||
|
||||
// luxround.go -- Wave-driven metastable threshold signing.
|
||||
// round.go -- Wave-driven metastable threshold signing.
|
||||
//
|
||||
// For validator pools above the GF(257) Pulsar-M cap of n=256, the
|
||||
// canonical Lux deployment does NOT run one large Pulsar-M ceremony.
|
||||
@@ -27,13 +27,14 @@ package pulsarm
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"math"
|
||||
)
|
||||
|
||||
// LuxRoundContext is the per-Lux-round binding context. Each Wave
|
||||
// Tick produces a fresh context whose bytes are committed into every
|
||||
// RoundContext is the per-Lux-round binding context. Each Wave Tick
|
||||
// produces a fresh context whose bytes are committed into every
|
||||
// Pulsar-M transcript-hash so cross-round Round-1 commits cannot be
|
||||
// replayed against a different Wave round.
|
||||
type LuxRoundContext struct {
|
||||
type RoundContext struct {
|
||||
// Epoch is the validator-set epoch identifier (e.g. block height
|
||||
// divided by the epoch length).
|
||||
Epoch uint64
|
||||
@@ -48,10 +49,10 @@ type LuxRoundContext struct {
|
||||
CommitteeRoot [32]byte
|
||||
}
|
||||
|
||||
// Encode returns the canonical wire encoding of a LuxRoundContext.
|
||||
// The encoding is consumed by transcripts; changing the layout
|
||||
// Encode returns the canonical wire encoding of a RoundContext. The
|
||||
// encoding is consumed by transcripts; changing the layout
|
||||
// invalidates every KAT pinned at this version.
|
||||
func (c LuxRoundContext) Encode() []byte {
|
||||
func (c RoundContext) Encode() []byte {
|
||||
out := make([]byte, 0, 8+8+32+32)
|
||||
var u [8]byte
|
||||
binary.BigEndian.PutUint64(u[:], c.Epoch)
|
||||
@@ -63,24 +64,24 @@ func (c LuxRoundContext) Encode() []byte {
|
||||
return out
|
||||
}
|
||||
|
||||
// LuxRoundSessionID derives a deterministic per-Lux-round Pulsar-M
|
||||
// SessionID from a LuxRoundContext. The session-id binds the Pulsar-M
|
||||
// RoundSessionID derives a deterministic per-Lux-round Pulsar-M
|
||||
// SessionID from a RoundContext. The session-id binds the Pulsar-M
|
||||
// per-round PRNG (PULSAR-M-SIGN-PRNG-V1 customisation tag in
|
||||
// transcript.go) to the unique (epoch, round, item, committee) tuple,
|
||||
// closing the CRIT-1 replay vector that del Pino-Niot's PRNGKeyForRound
|
||||
// addresses in the small-committee path.
|
||||
func LuxRoundSessionID(ctx LuxRoundContext) [16]byte {
|
||||
func RoundSessionID(ctx RoundContext) [16]byte {
|
||||
digest := cshake256(ctx.Encode(), 16, tagSignPRNG)
|
||||
var out [16]byte
|
||||
copy(out[:], digest)
|
||||
return out
|
||||
}
|
||||
|
||||
// LuxRoundCommitteeRoot returns the canonical 32-byte digest of a
|
||||
// RoundCommitteeRoot returns the canonical 32-byte digest of a
|
||||
// Wave-sampled K-committee (sorted ascending NodeID). Both the
|
||||
// per-round Pulsar-M transcript and the Wave protocol's per-round
|
||||
// commitments bind to this digest.
|
||||
func LuxRoundCommitteeRoot(committee []NodeID) [32]byte {
|
||||
func RoundCommitteeRoot(committee []NodeID) [32]byte {
|
||||
sorted := make([]NodeID, len(committee))
|
||||
copy(sorted, committee)
|
||||
for i := 1; i < len(sorted); i++ {
|
||||
@@ -96,16 +97,16 @@ func LuxRoundCommitteeRoot(committee []NodeID) [32]byte {
|
||||
return transcriptHash32(tagDKGCommit, parts...)
|
||||
}
|
||||
|
||||
// LuxRoundSigShare is the per-validator Pulsar-M signature
|
||||
// contribution emitted in one Lux round. It rides alongside the
|
||||
// validator's Wave preference vote on the Photon wire. The
|
||||
// aggregator at the Quasar layer collects β rounds worth of these,
|
||||
// passes them to LargeCombine (or the small-committee Combine), and
|
||||
// emits one FIPS 204 ML-DSA signature that the P3Q rollup attests
|
||||
// to as part of the final block certificate.
|
||||
type LuxRoundSigShare struct {
|
||||
// RoundSigShare is the per-validator Pulsar-M signature contribution
|
||||
// emitted in one Lux round. It rides alongside the validator's Wave
|
||||
// preference vote on the Photon wire. The aggregator at the Quasar
|
||||
// layer collects β rounds worth of these, passes them to LargeCombine
|
||||
// (or the small-committee Combine), and emits one FIPS 204 ML-DSA
|
||||
// signature that the P3Q rollup attests to as part of the final
|
||||
// block certificate.
|
||||
type RoundSigShare struct {
|
||||
// Context binds the share to its Lux round.
|
||||
Context LuxRoundContext
|
||||
Context RoundContext
|
||||
// Round1 is the per-round Pulsar-M Round-1 commit message.
|
||||
Round1 *Round1Message
|
||||
// Round2 is the per-round Pulsar-M Round-2 reveal message.
|
||||
@@ -113,8 +114,8 @@ type LuxRoundSigShare struct {
|
||||
Round2 *Round2Message
|
||||
}
|
||||
|
||||
// LuxRoundQuorumPolicy bundles the (alpha, beta) parameters Wave uses
|
||||
// to drive Lux-round agreement. These are exposed here so the
|
||||
// RoundQuorumPolicy bundles the (alpha, beta) parameters Wave uses to
|
||||
// drive Lux-round agreement. These are exposed here so the
|
||||
// consensus-layer Wave-driver can synchronise them with the Pulsar-M
|
||||
// combiner's expectations.
|
||||
//
|
||||
@@ -123,18 +124,18 @@ type LuxRoundSigShare struct {
|
||||
// number of consecutive successful Lux rounds before the Focus
|
||||
// tracker fires "decided" -- at which point the aggregator combines
|
||||
// β·alpha Pulsar-M shares into one FIPS 204 signature.
|
||||
type LuxRoundQuorumPolicy struct {
|
||||
type RoundQuorumPolicy struct {
|
||||
K int
|
||||
Alpha int
|
||||
Beta int
|
||||
}
|
||||
|
||||
// DefaultLuxRoundQuorumPolicy matches consensus/protocol/quasar's
|
||||
// DefaultRoundQuorumPolicy matches consensus/protocol/quasar's
|
||||
// grouped-threshold defaults (DefaultGroupSize=3, DefaultGroupThreshold=2)
|
||||
// extended to a metastable K-sample of 21 (Wave's typical K) with a
|
||||
// 15-of-21 alpha and 12-round beta. These are starting points; the
|
||||
// Quasar config layer (config/pq_mode.go) is the source of truth.
|
||||
var DefaultLuxRoundQuorumPolicy = LuxRoundQuorumPolicy{
|
||||
var DefaultRoundQuorumPolicy = RoundQuorumPolicy{
|
||||
K: 21,
|
||||
Alpha: 15,
|
||||
Beta: 12,
|
||||
@@ -151,7 +152,7 @@ var DefaultLuxRoundQuorumPolicy = LuxRoundQuorumPolicy{
|
||||
// pool: Pr[X >= alpha when X ~ Bin(K, rho)]. We return the natural
|
||||
// logarithm to avoid floating-point precision loss; callers convert
|
||||
// to log2 as needed.
|
||||
func ApproxRoundSecurity(rho float64, policy LuxRoundQuorumPolicy) float64 {
|
||||
func ApproxRoundSecurity(rho float64, policy RoundQuorumPolicy) float64 {
|
||||
// Cumulative binomial Pr[X >= alpha; K, rho] via direct sum.
|
||||
logFactorial := func(n int) float64 {
|
||||
acc := 0.0
|
||||
@@ -178,9 +179,9 @@ func ApproxRoundSecurity(rho float64, policy LuxRoundQuorumPolicy) float64 {
|
||||
return sum
|
||||
}
|
||||
|
||||
// logf / expf are stdlib wrappers; pulled out so the file imports a
|
||||
// single math symbol (and so unit tests can stub them if needed).
|
||||
// logf / expf are stdlib wrappers; pulled behind named indirection so
|
||||
// unit tests can stub them if needed.
|
||||
var (
|
||||
logf = func(x float64) float64 { return logFloat(x) }
|
||||
expf = func(x float64) float64 { return expFloat(x) }
|
||||
logf = func(x float64) float64 { return math.Log(x) }
|
||||
expf = func(x float64) float64 { return math.Exp(x) }
|
||||
)
|
||||
@@ -9,12 +9,12 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestLuxRoundContext_Encode_Deterministic verifies the wire encoding
|
||||
// of a LuxRoundContext is deterministic and per-field unique. Changing
|
||||
// TestRoundContext_Encode_Deterministic verifies the wire encoding
|
||||
// of a RoundContext is deterministic and per-field unique. Changing
|
||||
// any field changes the encoding -- catches accidental field reorder
|
||||
// in future refactors.
|
||||
func TestLuxRoundContext_Encode_Deterministic(t *testing.T) {
|
||||
base := LuxRoundContext{
|
||||
func TestRoundContext_Encode_Deterministic(t *testing.T) {
|
||||
base := RoundContext{
|
||||
Epoch: 1,
|
||||
Round: 2,
|
||||
Item: [32]byte{0xab},
|
||||
@@ -26,11 +26,11 @@ func TestLuxRoundContext_Encode_Deterministic(t *testing.T) {
|
||||
t.Fatalf("Encode is non-deterministic")
|
||||
}
|
||||
// Mutate each field; each mutation must change the encoding.
|
||||
for i, mutate := range []func(*LuxRoundContext){
|
||||
func(c *LuxRoundContext) { c.Epoch++ },
|
||||
func(c *LuxRoundContext) { c.Round++ },
|
||||
func(c *LuxRoundContext) { c.Item[0] = 0xff },
|
||||
func(c *LuxRoundContext) { c.CommitteeRoot[0] = 0xff },
|
||||
for i, mutate := range []func(*RoundContext){
|
||||
func(c *RoundContext) { c.Epoch++ },
|
||||
func(c *RoundContext) { c.Round++ },
|
||||
func(c *RoundContext) { c.Item[0] = 0xff },
|
||||
func(c *RoundContext) { c.CommitteeRoot[0] = 0xff },
|
||||
} {
|
||||
mutated := base
|
||||
mutate(&mutated)
|
||||
@@ -40,31 +40,31 @@ func TestLuxRoundContext_Encode_Deterministic(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestLuxRoundSessionID_PerRoundDistinct verifies that two consecutive
|
||||
// TestRoundSessionID_PerRoundDistinct verifies that two consecutive
|
||||
// Lux rounds get distinct session IDs even when item + committee
|
||||
// match -- so the Pulsar-M PRNG is forced to re-seed every round
|
||||
// (the CRIT-1 cross-round replay defense from del Pino-Niot).
|
||||
func TestLuxRoundSessionID_PerRoundDistinct(t *testing.T) {
|
||||
ctxR1 := LuxRoundContext{Epoch: 7, Round: 1, Item: [32]byte{0x11}, CommitteeRoot: [32]byte{0x22}}
|
||||
func TestRoundSessionID_PerRoundDistinct(t *testing.T) {
|
||||
ctxR1 := RoundContext{Epoch: 7, Round: 1, Item: [32]byte{0x11}, CommitteeRoot: [32]byte{0x22}}
|
||||
ctxR2 := ctxR1
|
||||
ctxR2.Round = 2
|
||||
if LuxRoundSessionID(ctxR1) == LuxRoundSessionID(ctxR2) {
|
||||
if RoundSessionID(ctxR1) == RoundSessionID(ctxR2) {
|
||||
t.Fatalf("session IDs collide across Lux rounds")
|
||||
}
|
||||
}
|
||||
|
||||
// TestLuxRoundCommitteeRoot_OrderIndependent verifies the committee
|
||||
// TestRoundCommitteeRoot_OrderIndependent verifies the committee
|
||||
// root is canonical under permutation -- Wave's K-sample order is
|
||||
// not stable, so the root must be too.
|
||||
func TestLuxRoundCommitteeRoot_OrderIndependent(t *testing.T) {
|
||||
func TestRoundCommitteeRoot_OrderIndependent(t *testing.T) {
|
||||
a := []NodeID{{0x01}, {0x02}, {0x03}}
|
||||
b := []NodeID{{0x03}, {0x01}, {0x02}}
|
||||
if LuxRoundCommitteeRoot(a) != LuxRoundCommitteeRoot(b) {
|
||||
if RoundCommitteeRoot(a) != RoundCommitteeRoot(b) {
|
||||
t.Fatalf("committee root depends on order")
|
||||
}
|
||||
}
|
||||
|
||||
// TestLuxRound_E2E_BetaRounds_Pulsar_M is the headline metastable
|
||||
// TestRound_E2E_BetaRounds_Pulsar_M is the headline metastable
|
||||
// signing test. It simulates:
|
||||
// 1. Validator pool of 100 (beyond GF(257) cap not needed; tests math).
|
||||
// 2. β = 4 consecutive Lux rounds.
|
||||
@@ -77,7 +77,7 @@ func TestLuxRoundCommitteeRoot_OrderIndependent(t *testing.T) {
|
||||
// the Wave / Focus / Cut wiring (those live in consensus). The
|
||||
// test asserts that each per-Lux-round signature verifies under
|
||||
// unmodified FIPS 204 ML-DSA.Verify.
|
||||
func TestLuxRound_E2E_BetaRounds_Pulsar_M(t *testing.T) {
|
||||
func TestRound_E2E_BetaRounds_Pulsar_M(t *testing.T) {
|
||||
const validatorPool = 100
|
||||
const K = 3
|
||||
const T = 2
|
||||
@@ -102,14 +102,14 @@ func TestLuxRound_E2E_BetaRounds_Pulsar_M(t *testing.T) {
|
||||
for i, idx := range idxs {
|
||||
committee[i] = pool[idx]
|
||||
}
|
||||
committeeRoot := LuxRoundCommitteeRoot(committee)
|
||||
ctx := LuxRoundContext{
|
||||
committeeRoot := RoundCommitteeRoot(committee)
|
||||
ctx := RoundContext{
|
||||
Epoch: 1,
|
||||
Round: round,
|
||||
Item: item,
|
||||
CommitteeRoot: committeeRoot,
|
||||
}
|
||||
sessionID := LuxRoundSessionID(ctx)
|
||||
sessionID := RoundSessionID(ctx)
|
||||
|
||||
// Per-round DKG.
|
||||
dkg := make([]*DKGSession, K)
|
||||
@@ -202,7 +202,7 @@ func TestLuxRound_E2E_BetaRounds_Pulsar_M(t *testing.T) {
|
||||
// 2. rho=1 gives 1 (adversary controls everything).
|
||||
// 3. monotonically increasing in rho.
|
||||
func TestApproxRoundSecurity_BoundsMatchExpectedShape(t *testing.T) {
|
||||
policy := DefaultLuxRoundQuorumPolicy // K=21, alpha=15, beta=12.
|
||||
policy := DefaultRoundQuorumPolicy // K=21, alpha=15, beta=12.
|
||||
if got := ApproxRoundSecurity(0.0, policy); got != 0 {
|
||||
t.Fatalf("rho=0: got %v, want 0", got)
|
||||
}
|
||||
@@ -228,7 +228,7 @@ func TestApproxRoundSecurity_BoundsMatchExpectedShape(t *testing.T) {
|
||||
// compounds to (4e-4)^12 ≈ 2^-135 -- ample security for finality.
|
||||
// See proofs/pulsar-m/lux-round-metastable.tex for the full claim.
|
||||
func TestApproxRoundSecurity_DefaultPolicyAt1OfThird(t *testing.T) {
|
||||
v := ApproxRoundSecurity(1.0/3.0, DefaultLuxRoundQuorumPolicy)
|
||||
v := ApproxRoundSecurity(1.0/3.0, DefaultRoundQuorumPolicy)
|
||||
if v >= math.Pow(2, -10) {
|
||||
t.Fatalf("at rho=1/3, K=21, alpha=15: per-round adv %v too high (>= 2^-10)", v)
|
||||
}
|
||||
@@ -236,7 +236,7 @@ func TestApproxRoundSecurity_DefaultPolicyAt1OfThird(t *testing.T) {
|
||||
t.Fatalf("at rho=1/3, K=21, alpha=15: per-round adv %v unexpectedly low (<= 2^-20)", v)
|
||||
}
|
||||
// Amplified bound after beta=12 rounds (assuming independent rounds).
|
||||
amplified := math.Pow(v, float64(DefaultLuxRoundQuorumPolicy.Beta))
|
||||
amplified := math.Pow(v, float64(DefaultRoundQuorumPolicy.Beta))
|
||||
if amplified >= math.Pow(2, -100) {
|
||||
t.Fatalf("beta=12 amplified bound %v too high (>= 2^-100)", amplified)
|
||||
}
|
||||
Reference in New Issue
Block a user