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 ./...
171 lines
4.9 KiB
Go
171 lines
4.9 KiB
Go
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
// Warp envelope fuzz harness.
|
|
//
|
|
// Property anchor: proofs/quasar/warp-pq-soundness.tex
|
|
// Theorem ref:warp-v2-soundness.
|
|
//
|
|
// Properties under fuzzing:
|
|
//
|
|
// 1. ParseEnvelope on arbitrary bytes never panics. The dispatcher
|
|
// accepts both v1 and v2 wire formats; malformed inputs MUST
|
|
// produce a clean error rather than a runtime panic.
|
|
//
|
|
// 2. Valid v2 envelopes round-trip via Bytes ↔ ParseEnvelopeV2.
|
|
//
|
|
// 3. v1 envelopes (RLP-encoded Message with no leading version byte)
|
|
// parse via the legacy path and produce an EnvelopeV2 with only
|
|
// the Beam lane populated.
|
|
|
|
package warp
|
|
|
|
import (
|
|
"bytes"
|
|
"testing"
|
|
|
|
"github.com/luxfi/crypto/bls"
|
|
"github.com/luxfi/ids"
|
|
)
|
|
|
|
// FuzzWarpEnvelopeV2 exercises ParseEnvelope against arbitrary bytes
|
|
// (v1 path, v2 path, and outright malformed inputs).
|
|
func FuzzWarpEnvelopeV2(f *testing.F) {
|
|
// Seed corpus: a few bytes pulled from the existing
|
|
// envelopeFixture used in envelope_test.go, both as v2 envelopes
|
|
// and as raw v1 wire bytes.
|
|
f.Add(makeFuzzSeedV2(7, 11, true, true))
|
|
f.Add(makeFuzzSeedV2(0, 0, false, false))
|
|
f.Add(makeFuzzSeedV1())
|
|
// Edge cases.
|
|
f.Add([]byte{})
|
|
f.Add([]byte{EnvelopeVersion2})
|
|
f.Add([]byte{0x05, 0x00, 0x00})
|
|
f.Add([]byte{EnvelopeVersion2, 0xFF})
|
|
|
|
f.Fuzz(func(t *testing.T, raw []byte) {
|
|
// Property 1: ParseEnvelope never panics.
|
|
env, err := ParseEnvelope(raw)
|
|
if err != nil {
|
|
// Errors are fine; we only care that no panic was thrown.
|
|
return
|
|
}
|
|
if env == nil || env.Message == nil {
|
|
t.Fatalf("ParseEnvelope returned nil envelope without error")
|
|
}
|
|
|
|
// Property 2: envelope passes structural verification.
|
|
if err := env.Verify(); err != nil {
|
|
t.Fatalf("ParseEnvelope returned envelope that fails Verify: %v", err)
|
|
}
|
|
|
|
// Property 3: re-encoding the parsed envelope and re-parsing
|
|
// produces a byte-equal envelope. Only run this on v2 inputs
|
|
// (v1 inputs have no version byte; re-encoding via v2 Bytes
|
|
// produces a v2 envelope that the v1 path cannot reproduce
|
|
// — that asymmetry is by design and exercised by
|
|
// TestParseEnvelopeForwardCompatV1Bytes).
|
|
if len(raw) > 0 && raw[0] == EnvelopeVersion2 {
|
|
out, err := env.Bytes()
|
|
if err != nil {
|
|
t.Fatalf("re-encode failed: %v", err)
|
|
}
|
|
env2, err := ParseEnvelopeV2(out)
|
|
if err != nil {
|
|
t.Fatalf("re-parse failed: %v", err)
|
|
}
|
|
if !env.Equal(env2) {
|
|
t.Fatalf("round-trip not byte-equal")
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestFuzzCorpus_EnvelopeReplay re-runs the seed corpus deterministically
|
|
// for CI environments that want a non-fuzz replay.
|
|
func TestFuzzCorpus_EnvelopeReplay(t *testing.T) {
|
|
seeds := [][]byte{
|
|
makeFuzzSeedV2(7, 11, true, true),
|
|
makeFuzzSeedV2(0, 0, false, false),
|
|
makeFuzzSeedV1(),
|
|
}
|
|
for i, s := range seeds {
|
|
env, err := ParseEnvelope(s)
|
|
if err != nil {
|
|
t.Fatalf("seed %d: parse failed: %v", i, err)
|
|
}
|
|
if err := env.Verify(); err != nil {
|
|
t.Fatalf("seed %d: verify failed: %v", i, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// makeFuzzSeedV2 builds a valid v2 wire envelope with the given lineage
|
|
// fields, optionally including a Pulse and / or ML-DSA cert set blob.
|
|
// We construct without depending on the test package's helper to avoid
|
|
// coupling between fuzz seeds and the unit-test fixture; this version
|
|
// is internal to the fuzz harness.
|
|
func makeFuzzSeedV2(eraID, generation uint64, withPulse, withCertSet bool) []byte {
|
|
const networkID = uint32(1)
|
|
chainID := ids.ID{0xA1, 0xA2}
|
|
payload := []byte("envelope-fuzz-payload")
|
|
|
|
unsigned, err := NewUnsignedMessage(networkID, chainID, payload)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
signers := NewBitSet()
|
|
signers.Add(0)
|
|
signers.Add(2)
|
|
var sig [bls.SignatureLen]byte
|
|
copy(sig[:], bytes.Repeat([]byte{0xAB}, bls.SignatureLen))
|
|
bsig := NewBitSetSignature(signers, sig)
|
|
msg, err := NewMessage(unsigned, bsig)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
env := &EnvelopeV2{
|
|
Message: msg,
|
|
SourceKeyEraID: eraID,
|
|
SourceGeneration: generation,
|
|
HashSuiteID: DefaultHashSuiteID,
|
|
SourceNebulaRoot: [32]byte{0xDE, 0xAD},
|
|
}
|
|
if withPulse {
|
|
env.PulsarPulse = bytes.Repeat([]byte{0x42}, 64)
|
|
}
|
|
if withCertSet {
|
|
env.MLDSACertSet = bytes.Repeat([]byte{0xC3}, 192)
|
|
}
|
|
out, err := env.Bytes()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// makeFuzzSeedV1 returns the v1 wire bytes for a representative
|
|
// Message. ParseEnvelope must accept these via the legacy path and
|
|
// return an EnvelopeV2 with only the Beam lane populated.
|
|
func makeFuzzSeedV1() []byte {
|
|
const networkID = uint32(1)
|
|
chainID := ids.ID{0xC1, 0xC2}
|
|
payload := []byte("envelope-fuzz-v1-payload")
|
|
|
|
unsigned, err := NewUnsignedMessage(networkID, chainID, payload)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
signers := NewBitSet()
|
|
signers.Add(0)
|
|
var sig [bls.SignatureLen]byte
|
|
copy(sig[:], bytes.Repeat([]byte{0xCD}, bls.SignatureLen))
|
|
bsig := NewBitSetSignature(signers, sig)
|
|
msg, err := NewMessage(unsigned, bsig)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return msg.Bytes()
|
|
}
|