mirror of
https://github.com/luxfi/pulsar.git
synced 2026-09-24 00:25:52 +00:00
pulsar: decomplect names to domain (NonceCert/Partial/Aggregate/ConsensusCert)
Rename mitigation-named public types to domain names (BCC* dropped; boundary clearance / carry elimination kept as implementation profile in docs/spec): BoundaryNonceCert -> NonceCert NonceCertVote -> NonceVote NonceMPCTranscript -> NonceTranscript MPCTranscriptRoot -> NonceTranscriptRoot BCCZPartial -> Partial ZAggregate -> Aggregate BCCSignature -> Signature (folded into existing type) ConsensusCert (kept) Files: threshold_bcc*.go/nonce_mpc.go/z_partial.go/tree_aggregate.go/ dkg_wellformed.go -> boundary.go/consensus.go/proof.go/nonce.go/partial.go/ aggregate.go/dkg_wellformed.go (+tests). Recover clobbered dkg.go from HEAD. All tests green, vet clean.
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
package pulsar
|
||||
|
||||
import "errors"
|
||||
|
||||
// Tree aggregation for ~1000-validator committees. Tree nodes carry only
|
||||
// z-sums, bitmaps, and proof roots — never hint material, w, or residual.
|
||||
|
||||
var (
|
||||
ErrDuplicateSigner = errors.New("pulsar: duplicate signer in tree aggregate")
|
||||
ErrWrongSessionAgg = errors.New("pulsar: aggregate session/nonce mismatch")
|
||||
)
|
||||
|
||||
func bitmapsOverlap(a, b []byte) bool {
|
||||
n := len(a)
|
||||
if len(b) < n {
|
||||
n = len(b)
|
||||
}
|
||||
for i := 0; i < n; i++ {
|
||||
if a[i]&b[i] != 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func bitmapUnion(a, b []byte) []byte {
|
||||
n := len(a)
|
||||
if len(b) > n {
|
||||
n = len(b)
|
||||
}
|
||||
out := make([]byte, n)
|
||||
for i := 0; i < n; i++ {
|
||||
var x, y byte
|
||||
if i < len(a) {
|
||||
x = a[i]
|
||||
}
|
||||
if i < len(b) {
|
||||
y = b[i]
|
||||
}
|
||||
out[i] = x | y
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// MergeAggregates merges child Aggregates: identical session+nonce, DISJOINT
|
||||
// bitmaps (no duplicate signer), z summed mod q.
|
||||
func MergeAggregates(children []Aggregate, l int) (Aggregate, error) {
|
||||
if len(children) == 0 {
|
||||
return Aggregate{}, nil
|
||||
}
|
||||
sid, nid := children[0].SessionID, children[0].NonceID
|
||||
z := make(polyVec, l)
|
||||
var bitmap []byte
|
||||
for _, ch := range children {
|
||||
if ch.SessionID != sid || ch.NonceID != nid {
|
||||
return Aggregate{}, ErrWrongSessionAgg
|
||||
}
|
||||
if bitmapsOverlap(bitmap, ch.SignerBitmap) {
|
||||
return Aggregate{}, ErrDuplicateSigner
|
||||
}
|
||||
bitmap = bitmapUnion(bitmap, ch.SignerBitmap)
|
||||
sumZ(z, unpackPolyVec(ch.ZSum, l), l)
|
||||
}
|
||||
return Aggregate{SessionID: sid, NonceID: nid, SignerBitmap: bitmap, ZSum: packPolyVec(z)}, nil
|
||||
}
|
||||
@@ -15,14 +15,14 @@ import (
|
||||
|
||||
// ---- Wire types (no hint-secret fields, no full w) ----
|
||||
|
||||
// BoundaryNonceCert is a boundary-cleared nonce certificate. It carries only
|
||||
// NonceCert is a boundary-cleared nonce certificate. It carries only
|
||||
// w1 = HighBits(w) (public, used in the challenge), a hiding commitment to w,
|
||||
// and a validator-run NonceMPC clearance certificate (MPCTranscriptRoot +
|
||||
// and a validator-run NonceMPC clearance certificate (NonceTranscriptRoot +
|
||||
// ClearanceQC) — NEVER full w, w_i shares, LowBits(w), boundary distances, or
|
||||
// hint-secret material (PULSAR-V13-W-LEAK). The production clearance backend
|
||||
// is the quorum-certified validator transcript; ClearanceProof is retained
|
||||
// only for an optional pluggable ZK backend.
|
||||
type BoundaryNonceCert struct {
|
||||
type NonceCert struct {
|
||||
NonceID [32]byte
|
||||
PKEpoch uint64
|
||||
CommitteeID [32]byte
|
||||
@@ -32,14 +32,14 @@ type BoundaryNonceCert struct {
|
||||
Margin uint32
|
||||
RegionRoot [32]byte
|
||||
CommitRoot [32]byte
|
||||
MPCTranscriptRoot [32]byte // root of the validator NonceMPC transcript
|
||||
NonceTranscriptRoot [32]byte // root of the validator NonceMPC transcript
|
||||
ClearanceQC QuorumCert // quorum of validators that verified the transcript
|
||||
ClearanceProof []byte // optional pluggable ZK backend (unused for the NonceMPC path)
|
||||
Consumed bool
|
||||
}
|
||||
|
||||
// BCCZPartial is a proof-carrying z-share. No CS2/CT0/r0/LowBits/hint fields.
|
||||
type BCCZPartial struct {
|
||||
// Partial is a proof-carrying z-share. No CS2/CT0/r0/LowBits/hint fields.
|
||||
type Partial struct {
|
||||
PartyID uint32
|
||||
NonceID [32]byte
|
||||
SessionID [32]byte
|
||||
@@ -48,12 +48,6 @@ type BCCZPartial struct {
|
||||
MAC []byte
|
||||
}
|
||||
|
||||
// BCCSignature is a final, unmodified FIPS 204 ML-DSA signature.
|
||||
type BCCSignature struct {
|
||||
Mode Mode
|
||||
Bytes []byte // sigEncode(c, z, h)
|
||||
}
|
||||
|
||||
// ConsensusCert is the two-certificate consensus artifact: the ML-DSA
|
||||
// signature proves the joint key signed; the bitmap + transcript root prove
|
||||
// which validators participated (BLS-like accountability).
|
||||
@@ -65,7 +59,7 @@ type ConsensusCert struct {
|
||||
JointPKID [32]byte
|
||||
SignerBitmap []byte
|
||||
TranscriptRoot [32]byte
|
||||
Signature BCCSignature
|
||||
Signature Signature
|
||||
}
|
||||
|
||||
// ---- Canonical, non-grindable nonce selection ----
|
||||
@@ -197,13 +191,12 @@ func (c *ConsensusCert) VerifyStructure(quorum, validatorSetSize int) error {
|
||||
|
||||
func productionWireTypes() []reflect.Type {
|
||||
return []reflect.Type{
|
||||
reflect.TypeOf(BoundaryNonceCert{}),
|
||||
reflect.TypeOf(BCCZPartial{}),
|
||||
reflect.TypeOf(BCCSignature{}),
|
||||
reflect.TypeOf(NonceCert{}),
|
||||
reflect.TypeOf(Partial{}),
|
||||
reflect.TypeOf(ConsensusCert{}),
|
||||
reflect.TypeOf(QuorumCert{}),
|
||||
reflect.TypeOf(NonceCertVote{}),
|
||||
reflect.TypeOf(ZAggregate{}),
|
||||
reflect.TypeOf(NonceVote{}),
|
||||
reflect.TypeOf(Aggregate{}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package pulsar
|
||||
|
||||
import "reflect"
|
||||
|
||||
// DKG public output: joint public key (t1) + share commitments + transcript
|
||||
// root + well-formedness QC. It NEVER carries t0, s2 hint material, the full
|
||||
// secret t, or any master secret (PULSAR-V13). Online signing needs only
|
||||
// s1_i, y_i (held locally) and public t1.
|
||||
|
||||
type DKGShareCommitment struct {
|
||||
PartyID uint32
|
||||
Commit []byte
|
||||
}
|
||||
|
||||
type DKGPublicOutput struct {
|
||||
PKEpoch uint64
|
||||
JointPublicKey []byte // packed ML-DSA public key (rho + t1)
|
||||
ShareCommitments []DKGShareCommitment
|
||||
DKGTranscriptRoot [32]byte
|
||||
WellFormednessQC QuorumCert
|
||||
}
|
||||
|
||||
func productionDKGTypes() []reflect.Type {
|
||||
return []reflect.Type{
|
||||
reflect.TypeOf(DKGPublicOutput{}),
|
||||
reflect.TypeOf(DKGShareCommitment{}),
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
|
||||
// NonceMPC is a validator-run consensus subprotocol (not a separate service):
|
||||
// validators jointly compute w1 = HighBits(w) and BoundaryClear(w, 2β) over
|
||||
// their hidden w = A·y shares and sign a NonceCertVote; a quorum forms the
|
||||
// their hidden w = A·y shares and sign a NonceVote; a quorum forms the
|
||||
// ClearanceQC. Full w is never opened. Public chain verifiers check the QC,
|
||||
// never the hidden w (PULSAR-V13-W-LEAK).
|
||||
|
||||
@@ -26,9 +26,9 @@ func (qc QuorumCert) IsEmpty() bool {
|
||||
return len(qc.SignerBitmap) == 0 && len(qc.Signatures) == 0 && qc.PayloadRoot == [32]byte{}
|
||||
}
|
||||
|
||||
// NonceCertVote is one validator's signed attestation that the NonceMPC
|
||||
// NonceVote is one validator's signed attestation that the NonceMPC
|
||||
// transcript proves a boundary-clear hidden w with the given w1.
|
||||
type NonceCertVote struct {
|
||||
type NonceVote struct {
|
||||
Epoch uint64
|
||||
CommitteeID [32]byte
|
||||
NonceID [32]byte
|
||||
@@ -36,12 +36,12 @@ type NonceCertVote struct {
|
||||
Margin uint32
|
||||
CommitRoot [32]byte
|
||||
RegionRoot [32]byte
|
||||
MPCTranscriptRoot [32]byte
|
||||
NonceTranscriptRoot [32]byte
|
||||
Signature []byte
|
||||
}
|
||||
|
||||
// ZAggregate is a tree-aggregation node: z-sums + bitmaps + proof roots only.
|
||||
type ZAggregate struct {
|
||||
// Aggregate is a tree-aggregation node: z-sums + bitmaps + proof roots only.
|
||||
type Aggregate struct {
|
||||
SessionID [32]byte
|
||||
NonceID [32]byte
|
||||
SignerBitmap []byte
|
||||
@@ -62,7 +62,7 @@ var (
|
||||
// nonce cert. Any mutation changes the root, so a QC over the old root no
|
||||
// longer matches — the cert is tamper-evident. (Full w is NOT bound; it is
|
||||
// never available to public verifiers.)
|
||||
func nonceCertPayloadRoot(cert *BoundaryNonceCert) [32]byte {
|
||||
func nonceCertPayloadRoot(cert *NonceCert) [32]byte {
|
||||
h := sha3.NewShake256()
|
||||
_, _ = h.Write([]byte("PULSAR-BCC-CEF/nonce-cert/v1"))
|
||||
_, _ = h.Write(cert.NonceID[:])
|
||||
@@ -76,18 +76,18 @@ func nonceCertPayloadRoot(cert *BoundaryNonceCert) [32]byte {
|
||||
_, _ = h.Write(u[:4])
|
||||
_, _ = h.Write(cert.CommitRoot[:])
|
||||
_, _ = h.Write(cert.RegionRoot[:])
|
||||
_, _ = h.Write(cert.MPCTranscriptRoot[:])
|
||||
_, _ = h.Write(cert.NonceTranscriptRoot[:])
|
||||
var out [32]byte
|
||||
_, _ = h.Read(out[:])
|
||||
return out
|
||||
}
|
||||
|
||||
// VerifyBoundaryNonceCert performs the structural consensus check: the
|
||||
// VerifyNonceCert performs the structural consensus check: the
|
||||
// clearance QC is present, binds the cert payload, meets quorum, and selects
|
||||
// only validators. The per-validator QC signatures are verified by the
|
||||
// consensus layer's registered validator-set verifier (out of this module's
|
||||
// structural scope). Without a valid QC there is no signing — fail closed.
|
||||
func VerifyBoundaryNonceCert(cert *BoundaryNonceCert, quorum, validatorSetSize int) error {
|
||||
func VerifyNonceCert(cert *NonceCert, quorum, validatorSetSize int) error {
|
||||
if cert.ClearanceQC.IsEmpty() {
|
||||
return ErrMissingClearanceQC
|
||||
}
|
||||
@@ -107,9 +107,9 @@ func VerifyBoundaryNonceCert(cert *BoundaryNonceCert, quorum, validatorSetSize i
|
||||
|
||||
// ---- NonceMPC transcript + voting (debug-oracle compute path) ----
|
||||
|
||||
// NonceMPCTranscript models the validator NonceMPC output. debugFullW is
|
||||
// NonceTranscript models the validator NonceMPC output. debugFullW is
|
||||
// TEST-ONLY and never enters the public view or the transcript root.
|
||||
type NonceMPCTranscript struct {
|
||||
type NonceTranscript struct {
|
||||
debugFullW polyVec // DEBUG ONLY — never serialized, never bound
|
||||
Epoch uint64
|
||||
CommitteeID [32]byte
|
||||
@@ -122,7 +122,7 @@ type NonceMPCTranscript struct {
|
||||
}
|
||||
|
||||
// Root binds only the public outputs (w1, clear, margin, roots) — never w.
|
||||
func (tr *NonceMPCTranscript) Root() [32]byte {
|
||||
func (tr *NonceTranscript) Root() [32]byte {
|
||||
h := sha3.NewShake256()
|
||||
_, _ = h.Write([]byte("PULSAR-BCC-CEF/nonce-mpc/v1"))
|
||||
_, _ = h.Write(tr.NonceID[:])
|
||||
@@ -144,7 +144,7 @@ func (tr *NonceMPCTranscript) Root() [32]byte {
|
||||
|
||||
// PublicView returns only the public outputs (w1 + clear + root). It NEVER
|
||||
// contains full w or its low bits.
|
||||
func (tr *NonceMPCTranscript) PublicView() []byte {
|
||||
func (tr *NonceTranscript) PublicView() []byte {
|
||||
out := append([]byte{}, tr.W1...)
|
||||
if tr.Clear {
|
||||
out = append(out, 1)
|
||||
@@ -160,22 +160,22 @@ func (tr *NonceMPCTranscript) PublicView() []byte {
|
||||
// validator MPC would, producing a cert with a quorum-signed bound payload.
|
||||
// The public view never reveals w. (A production NonceMPC replaces the direct
|
||||
// w with secret-shared MPC; the public API is identical.)
|
||||
func RunNonceMPCDebug(w polyVec, mode Mode, nonceID [32]byte) (*BoundaryNonceCert, *NonceMPCTranscript) {
|
||||
func RunNonceMPCDebug(w polyVec, mode Mode, nonceID [32]byte) (*NonceCert, *NonceTranscript) {
|
||||
gamma2, beta, _, _ := bccParams(mode)
|
||||
tr := &NonceMPCTranscript{
|
||||
tr := &NonceTranscript{
|
||||
debugFullW: w,
|
||||
NonceID: nonceID,
|
||||
W1: packPolyVec(highBitsVec(w, gamma2)),
|
||||
Margin: 2 * beta,
|
||||
Clear: BoundaryClear(w, gamma2, beta),
|
||||
}
|
||||
cert := &BoundaryNonceCert{
|
||||
cert := &NonceCert{
|
||||
NonceID: nonceID,
|
||||
W1: tr.W1,
|
||||
Margin: tr.Margin,
|
||||
CommitRoot: tr.CommitRoot,
|
||||
RegionRoot: tr.RegionRoot,
|
||||
MPCTranscriptRoot: tr.Root(),
|
||||
NonceTranscriptRoot: tr.Root(),
|
||||
}
|
||||
payload := nonceCertPayloadRoot(cert)
|
||||
cert.ClearanceQC = QuorumCert{
|
||||
@@ -189,14 +189,14 @@ func RunNonceMPCDebug(w polyVec, mode Mode, nonceID [32]byte) (*BoundaryNonceCer
|
||||
// ValidateAndVoteNonceCert is the validator voting rule: refuse to vote unless
|
||||
// the transcript outputs ONLY w1 + a clear bit (never full w) and the nonce is
|
||||
// boundary-clear.
|
||||
func ValidateAndVoteNonceCert(tr *NonceMPCTranscript) (*NonceCertVote, error) {
|
||||
func ValidateAndVoteNonceCert(tr *NonceTranscript) (*NonceVote, error) {
|
||||
if len(tr.W1) == 0 {
|
||||
return nil, ErrBadNonceMPCOutput
|
||||
}
|
||||
if !tr.Clear {
|
||||
return nil, ErrNonceNotBoundaryClear
|
||||
}
|
||||
return &NonceCertVote{
|
||||
return &NonceVote{
|
||||
Epoch: tr.Epoch,
|
||||
CommitteeID: tr.CommitteeID,
|
||||
NonceID: tr.NonceID,
|
||||
@@ -204,7 +204,7 @@ func ValidateAndVoteNonceCert(tr *NonceMPCTranscript) (*NonceCertVote, error) {
|
||||
Margin: tr.Margin,
|
||||
CommitRoot: tr.CommitRoot,
|
||||
RegionRoot: tr.RegionRoot,
|
||||
MPCTranscriptRoot: tr.Root(),
|
||||
NonceTranscriptRoot: tr.Root(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -26,54 +26,54 @@ func boundaryClearW(rng *rand.Rand, k int, gamma2, beta uint32) polyVec {
|
||||
}
|
||||
|
||||
// The production cert carries w1 + clearance QC, never full w.
|
||||
func TestBoundaryNonceCertHasNoFullW(t *testing.T) {
|
||||
typ := reflect.TypeOf(BoundaryNonceCert{})
|
||||
func TestNonceCertHasNoFullW(t *testing.T) {
|
||||
typ := reflect.TypeOf(NonceCert{})
|
||||
for _, f := range []string{"W", "FullW", "WPolyVec", "LowBitsW", "BoundaryDistance", "R0", "CS2", "CT0", "D2", "D0"} {
|
||||
if hasFieldNamed(typ, f) {
|
||||
t.Fatalf("BoundaryNonceCert contains forbidden field %q", f)
|
||||
t.Fatalf("NonceCert contains forbidden field %q", f)
|
||||
}
|
||||
}
|
||||
for _, f := range []string{"W1", "CommitRoot", "RegionRoot", "MPCTranscriptRoot", "ClearanceQC"} {
|
||||
for _, f := range []string{"W1", "CommitRoot", "RegionRoot", "NonceTranscriptRoot", "ClearanceQC"} {
|
||||
if !hasFieldNamed(typ, f) {
|
||||
t.Fatalf("BoundaryNonceCert missing required field %q", f)
|
||||
t.Fatalf("NonceCert missing required field %q", f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBoundaryNonceCertRequiresClearanceQC(t *testing.T) {
|
||||
func TestNonceCertRequiresClearanceQC(t *testing.T) {
|
||||
gamma2, beta, _, _ := bccParams(ModeP65)
|
||||
k, _, _ := modeShape(ModeP65)
|
||||
w := boundaryClearW(rand.New(rand.NewSource(11)), k, gamma2, beta)
|
||||
cert, _ := RunNonceMPCDebug(w, ModeP65, [32]byte{1})
|
||||
cert.ClearanceQC = QuorumCert{} // strip the QC
|
||||
if err := VerifyBoundaryNonceCert(cert, 5, 8); err != ErrMissingClearanceQC {
|
||||
if err := VerifyNonceCert(cert, 5, 8); err != ErrMissingClearanceQC {
|
||||
t.Fatalf("expected missing-QC, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBoundaryNonceCertBindsAllConsensusFields(t *testing.T) {
|
||||
func TestNonceCertBindsAllConsensusFields(t *testing.T) {
|
||||
gamma2, beta, _, _ := bccParams(ModeP65)
|
||||
k, _, _ := modeShape(ModeP65)
|
||||
w := boundaryClearW(rand.New(rand.NewSource(12)), k, gamma2, beta)
|
||||
base, _ := RunNonceMPCDebug(w, ModeP65, [32]byte{2})
|
||||
if err := VerifyBoundaryNonceCert(base, 5, 8); err != nil {
|
||||
if err := VerifyNonceCert(base, 5, 8); err != nil {
|
||||
t.Fatalf("base cert should verify, got %v", err)
|
||||
}
|
||||
mutations := []func(*BoundaryNonceCert){
|
||||
func(c *BoundaryNonceCert) { c.PKEpoch++ },
|
||||
func(c *BoundaryNonceCert) { c.CommitteeID[0] ^= 1 },
|
||||
func(c *BoundaryNonceCert) { c.SignerSetRoot[0] ^= 1 },
|
||||
func(c *BoundaryNonceCert) { c.CommitRoot[0] ^= 1 },
|
||||
func(c *BoundaryNonceCert) { c.RegionRoot[0] ^= 1 },
|
||||
func(c *BoundaryNonceCert) { c.MPCTranscriptRoot[0] ^= 1 },
|
||||
func(c *BoundaryNonceCert) { c.Margin++ },
|
||||
func(c *BoundaryNonceCert) { c.W1[0] ^= 1 },
|
||||
mutations := []func(*NonceCert){
|
||||
func(c *NonceCert) { c.PKEpoch++ },
|
||||
func(c *NonceCert) { c.CommitteeID[0] ^= 1 },
|
||||
func(c *NonceCert) { c.SignerSetRoot[0] ^= 1 },
|
||||
func(c *NonceCert) { c.CommitRoot[0] ^= 1 },
|
||||
func(c *NonceCert) { c.RegionRoot[0] ^= 1 },
|
||||
func(c *NonceCert) { c.NonceTranscriptRoot[0] ^= 1 },
|
||||
func(c *NonceCert) { c.Margin++ },
|
||||
func(c *NonceCert) { c.W1[0] ^= 1 },
|
||||
}
|
||||
for i, mut := range mutations {
|
||||
bad := *base
|
||||
bad.W1 = append([]byte{}, base.W1...)
|
||||
mut(&bad)
|
||||
if err := VerifyBoundaryNonceCert(&bad, 5, 8); err == nil {
|
||||
if err := VerifyNonceCert(&bad, 5, 8); err == nil {
|
||||
t.Fatalf("mutation %d not detected (QC must bind all fields)", i)
|
||||
}
|
||||
}
|
||||
@@ -142,18 +142,18 @@ func TestNonceCertQCVerifies(t *testing.T) {
|
||||
k, _, _ := modeShape(ModeP65)
|
||||
w := boundaryClearW(rand.New(rand.NewSource(16)), k, gamma2, beta)
|
||||
cert, _ := RunNonceMPCDebug(w, ModeP65, [32]byte{7})
|
||||
if err := VerifyBoundaryNonceCert(cert, 5, 8); err != nil {
|
||||
if err := VerifyNonceCert(cert, 5, 8); err != nil {
|
||||
t.Fatalf("valid NonceMPC cert should verify, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBadNonceMPCTranscriptRootRejected(t *testing.T) {
|
||||
func TestBadNonceTranscriptRootRejected(t *testing.T) {
|
||||
gamma2, beta, _, _ := bccParams(ModeP65)
|
||||
k, _, _ := modeShape(ModeP65)
|
||||
w := boundaryClearW(rand.New(rand.NewSource(17)), k, gamma2, beta)
|
||||
cert, _ := RunNonceMPCDebug(w, ModeP65, [32]byte{8})
|
||||
cert.MPCTranscriptRoot[0] ^= 1 // tamper → payload root no longer matches the QC
|
||||
if err := VerifyBoundaryNonceCert(cert, 5, 8); err != ErrBadClearanceQC {
|
||||
cert.NonceTranscriptRoot[0] ^= 1 // tamper → payload root no longer matches the QC
|
||||
if err := VerifyNonceCert(cert, 5, 8); err != ErrBadClearanceQC {
|
||||
t.Fatalf("tampered transcript root must be rejected, got %v", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package pulsar
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
)
|
||||
|
||||
// Partial-z correctness (PULSAR-V13-PARTIAL-Z-PROOF). Each z-share proves
|
||||
// z_i = λ_i·y_i + c·λ_i·s1_i bound to (session, nonce, party, challenge, DKG
|
||||
// share commitment, nonce commitment) WITHOUT revealing y_i or s1_i. The
|
||||
// public bindings below are enforced unconditionally; the zero-knowledge
|
||||
// correctness check is delegated to the registered (fail-closed)
|
||||
// PartialZVerifier. Do not tree-aggregate until a sound verifier is registered.
|
||||
|
||||
type ZPartialPublicInput struct {
|
||||
PartyID uint32
|
||||
Lambda []byte
|
||||
Challenge []byte
|
||||
SessionID [32]byte
|
||||
NonceID [32]byte
|
||||
DKGCommitment []byte
|
||||
NonceCommitment []byte
|
||||
ZShare []byte
|
||||
}
|
||||
|
||||
var (
|
||||
ErrWrongParty = errors.New("pulsar: z-partial party mismatch")
|
||||
ErrWrongSession = errors.New("pulsar: z-partial session mismatch")
|
||||
ErrWrongNonce = errors.New("pulsar: z-partial nonce mismatch")
|
||||
ErrBadZPartialProof = errors.New("pulsar: z-partial correctness proof failed")
|
||||
)
|
||||
|
||||
// VerifyZPartial enforces the public bindings, then delegates the ZK
|
||||
// correctness check to the registered PartialZVerifier (fail-closed default).
|
||||
func VerifyZPartial(p *Partial, in ZPartialPublicInput) error {
|
||||
if p.PartyID != in.PartyID {
|
||||
return ErrWrongParty
|
||||
}
|
||||
if p.SessionID != in.SessionID {
|
||||
return ErrWrongSession
|
||||
}
|
||||
if p.NonceID != in.NonceID {
|
||||
return ErrWrongNonce
|
||||
}
|
||||
if !bytes.Equal(p.ZShare, in.ZShare) {
|
||||
return ErrBadZPartialProof
|
||||
}
|
||||
return registeredPartialZVerifier.VerifyPartial(p, in.Challenge, in.DKGCommitment, in.NonceCommitment)
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package pulsar
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"math/rand"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type acceptAllPartialZ struct{}
|
||||
|
||||
func (acceptAllPartialZ) VerifyPartial(*Partial, []byte, []byte, []byte) error { return nil }
|
||||
|
||||
func validZPartial() (*Partial, ZPartialPublicInput) {
|
||||
z := []byte{1, 2, 3, 4}
|
||||
p := &Partial{PartyID: 3, SessionID: [32]byte{9}, NonceID: [32]byte{7}, ZShare: z}
|
||||
in := ZPartialPublicInput{PartyID: 3, SessionID: [32]byte{9}, NonceID: [32]byte{7},
|
||||
Challenge: []byte{0xc}, DKGCommitment: []byte{0xd}, NonceCommitment: []byte{0xe}, ZShare: z}
|
||||
return p, in
|
||||
}
|
||||
|
||||
func TestValidZPartialProofAccepted(t *testing.T) {
|
||||
old := registeredPartialZVerifier
|
||||
RegisterPartialZVerifier(acceptAllPartialZ{})
|
||||
defer func() { registeredPartialZVerifier = old }()
|
||||
p, in := validZPartial()
|
||||
if err := VerifyZPartial(p, in); err != nil {
|
||||
t.Fatalf("valid partial rejected: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBadZShareRejected(t *testing.T) {
|
||||
old := registeredPartialZVerifier
|
||||
RegisterPartialZVerifier(acceptAllPartialZ{})
|
||||
defer func() { registeredPartialZVerifier = old }()
|
||||
p, in := validZPartial()
|
||||
p.ZShare = []byte{9, 9, 9, 9} // corrupted
|
||||
if err := VerifyZPartial(p, in); err != ErrBadZPartialProof {
|
||||
t.Fatalf("corrupt z-share must be rejected, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestZPartialBindsSessionNoncePartyChallenge(t *testing.T) {
|
||||
old := registeredPartialZVerifier
|
||||
RegisterPartialZVerifier(acceptAllPartialZ{})
|
||||
defer func() { registeredPartialZVerifier = old }()
|
||||
for i, mut := range []func(*ZPartialPublicInput){
|
||||
func(in *ZPartialPublicInput) { in.SessionID[0] ^= 1 },
|
||||
func(in *ZPartialPublicInput) { in.NonceID[0] ^= 1 },
|
||||
func(in *ZPartialPublicInput) { in.PartyID++ },
|
||||
} {
|
||||
p, in := validZPartial()
|
||||
mut(&in)
|
||||
if err := VerifyZPartial(p, in); err == nil {
|
||||
t.Fatalf("binding mutation %d not detected", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPartialZFailClosedByDefault(t *testing.T) {
|
||||
p, in := validZPartial()
|
||||
if err := VerifyZPartial(p, in); err != ErrPartialZProofUnsound {
|
||||
t.Fatalf("default partial-z verifier must fail closed, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func aggWith(sid, nid [32]byte, bitmap []byte, z polyVec) Aggregate {
|
||||
return Aggregate{SessionID: sid, NonceID: nid, SignerBitmap: bitmap, ZSum: packPolyVec(z)}
|
||||
}
|
||||
|
||||
func TestMergeAggregatesDuplicateAndSession(t *testing.T) {
|
||||
_, l, _ := modeShape(ModeP65)
|
||||
rng := rand.New(rand.NewSource(21))
|
||||
sid, nid := [32]byte{1}, [32]byte{2}
|
||||
a := aggWith(sid, nid, []byte{0b0001}, randPolyVec(rng, l))
|
||||
b := aggWith(sid, nid, []byte{0b0010}, randPolyVec(rng, l))
|
||||
if _, err := MergeAggregates([]Aggregate{a, b}, l); err != nil {
|
||||
t.Fatalf("disjoint merge should succeed: %v", err)
|
||||
}
|
||||
dup := aggWith(sid, nid, []byte{0b0001}, randPolyVec(rng, l)) // overlaps a
|
||||
if _, err := MergeAggregates([]Aggregate{a, dup}, l); err != ErrDuplicateSigner {
|
||||
t.Fatalf("duplicate signer must be rejected, got %v", err)
|
||||
}
|
||||
wrong := aggWith([32]byte{9}, nid, []byte{0b0100}, randPolyVec(rng, l))
|
||||
if _, err := MergeAggregates([]Aggregate{a, wrong}, l); err != ErrWrongSessionAgg {
|
||||
t.Fatalf("wrong session must be rejected, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeAggregatesSumsZ(t *testing.T) {
|
||||
_, l, _ := modeShape(ModeP65)
|
||||
rng := rand.New(rand.NewSource(22))
|
||||
sid, nid := [32]byte{3}, [32]byte{4}
|
||||
z1, z2 := randPolyVec(rng, l), randPolyVec(rng, l)
|
||||
merged, err := MergeAggregates([]Aggregate{
|
||||
aggWith(sid, nid, []byte{0b0001}, z1),
|
||||
aggWith(sid, nid, []byte{0b0010}, z2),
|
||||
}, l)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := FlatAggregateZ([]polyVec{z1, z2}, l)
|
||||
if !bytes.Equal(merged.ZSum, packPolyVec(want)) {
|
||||
t.Fatal("merged ZSum != flat sum")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNoT0InProductionDKGTypes(t *testing.T) {
|
||||
for _, typ := range productionDKGTypes() {
|
||||
for _, f := range []string{"T0", "T0Share", "S2", "S2Share", "S2Correction", "FullT", "MasterSecret", "S1Master", "S2Master", "T0Master"} {
|
||||
if hasFieldNamed(typ, f) {
|
||||
t.Fatalf("%s has forbidden DKG field %q", typ.Name(), f)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -40,24 +40,24 @@ var (
|
||||
// ClearanceVerifier verifies a boundary-clearance proof on a nonce cert
|
||||
// without learning the hidden w.
|
||||
type ClearanceVerifier interface {
|
||||
VerifyClearance(cert *BoundaryNonceCert) error
|
||||
VerifyClearance(cert *NonceCert) error
|
||||
}
|
||||
|
||||
// PartialZVerifier verifies a z-partial's correctness proof without learning
|
||||
// y_i or s1_i.
|
||||
type PartialZVerifier interface {
|
||||
VerifyPartial(p *BCCZPartial, challenge []byte, dkgShareCommit, nonceCommit []byte) error
|
||||
VerifyPartial(p *Partial, challenge []byte, dkgShareCommit, nonceCommit []byte) error
|
||||
}
|
||||
|
||||
// failClosedClearance is the default: no sound proof exists yet.
|
||||
type failClosedClearance struct{}
|
||||
|
||||
func (failClosedClearance) VerifyClearance(*BoundaryNonceCert) error { return ErrClearanceProofUnsound }
|
||||
func (failClosedClearance) VerifyClearance(*NonceCert) error { return ErrClearanceProofUnsound }
|
||||
|
||||
// failClosedPartialZ is the default: no sound proof exists yet.
|
||||
type failClosedPartialZ struct{}
|
||||
|
||||
func (failClosedPartialZ) VerifyPartial(*BCCZPartial, []byte, []byte, []byte) error {
|
||||
func (failClosedPartialZ) VerifyPartial(*Partial, []byte, []byte, []byte) error {
|
||||
return ErrPartialZProofUnsound
|
||||
}
|
||||
|
||||
@@ -9,10 +9,10 @@ func TestProductionBCCSigningDisabledUntilSoundZK(t *testing.T) {
|
||||
if ProductionBCCSigningReady() {
|
||||
t.Fatal("production BCC signing must be disabled by default (no sound ZK proofs)")
|
||||
}
|
||||
if err := registeredClearanceVerifier.VerifyClearance(&BoundaryNonceCert{}); err != ErrClearanceProofUnsound {
|
||||
if err := registeredClearanceVerifier.VerifyClearance(&NonceCert{}); err != ErrClearanceProofUnsound {
|
||||
t.Fatalf("clearance verifier must fail closed, got %v", err)
|
||||
}
|
||||
if err := registeredPartialZVerifier.VerifyPartial(&BCCZPartial{}, nil, nil, nil); err != ErrPartialZProofUnsound {
|
||||
if err := registeredPartialZVerifier.VerifyPartial(&Partial{}, nil, nil, nil); err != ErrPartialZProofUnsound {
|
||||
t.Fatalf("partial-z verifier must fail closed, got %v", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user