Files
pq/mode_test.go
zeekay 42cd50a804 pq: canonical Mode (classical / hybrid / strict-pq)
Decomplects the strict-PQ posture from the per-package
SecurityProfile types that previously lived in luxfi/warp,
luxfi/zap, lux/dex, luxfi/fhe. Each of those redeclared the same
three constants + helpers + ErrClassicalAuthForbidden sentinel,
which forced cross-package callers to convert between equivalent
types and prevented errors.Is from working across boundaries.

The decomplected design:

  VALUE — lives in lux/pq:
    Mode (Classical / Hybrid / StrictPQ)
    ModeFromPQFlag(bool) Mode
    ModeFromString(string) (Mode, error)
    (Mode).IsPostQuantum() bool
    (Mode).IsPQAware() bool
    (Mode).Profile() *Profile          ← bridge to precompile forbid-bits
    ErrClassicalAuthForbidden          ← umbrella sentinel, errors.Is works everywhere

  PLACES — stay in each consumer:
    warp.RequireMLDSACertSetForMode(mode, envelope) error
    zap.RequireAttestationForMode(mode, attestation, ctx, verify) error
    dex.VerifyOrderForMode(mode, order) error
    fhe.DefaultParamsForMode(mode) ParametersLiteral
    evm.txpool admits MLDSATxType only when chainConfig.PQ != nil
                       (which lux/pq.Mode.Profile() populates at vm.Initialize)

Each consumer keeps its own RequireFooForMode because the INPUTS
differ (envelope / order / precompile-op / FHE-params), but they
all dispatch on the SAME pq.Mode type and return the SAME
pq.ErrClassicalAuthForbidden sentinel. Audit pipelines grep one
identifier across every refusal site.

Mode.Profile() is the bridge to the existing pq.Profile
(precompile forbid-bits, from profile.go): ModeStrictPQ →
Strict(), ModeClassical + ModeHybrid → Permissive(). Hybrid
validates PQ authentication at higher layers but doesn't refuse
classical primitives inside contracts.

Tests pin the predicate matrix (4 modes × 3 predicates), the
Mode → Profile bridge, ModeFromString case-sensitivity, and
errors.Is wrapping semantics.

Next: migrate warp, zap, dex, fhe, cli to import pq.Mode in
place of their local SecurityProfile types.
2026-05-12 19:08:05 -07:00

105 lines
3.2 KiB
Go

// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package pq
import (
"errors"
"testing"
)
func TestMode_StringAndPredicates(t *testing.T) {
for _, tc := range []struct {
mode Mode
wantString string
wantPostQuantum bool
wantPQAware bool
}{
{ModeClassical, "classical", false, false},
{ModeHybrid, "hybrid", false, true},
{ModeStrictPQ, "strict-pq", true, true},
{Mode(99), "unknown", false, false},
} {
if tc.mode.String() != tc.wantString {
t.Errorf("Mode(%d).String() = %q, want %q",
tc.mode, tc.mode.String(), tc.wantString)
}
if tc.mode.IsPostQuantum() != tc.wantPostQuantum {
t.Errorf("%s.IsPostQuantum() = %t, want %t",
tc.mode, tc.mode.IsPostQuantum(), tc.wantPostQuantum)
}
if tc.mode.IsPQAware() != tc.wantPQAware {
t.Errorf("%s.IsPQAware() = %t, want %t",
tc.mode, tc.mode.IsPQAware(), tc.wantPQAware)
}
}
}
// TestMode_ProfileLink pins the bridge between the higher-level
// named mode and the lower-level precompile forbid-bits. The
// invariant: strict-PQ mode installs Strict() at the EVM
// precompile layer; classical and hybrid install Permissive().
func TestMode_ProfileLink(t *testing.T) {
if p := ModeStrictPQ.Profile(); !p.ForbidEcrecover || !p.ForbidBLS12381 {
t.Error("ModeStrictPQ.Profile() did not return Strict()")
}
for _, m := range []Mode{ModeClassical, ModeHybrid} {
p := m.Profile()
if p.ForbidEcrecover || p.ForbidBLS12381 {
t.Errorf("%s.Profile() returned Strict() (want Permissive())", m)
}
}
}
func TestModeFromPQFlag(t *testing.T) {
if got := ModeFromPQFlag(true); got != ModeStrictPQ {
t.Errorf("ModeFromPQFlag(true) = %s, want strict-pq", got)
}
if got := ModeFromPQFlag(false); got != ModeClassical {
t.Errorf("ModeFromPQFlag(false) = %s, want classical", got)
}
}
func TestModeFromString(t *testing.T) {
for _, tc := range []struct {
input string
want Mode
wantErr bool
}{
{"classical", ModeClassical, false},
{"hybrid", ModeHybrid, false},
{"strict-pq", ModeStrictPQ, false},
{"", 0, true},
{"PQ", 0, true},
{"strict_pq", 0, true},
{"Strict-PQ", 0, true}, // case sensitive
} {
got, err := ModeFromString(tc.input)
if (err != nil) != tc.wantErr {
t.Errorf("ModeFromString(%q) err=%v, wantErr=%t",
tc.input, err, tc.wantErr)
continue
}
if !tc.wantErr && got != tc.want {
t.Errorf("ModeFromString(%q) = %s, want %s",
tc.input, got, tc.want)
}
}
}
func TestErrClassicalAuthForbidden_IsCheckable(t *testing.T) {
wrapped := errors.New("envelope rejected: " + ErrClassicalAuthForbidden.Error())
// Direct sentinel comparison via errors.Is works because the
// sentinel is a package-level var. Downstream packages wrap
// it with fmt.Errorf("...: %w", pq.ErrClassicalAuthForbidden);
// errors.Is unwraps and matches.
if errors.Is(wrapped, ErrClassicalAuthForbidden) {
t.Error("errors.Is matched on string-wrapped (not %w-wrapped) error — that's wrong, this test pins the negative case")
}
// Positive: %w-wrapped errors should unwrap correctly.
formatted := errors.Join(ErrClassicalAuthForbidden, errors.New("at gate X"))
if !errors.Is(formatted, ErrClassicalAuthForbidden) {
t.Error("errors.Is failed to match a Join-wrapped sentinel")
}
}