Files
warp/envelope_negative_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

245 lines
8.1 KiB
Go

// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// Package warp — Gate 4 negative-transcript tests over the Warp 2.0
// envelope's Pulsar-bound fields (Mar-3-2026 PQ Consensus Architecture
// Freeze).
//
// The Warp 2.0 envelope binds the Pulse signature to the source
// chain's transcript via the v2 fields:
//
// SourceNebulaRoot [32]byte
// SourceKeyEraID uint64
// SourceGeneration uint64
// HashSuiteID string (defaults to "Pulsar-SHA3" when empty)
// ImplementationVersion is NOT a field on EnvelopeV2 itself (the
// destination-chain key registry's resolver-supplied profile string
// plays an analogous role; receiver-side string-equality is tested
// in warp/pulsar/hashsuite_mismatch_test.go).
//
// For each transcript-binding field we mutate the envelope after
// "signing" (we use a fixed-byte-equality model in line with
// pulsar/reshare/negative_transcript_test.go's honestThresholdVerifier
// abstraction) and assert that:
//
// 1. The canonical signing bytes change.
// 2. The honest verifier rejects the mutated envelope.
//
// Citations (canonical proof bucket):
//
// proofs/definitions/transcript-binding.tex
// Definition ref:warp-v2-envelope
// proofs/pulsar/hash-suite-separation.tex
// Theorem ref:hash-suite-separation
package warp
import (
"bytes"
"testing"
"github.com/luxfi/crypto/bls"
"github.com/luxfi/ids"
"github.com/stretchr/testify/require"
)
// negEnvelopeFixture builds a fully-populated v2 envelope used as the
// baseline for transcript-mutation tests. Every Pulsar-bound field is
// non-zero so a mutation cannot accidentally land on the same value.
func negEnvelopeFixture(t *testing.T) *EnvelopeV2 {
t.Helper()
const networkID = uint32(1)
chainID := ids.ID{0xA1, 0xA2, 0xA3, 0xA4}
payload := []byte("envelope-negative-test-payload")
unsigned, err := NewUnsignedMessage(networkID, chainID, payload)
require.NoError(t, err)
signers := NewBitSet()
signers.Add(0)
signers.Add(2)
signers.Add(4)
var sigBytes [bls.SignatureLen]byte
copy(sigBytes[:], bytes.Repeat([]byte{0xAB}, bls.SignatureLen))
v1Sig := NewBitSetSignature(signers, sigBytes)
v1, err := NewMessage(unsigned, v1Sig)
require.NoError(t, err)
return &EnvelopeV2{
Message: v1,
SourceNebulaRoot: [32]byte{0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE},
SourceKeyEraID: 7,
SourceGeneration: 11,
HashSuiteID: DefaultHashSuiteID,
PulsarPulse: bytes.Repeat([]byte{0xCC}, 64),
}
}
// canonicalSigningBytes mimics warp/pulsar.BuildSigningBytes locally
// (without importing the subpackage, to avoid an import cycle with
// the test). It captures every Pulsar-transcript-bound field on the
// envelope so a single-field mutation flips the bytes.
func canonicalSigningBytes(env *EnvelopeV2) []byte {
const sigPrefix = "WARP-PULSAR-ENVELOPE-v1"
suite := env.HashSuiteOrDefault()
src := env.Message.UnsignedMessage.SourceChainID
msg := env.Message.UnsignedMessage.Bytes()
out := make([]byte, 0, len(sigPrefix)+32+32+8+8+4+len(suite)+4+len(msg))
out = append(out, []byte(sigPrefix)...)
out = append(out, src[:]...)
out = append(out, env.SourceNebulaRoot[:]...)
be := func(v uint64) []byte {
var b [8]byte
for i := 0; i < 8; i++ {
b[7-i] = byte(v >> (8 * i))
}
return b[:]
}
out = append(out, be(env.SourceKeyEraID)...)
out = append(out, be(env.SourceGeneration)...)
be32 := func(v uint32) []byte {
var b [4]byte
for i := 0; i < 4; i++ {
b[3-i] = byte(v >> (8 * i))
}
return b[:]
}
out = append(out, be32(uint32(len(suite)))...)
out = append(out, []byte(suite)...)
out = append(out, be32(uint32(len(msg)))...)
out = append(out, msg...)
return out
}
// envHonestVerifier returns a closure that mimics the behaviour of a
// real Pulsar.Verify under an unchanged GroupKey: it accepts iff the
// envelope's canonical signing bytes equal the baseline's.
func envHonestVerifier(baseline *EnvelopeV2) func(env *EnvelopeV2) bool {
baselineBytes := canonicalSigningBytes(baseline)
return func(env *EnvelopeV2) bool {
return bytes.Equal(canonicalSigningBytes(env), baselineBytes)
}
}
// envMutateField returns a copy of the baseline envelope with exactly
// one Pulsar-transcript field mutated. The field name MUST be one of
// the Warp-v2 transcript fields tracked by Gate 4 ("nebula_root",
// "key_era_id", "generation", "hash_suite_id") plus the source-chain
// payload-derived "source_chain_id" (mutated by re-creating the v1
// UnsignedMessage with a different chain id).
func envMutateField(t *testing.T, base *EnvelopeV2, field string) *EnvelopeV2 {
t.Helper()
cp := *base
cp.Message = base.Message
switch field {
case "nebula_root":
cp.SourceNebulaRoot = [32]byte{0x99, 0x88, 0x77, 0x66}
case "key_era_id":
cp.SourceKeyEraID = base.SourceKeyEraID + 1000
case "generation":
cp.SourceGeneration = base.SourceGeneration + 1000
case "hash_suite_id":
// Default-empty fallback to "Pulsar-SHA3" makes a non-empty
// distinct value the only safe mutation.
cp.HashSuiteID = "Pulsar-BLAKE3"
case "source_chain_id":
// Mutate the embedded UnsignedMessage's SourceChainID via a
// fresh Message — this is observable through canonical
// signing bytes via the source-chain id binding.
mu, err := NewUnsignedMessage(
base.Message.UnsignedMessage.NetworkID,
ids.ID{0xCA, 0xFE, 0xCA, 0xFE},
base.Message.UnsignedMessage.Payload,
)
require.NoError(t, err)
newMsg, err := NewMessage(mu, base.Message.Signature)
require.NoError(t, err)
cp.Message = newMsg
default:
t.Fatalf("unknown envelope field: %q", field)
}
return &cp
}
// TestWarpEnvelopeNegativeTranscriptMutationsRejected — one subtest
// per Pulsar-transcript-bound field. Mutate, recompute canonical
// signing bytes, assert the bytes changed and the honest verifier
// rejects.
func TestWarpEnvelopeNegativeTranscriptMutationsRejected(t *testing.T) {
fields := []string{
"nebula_root",
"key_era_id",
"generation",
"hash_suite_id",
"source_chain_id",
}
base := negEnvelopeFixture(t)
baselineBytes := canonicalSigningBytes(base)
verify := envHonestVerifier(base)
// Sanity: the baseline verifies cleanly.
require.True(t, verify(base))
for _, f := range fields {
t.Run(f, func(t *testing.T) {
mutated := envMutateField(t, base, f)
mBytes := canonicalSigningBytes(mutated)
if bytes.Equal(mBytes, baselineBytes) {
t.Fatalf("mutation of %q did not change signing bytes", f)
}
if verify(mutated) {
t.Fatalf("honest verifier accepted mutated %q field", f)
}
})
}
}
// TestWarpEnvelopeFieldsMutationsDistinct — orthogonality check: no
// two single-field mutations of the v2 envelope collide on canonical
// signing bytes.
func TestWarpEnvelopeFieldsMutationsDistinct(t *testing.T) {
fields := []string{
"nebula_root",
"key_era_id",
"generation",
"hash_suite_id",
"source_chain_id",
}
base := negEnvelopeFixture(t)
seen := make(map[string]string, len(fields))
for _, f := range fields {
mutated := envMutateField(t, base, f)
key := string(canonicalSigningBytes(mutated))
if prev, ok := seen[key]; ok {
t.Fatalf("envelope signing-bytes collision: %q and %q produce same bytes", prev, f)
}
seen[key] = f
}
}
// TestWarpEnvelopeHashSuiteDefaultBindsExplicitly — even though
// HashSuiteOrDefault returns "Pulsar-SHA3" for both empty-string and
// "Pulsar-SHA3" envelopes, the canonical signing bytes are computed
// from HashSuiteOrDefault, so two envelopes that resolve to the same
// suite produce the same signing bytes (this is the COMPATIBILITY
// behaviour). What we test here is that an envelope with an EXPLICIT
// non-default suite produces DIFFERENT signing bytes from the
// default-resolved one — i.e. the resolver's view always matches the
// declared field when the field is non-empty.
func TestWarpEnvelopeHashSuiteDefaultBindsExplicitly(t *testing.T) {
base := negEnvelopeFixture(t)
defaultBytes := canonicalSigningBytes(base)
cp := *base
cp.HashSuiteID = "Pulsar-BLAKE3"
mutatedBytes := canonicalSigningBytes(&cp)
if bytes.Equal(defaultBytes, mutatedBytes) {
t.Fatal("explicit non-default HashSuiteID failed to change signing bytes")
}
}