mirror of
https://github.com/luxfi/warp.git
synced 2026-10-30 15:13:04 +00:00
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 ./...
141 lines
4.1 KiB
Go
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")
|
|
}
|
|
}
|
|
}
|