Files
warp/envelope_negative_test.go
zeekay 443aa53f81 feat(warp)!: typed finality-evidence model (Beam/Pulsar/Corona/P3Q) + Corona relabel
Decomplect the PQ finality lanes into a typed, impossible-to-confuse model where
KIND chooses the verifier and SUITE only chooses parameterization (fail-closed on
mismatch). Kills the prior conflation where the corona-backed lattice lane was
mislabeled "Pulse/Pulsar-SHA3".

Four strict evidence kinds — each its own verifier + suite + key registry:
  beam-bls                BLS12-381 aggregate (fast classical)
  pulsar-threshold-mldsa  threshold ML-DSA, verified as a STANDARD FIPS-204
                          ML-DSA signature under a group public key; the TALUS
                          dealerless machinery is entirely offline (VerifyPulsar
                          imports only mldsa65 — enforced by an import-hygiene test)
  corona-ringtail         Ringtail/Module-LWE lattice threshold (the corona kernel)
  p3q-mldsa-rollup        succinct rollup of independent ML-DSA-65 sigs; strict
                          ONLY when its proof system is PQ (classical SNARK gated)
Plus the raw mldsa-cert-set as a NON-strict availability/audit artifact.

Subject-agnostic verifiers take (subject, evidence, keyEra): one dispatch serves
the warp cross-chain digest D and the consensus finality subject
  M = keccak256("QUASAR_FINALITY_V1" || chainID || height || round || blockID ||
      stateRoot || signerSetID || keyEraID || generation || pChainHeight || policyID)
folding generation + pChainHeight + policyID so a signature binds its era, its
validator snapshot, and its tier (tier-downgrade replay fails closed).

Policy tiers (BLS_FAST / HYBRID_PQ_CHECKPOINT / STRICT_QUASAR / RECOVERY) select
which lanes are required — orthogonal to verification and to keys. PulsarKeyEra
and CoronaKeyEra are symmetric but DISTINCT registry types; P3Q/cert-set resolve a
per-validator SignerSetAuthority — no resolver can alias another's key material.
Offline signers (pulsard, coronad, P3Q prover) are interfaces only, never imported
by the verify path.

Wire unchanged: D, the Message c14n, messageDST, the ZAP magic and kind bytes are
byte-identical (forge Go<->Solidity equality holds; golden envelope SHA unchanged).
The corona lane DST is renamed pulseDST->coronaDST (no deployed corona sigs;
envelope KAT regenerated).

22 tests green: non-confusable dispatch, suite/kind fail-closed, Pulsar import
hygiene, subject-identity across lanes, policyID downgrade, key-era distinctness,
P3Q-classical-root strict gate.
2026-06-27 13:47:13 -07:00

117 lines
3.6 KiB
Go

// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// envelope_negative_test.go — transcript-binding tests over the Message
// fields the digest D commits to. Under the ZAP fork the signed subject is
// D = keccak256("LUX-WARP-ZAP-CORE-v1" ‖ zap_c14n(Message)); folding the
// PQ lineage into the message means every lane (BLS Beam included) binds it.
// For each transcript field we mutate the message and assert:
//
// 1. D changes.
// 2. An honest verifier (D-equality model) rejects the mutated message.
// 3. No two single-field mutations collide on D (orthogonality).
package warp
import (
"testing"
"github.com/luxfi/ids"
"github.com/stretchr/testify/require"
)
func negMessageFixture(t *testing.T) *Message {
t.Helper()
return &Message{
NetworkID: 1,
SourceChainID: ids.ID{0xA1, 0xA2, 0xA3, 0xA4},
SourceNebulaRoot: [32]byte{0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE},
SourceKeyEraID: 7,
SourceGeneration: 11,
HashSuiteID: MessageHashProfileTag,
Payload: []byte("envelope-negative-test-payload"),
}
}
// honestVerifier models a real lane verifier under an unchanged key: it
// accepts iff the candidate message's D equals the baseline's.
func honestVerifier(baseline *Message) func(c *Message) bool {
d := baseline.ID()
return func(c *Message) bool { return c.ID() == d }
}
func negMutateField(t *testing.T, base *Message, field string) *Message {
t.Helper()
c := *base
switch field {
case "network_id":
c.NetworkID = base.NetworkID + 1000
case "source_chain_id":
c.SourceChainID = ids.ID{0xCA, 0xFE, 0xCA, 0xFE}
case "nebula_root":
c.SourceNebulaRoot = [32]byte{0x99, 0x88, 0x77, 0x66}
case "key_era_id":
c.SourceKeyEraID = base.SourceKeyEraID + 1000
case "generation":
c.SourceGeneration = base.SourceGeneration + 1000
case "hash_suite_id":
c.HashSuiteID = "Pulsar-BLAKE3"
case "payload":
c.Payload = append([]byte("X"), base.Payload...)
default:
t.Fatalf("unknown message field: %q", field)
}
return &c
}
var negFields = []string{
"network_id",
"source_chain_id",
"nebula_root",
"key_era_id",
"generation",
"hash_suite_id",
"payload",
}
// TestMessageTranscriptMutationsRejected mutates each field and asserts D
// changes and the honest verifier rejects.
func TestMessageTranscriptMutationsRejected(t *testing.T) {
base := negMessageFixture(t)
baseID := base.ID()
verify := honestVerifier(base)
require.True(t, verify(base), "baseline must verify")
for _, f := range negFields {
t.Run(f, func(t *testing.T) {
mutated := negMutateField(t, base, f)
require.NotEqual(t, baseID, mutated.ID(), "mutation of %q did not change D", f)
require.False(t, verify(mutated), "honest verifier accepted mutated %q", f)
})
}
}
// TestMessageTranscriptMutationsDistinct is the orthogonality check: no two
// single-field mutations collide on D.
func TestMessageTranscriptMutationsDistinct(t *testing.T) {
base := negMessageFixture(t)
seen := make(map[ids.ID]string, len(negFields))
for _, f := range negFields {
id := negMutateField(t, base, f).ID()
if prev, ok := seen[id]; ok {
t.Fatalf("D collision: %q and %q produce the same digest", prev, f)
}
seen[id] = f
}
}
// TestHashSuiteExplicitVsEmptyBindDistinctly proves an explicit non-default
// suite produces a different D from the resolved-default message — the suite
// is bound verbatim, no sign-time defaulting.
func TestHashSuiteExplicitVsEmptyBindDistinctly(t *testing.T) {
base := negMessageFixture(t)
cp := *base
cp.HashSuiteID = "Pulsar-BLAKE3"
require.NotEqual(t, base.ID(), cp.ID())
}