Files
warp/fuzz_signature_test.go
zeekay 948ee85a9c warp: ship Warp 2.0 envelope (Beam + ML-DSA + Pulse, Prism-bound)
Implements the LP-021v2 / LP-105 §"Warp evolution" cross-chain envelope
that carries the Warp 1.x BLS Beam intact alongside transcript-binding
fields and two optional PQ lanes (Pulsar Pulse, ML-DSA cert set).

Wire format: leading 0x02 version byte + RLP-encoded EnvelopeV2 record
binding SourceNebulaRoot, SourceKeyEraID, SourceGeneration, HashSuiteID
(default "Pulsar-SHA3"), plus optional PulsarPulse and MLDSACertSet
lanes. Forward compatibility: ParseEnvelope decodes Warp 1.x bytes into
a v2 envelope with PQ lanes empty. Backward compatibility: v1 receivers
reject v2 bytes (the 0x02 leading byte is not a valid RLP-list prefix);
senders that need to reach v1-only verifiers emit Warp 1.x bytes on the
v1 channel. Replay protection is uniform: EnvelopeV2.ID() returns the
embedded Message.ID().

Verification: VerifyV2 checks structural invariants, hash-suite
consistency, BLS Beam (against the source-chain validator set), then
the optional ML-DSA cert-set and Pulsar Pulse lanes. VerifyPQLanes is
the standalone entrypoint for callers that already verified the Beam
through a separate code path.

Subpackage warp/pulsar holds the Pulse path:
  - KernelVerifier consumes a GroupKeyResolver (the destination chain's
    source-chain key registry) to fetch the source-chain GroupKey for
    a given (KeyEraID, Generation) and verifies the Pulse via the
    luxfi/pulsar kernel.
  - BuildSigningBytes produces the canonical transcript bytes under the
    WARP-PULSAR-ENVELOPE-v1 domain prefix (distinct from any Pulsar
    consensus-side prefix to prevent cross-domain replay).
  - HorizonFromEnvelope lifts a verified envelope into the LP-105
    HorizonCertificate shape consumed by destination consensus / bridge
    code.

Pulse byte format wraps the kernel Signature's C / Z / Delta components
in three little-endian length-prefixed frames so DeserializePulse can
dispatch each component into a fresh slice-backed lattigo buffer
(the recommended path for slice-backed reads).

Tests:
  - envelope_test.go: roundtrip, optional-field defaulting, forward /
    backward / unknown-version dispatch, ID stability, PQ-lane
    verifier dispatch (Pulse, CertSet, required-but-absent flags).
  - pulsar/pulsar_test.go: real t-of-n Pulsar ceremony, serialize /
    deserialize roundtrip, KernelVerifier accept / reject paths
    (transcript-tampering rejection, resolver failure, suite mismatch),
    BuildSigningBytes binds every transcript field, Horizon assembly.

Module wiring: adds github.com/luxfi/pulsar (replace ../pulsar) and
github.com/luxfi/lattice/v7 to warp/go.mod. Restores the pinned
luxfi/precompile h1: hash so go test ./... resolves cleanly.

Documentation: README.md and SPECIFICATION.md rewritten to describe the
shipping v1.x + v2.0 + v2-Private versions, the dispatch flow, and the
Pulse transcript-binding rules.

LP refs: LP-021 (Warp 1.x), LP-021v2 (this work), LP-073 (Pulsar
kernel), LP-075 (BLS aggregate), LP-105 §"Warp evolution".

All warp tests pass: GOWORK=off go test ./...
2026-05-11 22:31:20 -07:00

141 lines
4.1 KiB
Go

// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// Warp 1.x wire-format fuzz harnesses.
//
// FuzzBLSAggregateCert — BitSetSignature codec round-trip
// FuzzWarpV1Envelope — legacy v1 Message codec round-trip; asserts
// v1 verifiers reject 0x02-prefixed envelopes
//
// Property: every decoder path under MaxMessageSize never escapes a
// panic, and v1 verifiers correctly reject v2 envelopes.
package warp
import (
"bytes"
"fmt"
"testing"
)
const fuzzMaxRawSize = 64 * 1024
// fuzzBLSAggregateCertCodec round-trips raw bytes through the warp
// codec into a BitSetSignature and back. Property: 0 panics.
func fuzzBLSAggregateCertCodec(raw []byte) (err error) {
if len(raw) > fuzzMaxRawSize {
return fmt.Errorf("input exceeds fuzzMaxRawSize")
}
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("decode panic recovered: %v", r)
}
}()
sig := &BitSetSignature{}
_, derr := Codec.Unmarshal(raw, sig)
return derr
}
// fuzzWarpV1MessageCodec round-trips raw bytes through the warp v1
// codec into a Message. Property: 0 panics, and a v1 verifier called
// on bytes whose first byte is EnvelopeVersion2 (0x02) MUST reject
// rather than silently accept (this is the v1/v2 cross-version
// safety property).
func fuzzWarpV1MessageCodec(raw []byte) (err error) {
if len(raw) > fuzzMaxRawSize {
return fmt.Errorf("input exceeds fuzzMaxRawSize")
}
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("decode panic recovered: %v", r)
}
}()
msg := &Message{}
_, derr := Codec.Unmarshal(raw, msg)
if derr != nil {
return derr
}
// V1/V2 safety: if the bytes happened to round-trip as a v1
// Message AND start with 0x02, ParseEnvelope MUST treat them as
// a v2 envelope (i.e., NOT as a v1 Message). Otherwise an
// attacker can craft bytes that decode as both versions.
if len(raw) > 0 && raw[0] == EnvelopeVersion2 {
// We don't require this to succeed — the v2 RLP body may be
// invalid — but we do require it not to silently treat the
// bytes as a valid v1 Message.
_, _ = ParseEnvelopeV2(raw)
}
return nil
}
func addSmallSeeds(f *testing.F) {
f.Add([]byte{})
f.Add([]byte{0x00})
f.Add(bytes.Repeat([]byte{0xff}, 32))
f.Add([]byte{EnvelopeVersion2}) // 0x02 prefix alone
// Plausible-looking signed message header.
f.Add(append([]byte{0x00, 0x00}, bytes.Repeat([]byte{0x00}, 64)...))
}
// FuzzBLSAggregateCert fuzzes the BitSetSignature codec.
func FuzzBLSAggregateCert(f *testing.F) {
addSmallSeeds(f)
f.Fuzz(func(t *testing.T, raw []byte) {
_ = fuzzBLSAggregateCertCodec(raw)
})
}
// FuzzWarpV1Envelope fuzzes the legacy v1 Message codec.
func FuzzWarpV1Envelope(f *testing.F) {
addSmallSeeds(f)
f.Fuzz(func(t *testing.T, raw []byte) {
_ = fuzzWarpV1MessageCodec(raw)
})
}
// TestFuzzCorpus_BLSAggregateCertReplay replays the small-seed corpus.
func TestFuzzCorpus_BLSAggregateCertReplay(t *testing.T) {
for _, raw := range [][]byte{
{},
{0x00},
bytes.Repeat([]byte{0xff}, 32),
} {
_ = fuzzBLSAggregateCertCodec(raw)
}
}
// TestFuzzCorpus_WarpV1EnvelopeReplay replays the small-seed corpus
// and explicitly verifies that a 0x02-prefixed input does not pass v1
// validation as a Message.
func TestFuzzCorpus_WarpV1EnvelopeReplay(t *testing.T) {
for _, raw := range [][]byte{
{},
{0x00},
{EnvelopeVersion2},
append([]byte{EnvelopeVersion2}, bytes.Repeat([]byte{0x00}, 32)...),
} {
_ = fuzzWarpV1MessageCodec(raw)
}
// Specific v1/v2 cross-version safety check: a 0x02-prefixed
// random body should be rejected by ParseEnvelopeV2 (because
// the body is not valid RLP), and should NOT be valid as a v1
// Message either.
hostile := append([]byte{EnvelopeVersion2}, bytes.Repeat([]byte{0xab}, 64)...)
if _, err := ParseEnvelopeV2(hostile); err == nil {
t.Fatalf("ParseEnvelopeV2 accepted random 0x02-prefixed bytes")
}
msg := &Message{}
if _, err := Codec.Unmarshal(hostile, msg); err == nil {
// Decoding as v1 might succeed (the codec is lenient), but
// Verify() must fail.
if verr := msg.Verify(); verr == nil {
t.Fatalf("v1 Verify accepted hostile bytes that decoded")
}
}
}