Files
consensus/protocol/quasar/witness.go
zeekay 24b7cd2154 consensus: route corona + pulsar through threshold/protocols alias
Replaces every direct import of luxfi/corona/* and luxfi/pulsar/ref/go
in consensus with the canonical luxfi/threshold/protocols/{corona,pulsar}
alias surface. Pure import swap + go.mod bump; no behaviour change.

Files touched (16):

  bench/pq_modes_bench_test.go
  engine/pq/crypto.go
  engine/pq/engine.go
  pkg/wire/policies.go
  protocol/quasar/cert_size_compare_test.go
  protocol/quasar/corona_gob.go
  protocol/quasar/dual_threshold_test.go
  protocol/quasar/epoch.go
  protocol/quasar/epoch_test.go
  protocol/quasar/gpu_accel.go
  protocol/quasar/grouped_threshold.go
  protocol/quasar/quasar.go
  protocol/quasar/types.go
  protocol/quasar/wave_signer.go
  protocol/quasar/wave_signer_test.go
  protocol/quasar/witness.go
  protocol/quasar/witness_pulsar_test.go

Import path swaps:

  luxfi/corona/threshold        -> luxfi/threshold/protocols/corona
  luxfi/corona/keyera           -> luxfi/threshold/protocols/corona/keyera
  luxfi/corona/primitives       -> luxfi/threshold/protocols/corona/primitives
  luxfi/corona/reshare          -> luxfi/threshold/protocols/corona/reshare
  luxfi/corona/sign             -> luxfi/threshold/protocols/corona/sign
  luxfi/corona/gpu              -> luxfi/threshold/protocols/corona/gpu
  luxfi/pulsar/ref/go/pkg/pulsar -> luxfi/threshold/protocols/pulsar

The aliased package name "coronaThreshold" used by quasar / engine/pq /
pkg/wire identifiers is preserved at the call sites (it now refers to
the threshold/protocols/corona alias package). engine/pq/crypto.go is
the only file where the package was bare-imported as `threshold`; that
identifier was renamed to `corona` to match the new alias path.

go.mod:

  - luxfi/threshold bumped to a branch pseudo-version that carries the
    new alias subpackages; will pin to a stable tag once threshold's
    companion commit lands and a release is cut.
  - luxfi/corona and luxfi/pulsar drop to indirect (consensus now
    reaches them only via threshold/protocols).
  - luxfi/math v1.4.0 -> v1.4.1 and luxfi/metric v1.5.0 -> v1.5.7 are
    transitive minimum-version bumps pulled by the new threshold
    requirement; they are no-op for consensus call sites.

Comment-only references to "github.com/luxfi/corona/..." in bls.go,
round_digest.go, reshare_epoch.go, comprehensive_test.go,
engine_test.go, quasar_bench_test.go and config/pq_mode.go are
documentation pointers to where the underlying implementation lives;
they are accurate either way and are left unchanged (out of refactor
scope).

Layering shape after:

  consensus -> threshold/protocols/{corona,pulsar}  (stable alias)
                 |
                 v
              luxfi/corona, luxfi/pulsar  (primitive implementations)

Closes the 22-file layering violation flagged by the 2026-05-31
integration audit. Tests pass: ./engine/pq/..., ./pkg/wire/...,
./protocol/quasar/... (all key cases — TestEpochManager*,
TestRoundSigner*, TestVerifyPQFinality*, TestBLSThreshold*,
TestDualSigningFlow, TestCoronaCertSize* — green; the pre-existing
TestGroupedEpochManager_ParallelKeygen perf-bound flake on slow
machines was reproduced on unmodified main, so it is not a
regression).
2026-05-31 12:39:47 -07:00

748 lines
24 KiB
Go

// Copyright (C) 2025-2026, Lux Industries Inc All rights reserved.
// Verkle Witness for Hyper-Efficient Verification with PQ Finality
package quasar
import (
"crypto/subtle"
"encoding/binary"
"errors"
"sync"
"time"
"github.com/luxfi/consensus/config"
"github.com/luxfi/crypto/banderwagon"
"github.com/luxfi/crypto/bls"
"github.com/luxfi/crypto/mldsa"
"github.com/luxfi/threshold/protocols/pulsar"
"golang.org/x/crypto/sha3"
)
// Pulsar finality verification errors. Exported so call sites can
// route on the exact failure reason without parsing error strings.
var (
// ErrPulsarVerifyFail is returned when the Pulsar finality
// signature does not verify under the bound group public key.
// Closes F107/F109: previously a bit-count tautology; now a real
// lattice signature check via luxfi/pulsar.
ErrPulsarVerifyFail = errors.New("quasar: Pulsar finality signature did not verify under group public key")
// ErrBLSForbiddenUnderStrictPQ is returned when verifyBLSAggregate
// is invoked while a strict-PQ profile is active. The BLS aggregate
// path is callable only under ForkClassicalCompatUnsafe; under
// StrictPQ / FIPS it is a hard refusal. Closes F107.
ErrBLSForbiddenUnderStrictPQ = errors.New("quasar: BLS aggregate verification is forbidden under strict-PQ profile")
)
// VerkleWitness provides hyper-efficient state verification.
//
// "PQ finality" used to mean "the Corona bitfield carries enough
// set bits"; that is a bit-count tautology with zero cryptographic
// content. The current implementation REQUIRES a real ML-DSA-65
// (Pulsar-65 wire format) signature over a canonical signing digest
// before VerifyStateTransition admits a witness. The bit-count check
// remains as a pre-filter ONLY because a witness that does not even
// claim enough signers does not deserve a verifying-key dispatch.
//
// Closes red-team finding F109 ("checkPQFinality verifies by
// bit-counting"). Closes F107 ("verifyBLSAggregate only checks
// curve-point parseability") by making the BLS aggregate path opt-in
// (strict-PQ profiles refuse it).
type VerkleWitness struct {
// RWMutex for cache operations
mu sync.RWMutex
// Verkle tree commitment
root *banderwagon.Element
// Cached witnesses for fast verification
witnessCache map[string]*WitnessProof
cacheSize int
// PQ finality assumption
assumePQFinal bool
minThreshold int
// pqGroupKey is the ML-DSA-65 (Pulsar-M-65) group public key
// whose signature authenticates a witness. nil means the witness
// path is operating in pre-PQ legacy mode (BLS aggregate path);
// strict-PQ deployments always set this. Mutex-protected so a
// validator rotation can swap the group key without restarting.
pqGroupKey []byte
// pqMode is the ML-DSA mode the group key targets. Defaults to
// MLDSA65 (Pulsar-M-65 / FIPS 204 Category 3).
pqMode mldsa.Mode
// profile is the chain-wide security profile this witness
// verifier operates under. nil is permissive (legacy callers);
// non-nil strict-PQ refuses every classical fallback at the
// FullVerification path.
profile *config.ChainSecurityProfile
}
// WitnessProof contains the minimal proof for state verification.
//
// PQSignature is the load-bearing field: an ML-DSA-65 signature over
// SigningDigest(witness) produced by the group key the verifier holds.
// BLSAggregate / CoronaBits / ValidatorSet are legacy fields kept
// for wire compatibility with classical-compat deployments; under
// strict-PQ the verifier ignores them.
type WitnessProof struct {
// Verkle proof components
Commitment []byte // 32 bytes banderwagon point
Path []byte // Compressed path in tree
OpeningProof []byte // IPA opening proof
// PQ finality signature — the canonical strict-PQ verification
// target. ML-DSA-65 / Pulsar-M-65 wire format, 3309 bytes for
// MLDSA65 per FIPS 204 §4 Table 1.
PQSignature []byte
// Legacy fields (classical-compat deployments). Strict-PQ
// verifiers refuse to consult CoronaBits / BLSAggregate.
BLSAggregate []byte // Aggregated BLS signature
CoronaBits []byte // Bitfield of Corona signers
ValidatorSet []byte // Compressed validator set hash
// Block metadata
BlockHeight uint64
StateRoot []byte
Timestamp uint64
}
// signingDigestCustomization is the cSHAKE customization tag that
// derives the per-witness signing digest. Bumping the tag invalidates
// every prior PQ signature; that is the correct semantics for a
// breaking change to the witness encoding.
const signingDigestCustomization = "LUX-VERKLE-WITNESS-V1"
// SigningDigest returns the 48-byte cSHAKE256 digest a strict-PQ
// verifier signs over. The digest binds every consensus-relevant
// field of the witness so a signature against witness W cannot be
// replayed against a witness W' with any differing metadata.
//
// Encoding is TupleHash256-style (SP 800-185): each field is
// length-prefixed (left_encode(len*8) || bytes) and the final
// right_encode(384) commits the output length. The 48-byte width
// matches MinHashOutputBits=384 on the strict-PQ profile.
func (w *WitnessProof) SigningDigest() [48]byte {
parts := [][]byte{
[]byte("Lux/VerkleWitness/v1"),
w.Commitment,
w.Path,
w.OpeningProof,
u64BEWitness(w.BlockHeight),
w.StateRoot,
u64BEWitness(w.Timestamp),
}
var x []byte
for _, p := range parts {
x = append(x, encodeStringSP800185Witness(p)...)
}
x = append(x, rightEncodeSP800185Witness(uint64(48)*8)...)
h := sha3.NewCShake256([]byte("TupleHash"), []byte(signingDigestCustomization))
_, _ = h.Write(x)
out := make([]byte, 48)
_, _ = h.Read(out)
var digest [48]byte
copy(digest[:], out)
return digest
}
// NewVerkleWitness creates a lightweight Verkle witness verifier with
// no PQ group key bound. Callers that want strict-PQ verification call
// BindPQGroupKey and SetProfile before VerifyStateTransition. Calls
// against this constructor's result fall through to legacy BLS-
// aggregate behaviour (still subject to a real curve-point check;
// not a no-op).
func NewVerkleWitness(threshold int) *VerkleWitness {
return &VerkleWitness{
witnessCache: make(map[string]*WitnessProof),
cacheSize: 1000, // Cache last 1000 witnesses
assumePQFinal: true, // Always assume PQ finality
minThreshold: threshold,
pqMode: mldsa.MLDSA65,
}
}
// NewVerkleWitnessUnderProfile constructs a witness verifier that
// admits ONLY signatures verifiable under the profile-pinned
// FinalitySchemeID. The profile is captured by reference and consulted
// at every VerifyStateTransition.
func NewVerkleWitnessUnderProfile(
threshold int,
groupPubkey []byte,
profile *config.ChainSecurityProfile,
) *VerkleWitness {
v := NewVerkleWitness(threshold)
v.pqGroupKey = append([]byte(nil), groupPubkey...)
v.profile = profile
return v
}
// BindPQGroupKey records the ML-DSA-65 group public key the witness
// verifier checks signatures against. Mutex-protected so an
// epoch-rotation handler can swap the key without re-creating the
// verifier.
func (v *VerkleWitness) BindPQGroupKey(pubkey []byte) {
v.mu.Lock()
defer v.mu.Unlock()
v.pqGroupKey = append([]byte(nil), pubkey...)
}
// SetProfile records the chain-wide security profile this verifier
// enforces. Mutex-protected; nil is permissive.
func (v *VerkleWitness) SetProfile(p *config.ChainSecurityProfile) {
v.mu.Lock()
defer v.mu.Unlock()
v.profile = p
}
// VerifyStateTransition verifies state transition with minimal overhead.
//
// Strict-PQ path: every witness MUST carry a PQSignature that verifies
// against the bound ML-DSA-65 group public key over SigningDigest.
// The bit-count pre-filter still runs (a witness that doesn't even
// claim enough signers is structurally invalid) but the cryptographic
// decision is the signature check.
//
// Legacy path (nil profile, nil pqGroupKey): falls through to the
// classical BLS aggregate verification, which itself now performs a
// real curve-point check.
func (v *VerkleWitness) VerifyStateTransition(witness *WitnessProof) error {
if witness == nil {
return errors.New("nil witness")
}
v.mu.RLock()
groupKey := v.pqGroupKey
mode := v.pqMode
profile := v.profile
v.mu.RUnlock()
// Strict-PQ path: require a real ML-DSA-65 signature verification.
if profile != nil && profile.FinalitySchemeID.IsPulsarM() {
if len(groupKey) == 0 {
return errors.New("strict-PQ profile pinned but no PQ group key bound")
}
if err := v.verifyPQFinality(witness, groupKey, mode); err != nil {
return err
}
return v.verifyVerkleCommitment(witness)
}
// Legacy / classical-compat path. checkPQFinality is the
// bit-count pre-filter; the actual cryptographic check is
// verifyBLSAggregate on the slow path.
if v.assumePQFinal && v.checkPQFinality(witness) {
// Bit-count satisfied + caller declared "assume PQ final" —
// trust the pre-filter and skip BLS. This branch exists for
// pre-strict-PQ deployments that already operate under an
// out-of-band PQ-final assumption (e.g. checkpointed by the
// EpochManager). Strict-PQ deployments DO NOT take this
// branch (the FinalitySchemeID.IsPulsarM() check above
// short-circuits to the real verifier).
return v.verifyVerkleCommitment(witness)
}
// Slow path: full verification (shouldn't happen with PQ finality).
return v.fullVerification(witness)
}
// verifyPQFinality runs the canonical strict-PQ check: a real
// Pulsar-M signature verification of witness.PQSignature against the
// bound group public key over SigningDigest(witness). Dispatches to
// luxfi/pulsar-m.Verify, which is a thin wrapper over FIPS 204
// ML-DSA.Verify (Class N1 manifesto: a Pulsar-M signature verifies
// under unmodified FIPS 204 ML-DSA verification). Closes F109.
//
// The bit-count pre-filter runs first as a cheap reject: a witness
// that does not even claim enough signers is structurally invalid
// and we don't want to pay the lattice verify cost for it.
//
// Returns ErrPulsarVerifyFail when the lattice verification rejects
// the signature; returns a typed wrapping of the underlying pulsar
// error for structural failures (size, parameter set, nil pointer).
func (v *VerkleWitness) verifyPQFinality(
witness *WitnessProof,
groupKey []byte,
mode mldsa.Mode,
) error {
if !v.checkPQFinality(witness) {
return errors.New("PQ threshold pre-filter failed: insufficient claimed signers")
}
if len(witness.PQSignature) == 0 {
return errors.New("PQ signature missing from witness")
}
pulsarMode, err := pulsarModeFromMLDSAMode(mode)
if err != nil {
return err
}
params, err := pulsar.ParamsFor(pulsarMode)
if err != nil {
return err
}
if expected := params.SignatureSize; len(witness.PQSignature) != expected {
return errors.New("PQ signature has wrong size for declared mode")
}
if expected := params.PublicKeySize; len(groupKey) != expected {
return errors.New("PQ group public key has wrong size for declared mode")
}
pub := &pulsar.PublicKey{Mode: pulsarMode, Bytes: append([]byte(nil), groupKey...)}
sig := &pulsar.Signature{Mode: pulsarMode, Bytes: append([]byte(nil), witness.PQSignature...)}
digest := witness.SigningDigest()
if err := pulsar.VerifyCtx(params, pub, digest[:], []byte(signingDigestCustomization), sig); err != nil {
// Re-wrap with our exported sentinel so call sites can route
// on errors.Is(err, ErrPulsarVerifyFail) without depending on
// the pulsar error variants.
return errors.Join(ErrPulsarVerifyFail, err)
}
return nil
}
// pulsarModeFromMLDSAMode translates a luxfi/crypto/mldsa.Mode into the
// matching luxfi/pulsar-m.Mode. Single dispatch point so the rest of
// the consensus code can keep its mldsa.Mode-typed configuration while
// the cryptographic verifier lives in luxfi/pulsar-m.
func pulsarModeFromMLDSAMode(m mldsa.Mode) (pulsar.Mode, error) {
switch m {
case mldsa.MLDSA44:
return pulsar.ModeP44, nil
case mldsa.MLDSA65:
return pulsar.ModeP65, nil
case mldsa.MLDSA87:
return pulsar.ModeP87, nil
default:
return pulsar.ModeUnspecified, errors.New("unknown ML-DSA mode")
}
}
// checkPQFinality is the structural pre-filter: counts set bits in
// the Corona bitfield against the configured threshold. NOT a
// cryptographic check on its own — strict-PQ paths additionally call
// verifyPQFinality which performs real ML-DSA-65 signature
// verification. Kept as a public-shaped helper because the bit-count
// is genuinely useful as a structural reject before paying the
// signature-verify cost.
func (v *VerkleWitness) checkPQFinality(witness *WitnessProof) bool {
if witness == nil {
return false
}
coronaCount := countSetBits(witness.CoronaBits)
return coronaCount >= v.minThreshold
}
// verifyVerkleCommitment does ultra-fast Verkle proof verification
func (v *VerkleWitness) verifyVerkleCommitment(witness *WitnessProof) error {
// Reconstruct the commitment point
var commitment banderwagon.Element
if err := commitment.SetBytes(witness.Commitment); err != nil {
return errors.New("invalid commitment")
}
// Verify opening proof (IPA)
// This is O(log n) and very fast
if !verifyIPAOpening(&commitment, witness.Path, witness.OpeningProof) {
return errors.New("invalid Verkle opening proof")
}
// Cache the witness for future use
v.cacheWitness(witness)
return nil
}
// fullVerification does complete verification (fallback, rarely used).
// Refuses to run the classical BLS aggregate path under strict-PQ.
func (v *VerkleWitness) fullVerification(witness *WitnessProof) error {
v.mu.RLock()
profile := v.profile
v.mu.RUnlock()
// Strict-PQ refuses the BLS aggregate path. Closes F107: any
// classical pairing-based aggregate is forbidden by the
// ForbidPairings invariant of every strict-PQ profile.
if profile != nil &&
(profile.ProfileID == uint32(config.ProfileStrictPQ) ||
profile.ProfileID == uint32(config.ProfileFIPS) ||
profile.ForbidPairings) {
return errors.New("strict-PQ profile forbids BLS-aggregate fallback verification")
}
// Verify BLS aggregate signature (classical-compat only).
if err := v.verifyBLSAggregate(witness.BLSAggregate, witness.ValidatorSet); err != nil {
return err
}
// Verify Corona threshold
if !v.verifyCoronaThreshold(witness.CoronaBits) {
return errors.New("corona threshold not met")
}
// Verify Verkle commitment
return v.verifyVerkleCommitment(witness)
}
// CreateWitness creates a minimal witness for a state transition
func (v *VerkleWitness) CreateWitness(
stateRoot []byte,
blsAgg *bls.Signature,
coronaSigners []bool,
height uint64,
) (*WitnessProof, error) {
// Create Verkle commitment
commitment, err := createVerkleCommitment(stateRoot)
if err != nil {
return nil, err
}
// Create opening proof (IPA)
openingProof := createIPAProof(commitment, stateRoot)
// Compress Corona signers to bitfield
coronaBits := compressToBitfield(coronaSigners)
// Create witness
commitmentBytes := commitment.Bytes()
witness := &WitnessProof{
Commitment: commitmentBytes[:],
Path: compressPath(stateRoot),
OpeningProof: openingProof,
BLSAggregate: bls.SignatureToBytes(blsAgg),
CoronaBits: coronaBits,
ValidatorSet: hashValidatorSet(),
BlockHeight: height,
StateRoot: stateRoot,
Timestamp: uint64(timeNow()),
}
// Cache it
v.cacheWitness(witness)
return witness, nil
}
// BatchVerify verifies multiple witnesses in parallel (hyper-efficient)
func (v *VerkleWitness) BatchVerify(witnesses []*WitnessProof) error {
// Since we assume PQ finality, we can verify in parallel
errs := make(chan error, len(witnesses))
var wg sync.WaitGroup
for _, witness := range witnesses {
wg.Add(1)
go func(w *WitnessProof) {
defer wg.Done()
if err := v.VerifyStateTransition(w); err != nil {
errs <- err
}
}(witness)
}
wg.Wait()
close(errs)
// Check for any errors
for err := range errs {
if err != nil {
return err
}
}
return nil
}
// Lightweight helper functions
func countSetBits(bits []byte) int {
count := 0
for _, b := range bits {
for b != 0 {
count += int(b & 1)
b >>= 1
}
}
return count
}
func compressToBitfield(signers []bool) []byte {
bitfield := make([]byte, (len(signers)+7)/8)
for i, signed := range signers {
if signed {
bitfield[i/8] |= 1 << (i % 8)
}
}
return bitfield
}
func createVerkleCommitment(stateRoot []byte) (*banderwagon.Element, error) {
if len(stateRoot) < 32 {
return nil, errors.New("stateRoot too short for commitment (need >= 32 bytes)")
}
// Create commitment from state root
var point banderwagon.Element
if err := point.SetBytes(stateRoot[:32]); err != nil {
return nil, errors.New("invalid stateRoot: not a valid banderwagon point")
}
return &point, nil
}
func createIPAProof(commitment *banderwagon.Element, data []byte) []byte {
// Simplified IPA proof creation.
// Uses SHA3-384 (48 bytes) so the output width matches the
// strict-PQ MinHashOutputBits=384 floor. Bound under a Lux-specific
// customisation so this digest can never collide with another
// SHA3-384 usage in the codebase.
h := sha3.NewCShake256([]byte("KMAC"), []byte("LUX-VERKLE-IPA-PROOF-V1"))
commitmentBytes := commitment.Bytes()
_, _ = h.Write(commitmentBytes[:])
_, _ = h.Write(data)
out := make([]byte, 48)
_, _ = h.Read(out)
return out
}
func verifyIPAOpening(commitment *banderwagon.Element, path []byte, proof []byte) bool {
// Mirrors createIPAProof: re-derive the 48-byte digest and
// constant-time compare against the stored proof bytes.
h := sha3.NewCShake256([]byte("KMAC"), []byte("LUX-VERKLE-IPA-PROOF-V1"))
commitmentBytes := commitment.Bytes()
_, _ = h.Write(commitmentBytes[:])
_, _ = h.Write(path)
expected := make([]byte, 48)
_, _ = h.Read(expected)
if len(proof) != len(expected) {
return false
}
return subtle.ConstantTimeCompare(expected, proof) == 1
}
func compressPath(stateRoot []byte) []byte {
if len(stateRoot) < 16 {
out := make([]byte, 16)
copy(out, stateRoot)
return out
}
compressed := make([]byte, 16)
copy(compressed, stateRoot[:16])
return compressed
}
func hashValidatorSet() []byte {
// Hash of current validator set. SHA3-256 under a Lux-specific
// customisation: the 32-byte width is preserved (callers store it
// in fixed 32-byte slots in CertBundle wire format); the
// FIPS 202 family is what the strict-PQ HashSuiteID pins. Closes
// the SHA-2 leak path while keeping the existing wire layout.
h := sha3.NewCShake256([]byte("KMAC"), []byte("LUX-VALIDATOR-SET-DIGEST-V1"))
_, _ = h.Write([]byte("validator_set_v1"))
out := make([]byte, 32)
_, _ = h.Read(out)
return out
}
func timeNow() int64 {
return time.Now().Unix()
}
func (v *VerkleWitness) cacheWitness(witness *WitnessProof) {
v.mu.Lock()
defer v.mu.Unlock()
key := string(witness.StateRoot)
v.witnessCache[key] = witness
// Evict old entries if cache is full
if len(v.witnessCache) > v.cacheSize {
// Simple eviction: remove first entry (could be improved with LRU)
for k := range v.witnessCache {
delete(v.witnessCache, k)
break
}
}
}
// verifyBLSAggregate verifies BLS aggregate signature for the legacy
// classical-compat path. Refuses under strict-PQ profiles — every call
// site that operates under StrictPQ / FIPS / a profile that
// pins ForbidPairings MUST take the verifyPQFinality path instead.
//
// The function is callable ONLY under the ForkClassicalCompatUnsafe
// profile (or a nil-profile legacy caller pre-dating the locked-
// profile architecture). The hard refusal closes F107: a strict-PQ
// deployment cannot accidentally fall through to BLS verification
// even if a caller passes the wrong path.
//
// The check itself is structural plus a real BLS curve-point
// deserialisation (closes the "any well-formed bytes pass" footgun
// from the original implementation); the cryptographic decision for
// production strict-PQ flows through verifyPQFinality.
//
// Deprecated: use verifyPQFinality under strict-PQ profiles. Callers
// remaining on this path MUST be running ForkClassicalCompatUnsafe.
func (v *VerkleWitness) verifyBLSAggregate(aggSig []byte, validatorSet []byte) error {
// Profile gate: refuse under strict-PQ profiles even if a caller
// reaches this function directly. Mirrors the gate in
// fullVerification (defence-in-depth).
v.mu.RLock()
profile := v.profile
v.mu.RUnlock()
if profile != nil &&
(profile.ProfileID == uint32(config.ProfileStrictPQ) ||
profile.ProfileID == uint32(config.ProfileFIPS) ||
profile.ForbidPairings) {
return ErrBLSForbiddenUnderStrictPQ
}
// Validate signature is present
if len(aggSig) == 0 {
return errors.New("empty aggregate signature")
}
// Signature length check (BLS G1 compressed = 48 bytes, G2 = 96 bytes)
if len(aggSig) < 48 {
return errors.New("invalid BLS signature length")
}
// Deserialize the signature to validate it is a valid curve point.
// This prevents accepting arbitrary bytes as a "valid" signature.
if _, err := bls.SignatureFromBytes(aggSig); err != nil {
return errors.New("BLS signature deserialization failed: not a valid curve point")
}
return nil
}
func (v *VerkleWitness) verifyCoronaThreshold(bits []byte) bool {
count := countSetBits(bits)
return count >= v.minThreshold
}
// GetCachedWitness retrieves a cached witness if available
func (v *VerkleWitness) GetCachedWitness(stateRoot []byte) (*WitnessProof, bool) {
v.mu.RLock()
defer v.mu.RUnlock()
witness, exists := v.witnessCache[string(stateRoot)]
return witness, exists
}
// WitnessSize returns the size of a witness in bytes
func (w *WitnessProof) Size() int {
return len(w.Commitment) + len(w.Path) + len(w.OpeningProof) +
len(w.PQSignature) +
len(w.BLSAggregate) + len(w.CoronaBits) + len(w.ValidatorSet) +
8 + len(w.StateRoot) + 8 // BlockHeight + StateRoot + Timestamp
}
// IsLightweight checks if witness is under 1KB (hyper-efficient)
func (w *WitnessProof) IsLightweight() bool {
return w.Size() < 1024
}
// CompressedWitness creates an even smaller witness for network transmission
type CompressedWitness struct {
CommitmentAndProof []byte // Combined commitment + proof
Metadata uint64 // Packed height + timestamp
Validators uint32 // Validator bitfield (up to 32 validators)
}
// Compress creates a compressed witness (< 200 bytes)
func (w *WitnessProof) Compress() *CompressedWitness {
// Combine commitment and opening proof
combined := append(w.Commitment[:16], w.OpeningProof[:16]...)
// Pack metadata
metadata := (w.BlockHeight << 32) | (w.Timestamp & 0xFFFFFFFF)
// Compress validator bits to uint32 (supports up to 32 validators)
var validatorBits uint32
for i := 0; i < len(w.CoronaBits) && i < 4; i++ {
validatorBits |= uint32(w.CoronaBits[i]) << (i * 8)
}
return &CompressedWitness{
CommitmentAndProof: combined,
Metadata: metadata,
Validators: validatorBits,
}
}
// Size returns size of compressed witness
func (c *CompressedWitness) Size() int {
return len(c.CommitmentAndProof) + 8 + 4 // ~44 bytes
}
// validateCompressedStructure checks structural validity of a compressed witness.
// This is NOT cryptographic verification -- it only checks that the validator
// threshold is met in the bitfield. Callers must not treat a nil return as
// proof of authenticity.
func (v *VerkleWitness) validateCompressedStructure(cw *CompressedWitness) error {
// Extract validator count from bitfield
validatorCount := 0
for i := uint32(0); i < 32; i++ {
if cw.Validators&(1<<i) != 0 {
validatorCount++
}
}
// Check threshold (assuming PQ finality)
if validatorCount < v.minThreshold {
return errors.New("insufficient validators")
}
// With PQ finality assumption, we're done!
return nil
}
// =============================================================================
// SP 800-185 helpers (local copies — keep witness.go reviewable in
// isolation; identical to the ones in round_digest.go / kmac256.go).
// =============================================================================
func u64BEWitness(v uint64) []byte {
var b [8]byte
binary.BigEndian.PutUint64(b[:], v)
return b[:]
}
func encodeStringSP800185Witness(s []byte) []byte {
out := leftEncodeSP800185Witness(uint64(len(s)) * 8)
out = append(out, s...)
return out
}
func leftEncodeSP800185Witness(x uint64) []byte {
if x == 0 {
return []byte{0x01, 0x00}
}
var buf [8]byte
binary.BigEndian.PutUint64(buf[:], x)
i := 0
for i < 7 && buf[i] == 0 {
i++
}
out := make([]byte, 0, 9-i)
out = append(out, byte(8-i))
out = append(out, buf[i:]...)
return out
}
func rightEncodeSP800185Witness(x uint64) []byte {
if x == 0 {
return []byte{0x00, 0x01}
}
var buf [8]byte
binary.BigEndian.PutUint64(buf[:], x)
i := 0
for i < 7 && buf[i] == 0 {
i++
}
out := make([]byte, 0, 9-i)
out = append(out, buf[i:]...)
out = append(out, byte(8-i))
return out
}