mirror of
https://github.com/luxfi/corona.git
synced 2026-07-27 02:50:34 +00:00
Leftover from Corona's "Pulsar-R" lineage. Pulsar (M-LWE) and Corona
(R-LWE) are independent constructions with separate hardness assumptions,
so their cSHAKE personalisation strings must be distinct.
Changes (Go, non-luxcpp):
- hash tags: PULSAR-HC-v1 -> CORONA-HC-v1, etc. (HU, TRANSCRIPT, PRF, MAC, PAIRWISE)
- profile IDs: "Pulsar-SHA3" -> "Corona-SHA3", "Pulsar-BLAKE3" -> "Corona-BLAKE3"
- context strings: pulsar.dkg2.A.v1 -> corona.dkg2.A.v1, etc.
- env vars: PULSAR_RESHARE_KAT_PATH -> CORONA_RESHARE_KAT_PATH, etc.
- struct names: pulsarSHA3 -> coronaSHA3
- KAT derive roots: sign_e2e_pulsar -> sign_e2e_corona
What's preserved (different scope):
- luxcpp/crypto/pulsar/* path references in comments (separate repo,
out of scope; the C++ side will rename in its own commit)
- Cross-runtime KAT files on disk (will regenerate next CI run)
All 11 packages test green: dkg, dkg2, hash, keyera, networking,
primitives, reshare, sign, threshold, utils, wire.
236 lines
7.0 KiB
Go
236 lines
7.0 KiB
Go
// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
// Package reshare fuzz harness for transcript binding.
|
|
//
|
|
// Property anchor: proofs/definitions/transcript-binding.tex
|
|
// Definition ref:pulsar-transcript ("Corona TranscriptInputs.Hash") —
|
|
// the canonical TranscriptHash is collision-resistant, and any two
|
|
// distinct field tuples yield distinct hashes by collision resistance
|
|
// of TupleHash256.
|
|
//
|
|
// This harness exercises the Go implementation of the canonical
|
|
// TranscriptInputs encoder against arbitrary mutated inputs, asserting:
|
|
//
|
|
// 1. Encoding never panics (decode-encode-cycle robustness).
|
|
// 2. The encoding is canonical: re-hashing identical inputs yields the
|
|
// same bytes (determinism is the input-side requirement of the
|
|
// collision-resistance bound).
|
|
// 3. Two TranscriptInputs values that differ in any single field yield
|
|
// distinct hashes — the field-level binding property is what makes
|
|
// the activation cert non-replayable across epochs / variants /
|
|
// suites.
|
|
|
|
package reshare
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"testing"
|
|
)
|
|
|
|
// FuzzTranscriptInputsHash exercises the canonical transcript hash
|
|
// against arbitrary byte inputs.
|
|
//
|
|
// We cannot decode-and-re-encode TranscriptInputs because the type has
|
|
// strongly-typed fields (uint64 era IDs, [32]byte set hashes) — there
|
|
// is no "wire format" we serialize/deserialize. Instead we drive the
|
|
// fuzzer at the transcript-hash function: produce arbitrary inputs,
|
|
// confirm Hash never panics, and confirm that mutating a single
|
|
// canonical-encoding input always changes the hash.
|
|
func FuzzTranscriptInputsHash(f *testing.F) {
|
|
// Seed corpus: a few representative TranscriptInputs values pulled
|
|
// from the activation_oracle KAT shape. Each is encoded as a flat
|
|
// byte stream the harness decodes into a TranscriptInputs.
|
|
f.Add(seedTranscriptBytes("lux-mainnet", "quasar-pq", 1, 100, 101, 11, 11, "Corona-SHA3", "v1", "reshare"))
|
|
f.Add(seedTranscriptBytes("lux-testnet", "g0", 0, 1, 2, 3, 3, "Corona-BLAKE3", "v1", "refresh"))
|
|
f.Add(seedTranscriptBytes("", "", 0, 0, 0, 0, 0, "", "", ""))
|
|
|
|
f.Fuzz(func(t *testing.T, raw []byte) {
|
|
in1 := decodeFuzzTranscript(raw)
|
|
|
|
// Property 1: Hash never panics on arbitrary inputs.
|
|
h1 := in1.Hash(nil)
|
|
|
|
// Property 2: deterministic — same inputs, same output.
|
|
h2 := in1.Hash(nil)
|
|
if h1 != h2 {
|
|
t.Fatalf("non-deterministic hash on identical inputs")
|
|
}
|
|
|
|
// Property 3: the build-parts canonicalization yields the same
|
|
// hash regardless of whether the same struct value is hashed
|
|
// once or twice in succession.
|
|
h3 := in1.Hash(nil)
|
|
if h1 != h3 {
|
|
t.Fatalf("hash changed across repeated invocations")
|
|
}
|
|
|
|
// Property 4: distinct inputs produce distinct hashes. We mutate
|
|
// one canonical field (the variant tag) and confirm divergence.
|
|
// If the original variant was non-empty we flip a byte; if empty
|
|
// we set it.
|
|
in2 := in1
|
|
if len(in2.Variant) == 0 {
|
|
in2.Variant = "x"
|
|
} else {
|
|
b := []byte(in2.Variant)
|
|
b[0] ^= 0x01
|
|
in2.Variant = string(b)
|
|
}
|
|
h4 := in2.Hash(nil)
|
|
if h1 == h4 {
|
|
t.Fatalf("variant mutation did not change transcript hash")
|
|
}
|
|
})
|
|
}
|
|
|
|
// FuzzCorpus_TranscriptReplay re-runs the seed corpus deterministically
|
|
// for CI environments that want to confirm the canonical inputs still
|
|
// hash to stable values without invoking the native fuzzer.
|
|
func TestFuzzCorpus_TranscriptReplay(t *testing.T) {
|
|
seeds := [][]byte{
|
|
seedTranscriptBytes("lux-mainnet", "quasar-pq", 1, 100, 101, 11, 11, "Corona-SHA3", "v1", "reshare"),
|
|
seedTranscriptBytes("lux-testnet", "g0", 0, 1, 2, 3, 3, "Corona-BLAKE3", "v1", "refresh"),
|
|
seedTranscriptBytes("", "", 0, 0, 0, 0, 0, "", "", ""),
|
|
}
|
|
for i, s := range seeds {
|
|
in := decodeFuzzTranscript(s)
|
|
h1 := in.Hash(nil)
|
|
h2 := in.Hash(nil)
|
|
if h1 != h2 {
|
|
t.Fatalf("seed %d: non-deterministic", i)
|
|
}
|
|
}
|
|
}
|
|
|
|
// seedTranscriptBytes builds a flat byte stream the fuzz harness can
|
|
// decode into a TranscriptInputs. The encoding is intentionally simple
|
|
// (the fuzzer does not care about field semantics, only that we feed it
|
|
// arbitrary bytes the decoder accepts) and is NOT used in any
|
|
// production wire format — it lives entirely inside the test binary.
|
|
func seedTranscriptBytes(
|
|
chain, group string,
|
|
era uint64,
|
|
oldEpoch, newEpoch uint64,
|
|
tOld, tNew uint32,
|
|
suite, ver, variant string,
|
|
) []byte {
|
|
var b []byte
|
|
b = appendString(b, chain)
|
|
b = appendString(b, group)
|
|
b = appendU64(b, era)
|
|
b = appendU64(b, oldEpoch)
|
|
b = appendU64(b, newEpoch)
|
|
b = appendU32(b, tOld)
|
|
b = appendU32(b, tNew)
|
|
b = appendString(b, suite)
|
|
b = appendString(b, ver)
|
|
b = appendString(b, variant)
|
|
return b
|
|
}
|
|
|
|
// decodeFuzzTranscript pulls fields out of an arbitrary byte stream
|
|
// using length-prefixed reads. Any read past the end returns the
|
|
// zero value; the harness still produces a valid TranscriptInputs that
|
|
// Hash will accept.
|
|
func decodeFuzzTranscript(raw []byte) TranscriptInputs {
|
|
r := &fuzzReader{buf: raw}
|
|
chain := r.readBytes()
|
|
group := r.readBytes()
|
|
era := r.readU64()
|
|
oldEpoch := r.readU64()
|
|
newEpoch := r.readU64()
|
|
tOld := r.readU32()
|
|
tNew := r.readU32()
|
|
suite := r.readString()
|
|
ver := r.readString()
|
|
variant := r.readString()
|
|
|
|
in := TranscriptInputs{
|
|
ChainID: chain,
|
|
GroupID: group,
|
|
KeyEraID: era,
|
|
OldEpochID: oldEpoch,
|
|
NewEpochID: newEpoch,
|
|
ThresholdOld: tOld,
|
|
ThresholdNew: tNew,
|
|
HashSuiteID: suite,
|
|
ImplementationVersion: ver,
|
|
Variant: variant,
|
|
}
|
|
// Bind a few of the [32]byte fields from the remaining stream so
|
|
// the harness explores those positions too.
|
|
copy(in.OldSetHash[:], r.readN(32))
|
|
copy(in.NewSetHash[:], r.readN(32))
|
|
copy(in.GroupPublicKeyHash[:], r.readN(32))
|
|
copy(in.NebulaRoot[:], r.readN(32))
|
|
return in
|
|
}
|
|
|
|
type fuzzReader struct {
|
|
buf []byte
|
|
}
|
|
|
|
func (r *fuzzReader) readU32() uint32 {
|
|
if len(r.buf) < 4 {
|
|
r.buf = nil
|
|
return 0
|
|
}
|
|
v := binary.BigEndian.Uint32(r.buf[:4])
|
|
r.buf = r.buf[4:]
|
|
return v
|
|
}
|
|
|
|
func (r *fuzzReader) readU64() uint64 {
|
|
if len(r.buf) < 8 {
|
|
r.buf = nil
|
|
return 0
|
|
}
|
|
v := binary.BigEndian.Uint64(r.buf[:8])
|
|
r.buf = r.buf[8:]
|
|
return v
|
|
}
|
|
|
|
func (r *fuzzReader) readBytes() []byte {
|
|
n := r.readU32()
|
|
if uint64(n) > uint64(len(r.buf)) {
|
|
out := append([]byte(nil), r.buf...)
|
|
r.buf = nil
|
|
return out
|
|
}
|
|
out := append([]byte(nil), r.buf[:n]...)
|
|
r.buf = r.buf[n:]
|
|
return out
|
|
}
|
|
|
|
func (r *fuzzReader) readString() string { return string(r.readBytes()) }
|
|
|
|
func (r *fuzzReader) readN(n int) []byte {
|
|
if len(r.buf) < n {
|
|
out := make([]byte, n)
|
|
copy(out, r.buf)
|
|
r.buf = nil
|
|
return out
|
|
}
|
|
out := append([]byte(nil), r.buf[:n]...)
|
|
r.buf = r.buf[n:]
|
|
return out
|
|
}
|
|
|
|
func appendU32(dst []byte, v uint32) []byte {
|
|
var b [4]byte
|
|
binary.BigEndian.PutUint32(b[:], v)
|
|
return append(dst, b[:]...)
|
|
}
|
|
|
|
func appendU64(dst []byte, v uint64) []byte {
|
|
var b [8]byte
|
|
binary.BigEndian.PutUint64(b[:], v)
|
|
return append(dst, b[:]...)
|
|
}
|
|
|
|
func appendString(dst []byte, s string) []byte {
|
|
dst = appendU32(dst, uint32(len(s)))
|
|
return append(dst, s...)
|
|
}
|