bridge: enforce LUX_STRICT_PQ_BRIDGE profile

Adds BridgeProfile + LuxStrictPQBridgeProfile (Pulsar-M-65 on both sides,
STARK_FRI_SHA3_PQ proofs, ML-DSA-87 admin, ML-DSA-65 multisig pause).
Forbids ECDSA admin, BLS aggregate, KZG commitments, Groth16 wrappers,
and pairing precompiles in strict mode.

post_quantum_end_to_end label exposed via RPC + block-explorer metadata.

Classical-compat bridges remain available under BridgeClassicalCompat
profile but are explicitly labeled non-E2E-PQ.

Integration sites:
- bridge/profile.go is the bridge-package audit gate (mirrors consensus
  type, owns the bridge_classical_compat_total counter).
- opnet-watcher/signer.go: SignRaw, SignTransaction, signEthMessage now
  consult profile.RefuseClassicalAdmin before any ecrecover / EIP-191 /
  secp256k1 sign — strict-PQ refuses, classical-compat increments
  the metric.
- cmd/bridge/api.go: exposes bridge_getProfile JSON-RPC, GET
  /v1/bridge/profile, and /metrics (Prometheus, including
  bridge_classical_compat_total{primitive=...}).
- --profile flag (BRIDGE_PROFILE env) selects strict-pq | classical-compat
  on both cmd/bridge and opnet-watcher.

Patch-bump.
This commit is contained in:
2026-05-10 20:07:49 -07:00
parent c09467d293
commit 7d738a4056
8 changed files with 999 additions and 5 deletions
+118 -3
View File
@@ -2,20 +2,38 @@ package main
import (
"encoding/json"
"fmt"
"net/http"
"net/http/httputil"
"net/url"
"strings"
"github.com/luxfi/bridge"
)
// API serves the bridge HTTP surface. Read paths (networks, tokens,
// exchanges, limits) are answered natively from Config — no Postgres
// hit. MPC-heavy paths (quote, rate, swaps, explorer) reverse-proxy to
// the legacy Node backend at backendURL until the native port lands.
// exchanges, limits, profile) are answered natively from Config — no
// Postgres hit. MPC-heavy paths (quote, rate, swaps, explorer)
// reverse-proxy to the legacy Node backend at backendURL until the
// native port lands.
//
// The bridge profile (BridgeProfile) is the audit-visible label that
// names which classical primitives are gated under the current
// posture. It is exposed via:
//
// GET /v1/bridge/profile — current bridge profile
// POST /v1/bridge/rpc — JSON-RPC: bridge_getProfile
// GET /metrics — Prometheus, including
// bridge_classical_compat_total
//
// Every deposit / withdrawal record SHOULD carry the BridgeProfile.ProfileID
// so an auditor can later distinguish a strict-PQ deposit from a
// classical-compat one.
type API struct {
cfg Config
backend string
proxy *httputil.ReverseProxy
profile *bridge.BridgeProfile
}
func NewAPI(cfg Config, backendURL string) *API {
@@ -27,9 +45,25 @@ func NewAPI(cfg Config, backendURL string) *API {
a.proxy.Director = stripPathPrefix(u, "/v1/bridge")
}
}
// Default posture: classical-compat. cmd/bridge is the user-facing
// bridge UI; the canonical destination-side proof verifier sits on
// Z-Chain. Operators MAY pin LuxStrictPQBridgeProfile via the
// BRIDGE_PROFILE env var (handled in main.go).
p := bridge.BridgeClassicalCompat
a.profile = &p
return a
}
// SetProfile pins the bridge profile for this API instance. Called from
// main.go after config + flags are parsed; the default is
// BridgeClassicalCompat (the user-facing bridge UI talks to external
// L1s on classical primitives).
func (a *API) SetProfile(p *bridge.BridgeProfile) {
if p != nil {
a.profile = p
}
}
// Register mounts handlers on the given mux. The /v1/bridge prefix matches
// what the SPA fetches and what hanzo/ingress routes externally.
func (a *API) Register(mux *http.ServeMux) {
@@ -37,6 +71,13 @@ func (a *API) Register(mux *http.ServeMux) {
mux.HandleFunc("/v1/bridge/tokens", a.tokens)
mux.HandleFunc("/v1/bridge/exchanges", a.exchanges)
mux.HandleFunc("/v1/bridge/limits", a.limits)
mux.HandleFunc("/v1/bridge/profile", a.profileGET)
// JSON-RPC surface for bridge_getProfile (and future bridge_* methods).
mux.HandleFunc("/v1/bridge/rpc", a.rpc)
// Prometheus metrics including bridge_classical_compat_total.
mux.HandleFunc("/metrics", a.metrics)
// Proxied (require Node backend). When backend is unset these 503
// to make the missing dependency obvious instead of silently 404ing.
@@ -74,6 +115,80 @@ func (a *API) limits(w http.ResponseWriter, r *http.Request) {
writeJSON(w, a.cfg.Limits)
}
// profileGET answers GET /v1/bridge/profile with the active bridge
// profile metadata (REST mirror of the bridge_getProfile RPC).
func (a *API) profileGET(w http.ResponseWriter, r *http.Request) {
writeJSON(w, a.profile.Metadata())
}
// jsonrpcReq is the request shape for /v1/bridge/rpc. Minimal JSON-RPC
// 2.0 — we only implement bridge_getProfile here; future bridge_*
// methods land in this switch.
type jsonrpcReq struct {
JSONRPC string `json:"jsonrpc"`
ID json.RawMessage `json:"id"`
Method string `json:"method"`
Params json.RawMessage `json:"params"`
}
// jsonrpcResp is the response shape. result xor error.
type jsonrpcResp struct {
JSONRPC string `json:"jsonrpc"`
ID json.RawMessage `json:"id,omitempty"`
Result any `json:"result,omitempty"`
Error *jsonrpcError `json:"error,omitempty"`
}
type jsonrpcError struct {
Code int `json:"code"`
Message string `json:"message"`
}
// rpc dispatches /v1/bridge/rpc calls. Currently:
//
// bridge_getProfile — returns the active BridgeProfile.Metadata
func (a *API) rpc(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, `{"error":"method_not_allowed"}`, http.StatusMethodNotAllowed)
return
}
var req jsonrpcReq
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeJSON(w, jsonrpcResp{JSONRPC: "2.0", Error: &jsonrpcError{Code: -32700, Message: "parse error"}})
return
}
switch req.Method {
case "bridge_getProfile":
writeJSON(w, jsonrpcResp{JSONRPC: "2.0", ID: req.ID, Result: a.profile.Metadata()})
default:
writeJSON(w, jsonrpcResp{JSONRPC: "2.0", ID: req.ID, Error: &jsonrpcError{Code: -32601, Message: "method not found"}})
}
}
// metrics serves Prometheus text exposition format. The bridge module
// surfaces bridge_classical_compat_total{primitive=...} so operators can
// alert on a classical-compat traversal spike.
func (a *API) metrics(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain; version=0.0.4")
totals := bridge.ClassicalCompatTotal()
// HELP / TYPE lines first.
_, _ = w.Write([]byte("# HELP bridge_classical_compat_total Count of classical-compat gate traversals broken down by primitive.\n"))
_, _ = w.Write([]byte("# TYPE bridge_classical_compat_total counter\n"))
for _, prim := range []string{"admin", "bls_aggregate", "kzg", "groth16", "pairing"} {
fmt.Fprintf(w, "bridge_classical_compat_total{profile=%q,primitive=%q} %d\n",
a.profile.Name, prim, totals[prim])
}
// Profile posture is observable as a label-only gauge so dashboards
// can colour-code strict-PQ vs classical-compat.
pq := 0
if a.profile.IsPostQuantumEndToEnd() {
pq = 1
}
_, _ = w.Write([]byte("# HELP bridge_profile_post_quantum_end_to_end 1 iff the active bridge profile is labelled E2E-PQ.\n"))
_, _ = w.Write([]byte("# TYPE bridge_profile_post_quantum_end_to_end gauge\n"))
fmt.Fprintf(w, "bridge_profile_post_quantum_end_to_end{profile=%q} %d\n", a.profile.Name, pq)
}
func (a *API) proxied(w http.ResponseWriter, r *http.Request) {
if a.proxy == nil {
http.Error(w, `{"error":"backend_unavailable","detail":"set BRIDGE_BACKEND_URL to enable swap/quote/explorer routes"}`, http.StatusServiceUnavailable)
+34 -1
View File
@@ -32,6 +32,8 @@ import (
"os/signal"
"syscall"
"time"
"github.com/luxfi/bridge"
)
var version = "dev"
@@ -41,6 +43,8 @@ func main() {
addr := flag.String("addr", envOr("BRIDGE_ADDR", ":8080"), "listen address")
backend := flag.String("backend", envOr("BRIDGE_BACKEND_URL", ""), "legacy Node backend URL for proxied routes (empty disables proxy)")
staticDir := flag.String("static", envOr("BRIDGE_STATIC_DIR", ""), "override embedded SPA from disk")
profileFlag := flag.String("profile", envOr("BRIDGE_PROFILE", "classical-compat"),
"bridge security profile: strict-pq | classical-compat")
showVersion := flag.Bool("version", false, "print version and exit")
flag.Parse()
@@ -49,6 +53,16 @@ func main() {
return
}
// Resolve the bridge profile. Default is classical-compat (the
// user-facing UI talks to external L1s); operators pin
// strict-pq for an internal Lux↔Lux bridge.
profile, err := selectProfile(*profileFlag)
if err != nil {
log.Fatalf("bridge profile: %v", err)
}
log.Printf("bridge profile: %s (post_quantum_end_to_end=%t)",
profile.Name, profile.IsPostQuantumEndToEnd())
cfg, err := LoadConfig(*cfgPath)
if err != nil {
log.Fatalf("load config: %v", err)
@@ -60,12 +74,14 @@ func main() {
}
api := NewAPI(cfg, *backend)
api.SetProfile(profile)
mux := http.NewServeMux()
api.Register(mux)
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{"status":"ok","version":%q,"backend_proxy":%t}`, version, *backend != "")
fmt.Fprintf(w, `{"status":"ok","version":%q,"backend_proxy":%t,"profile":%q,"post_quantum_end_to_end":%t}`,
version, *backend != "", profile.Name, profile.IsPostQuantumEndToEnd())
})
mux.Handle("/", frontend)
@@ -98,3 +114,20 @@ func envOr(k, fallback string) string {
}
return fallback
}
// selectProfile resolves the --profile flag value to a bridge profile
// pointer. Refuses an unknown value rather than silently defaulting:
// an operator who typos the profile name must see the error rather
// than ship under the wrong posture.
func selectProfile(name string) (*bridge.BridgeProfile, error) {
switch name {
case "strict-pq", "lux-strict-pq", "lux-strict-pq-bridge":
p := bridge.LuxStrictPQBridgeProfile
return &p, nil
case "classical-compat", "bridge-classical-compat-unsafe":
p := bridge.BridgeClassicalCompat
return &p, nil
default:
return nil, fmt.Errorf("unknown bridge profile %q (valid: strict-pq, classical-compat)", name)
}
}
+6
View File
@@ -3,10 +3,16 @@ module github.com/luxfi/bridge/opnet-watcher
go 1.26.1
require (
github.com/luxfi/bridge v0.0.0-00010101000000-000000000000
github.com/luxfi/crypto v1.17.55
gopkg.in/yaml.v3 v3.0.1
)
// The bridge top-level module (BridgeProfile audit gate) is co-located
// in this repository; consume it via a local replace until the next
// patch tag is published.
replace github.com/luxfi/bridge => ../
require (
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260215031811-a0ab0b218a81 // indirect
github.com/cloudflare/circl v1.6.3 // indirect
+40
View File
@@ -22,6 +22,7 @@ package main
import (
"context"
"flag"
"fmt"
"log"
"net/http"
"os"
@@ -29,6 +30,7 @@ import (
"syscall"
"time"
"github.com/luxfi/bridge"
"github.com/luxfi/bridge/opnet-watcher/plugins"
"github.com/luxfi/crypto/threshold"
)
@@ -68,12 +70,22 @@ func main() {
flag.StringVar(&cfg.SourceRPCURL, "opnet-rpc", "", "OP_NET node JSON-RPC URL (legacy, use --source-rpc)")
flag.StringVar(&cfg.SourceBridgeAddr, "opnet-bridge-addr", "", "OP_NET bridge address (legacy, use --source-addr)")
profileName := flag.String("profile", envOrDefault("BRIDGE_PROFILE", "classical-compat"),
"bridge security profile: strict-pq | classical-compat")
flag.Parse()
if err := cfg.Validate(); err != nil {
log.Fatalf("invalid config: %v", err)
}
bridgeProfile, err := selectBridgeProfile(*profileName)
if err != nil {
log.Fatalf("bridge profile: %v", err)
}
log.Printf("bridge profile: %s (post_quantum_end_to_end=%t)",
bridgeProfile.Name, bridgeProfile.IsPostQuantumEndToEnd())
// Select chain plugin.
plugin := selectPlugin(chainName, cfg)
@@ -113,6 +125,16 @@ func main() {
log.Printf("chain=%s chain_id=%d signer=0x%x mode=single-key-DEV-ONLY", plugin.Name(), plugin.ChainID(), signer.Address())
}
// Pin the bridge security profile on both signers. opnet-watcher
// is inherently a classical-compat path (Teleporter ECDSA); a
// strict-PQ profile here causes every signing call to refuse with
// ErrBridgeProfileForbidden — exactly the audit posture we want
// when running this binary under LUX_STRICT_E2E_PQ.
signer.SetProfile(bridgeProfile)
if txSigner != signer {
txSigner.SetProfile(bridgeProfile)
}
relay := NewRelay(cfg.LuxRPCURL, cfg.TeleporterAddr, signer, txSigner, cfg.LuxChainIDOverride)
watcher := NewWatcher(cfg, plugin, signer, relay)
@@ -170,6 +192,24 @@ func envOrDefault(key, fallback string) string {
return fallback
}
// selectBridgeProfile resolves the --profile flag value to a bridge
// profile pointer. opnet-watcher is inherently a classical-compat
// signer (Teleporter ECDSA); the strict-pq option is provided so an
// operator can run an audit drill (every signing call refuses) and
// verify the bridge_classical_compat_total metric goes to zero.
func selectBridgeProfile(name string) (*bridge.BridgeProfile, error) {
switch name {
case "strict-pq", "lux-strict-pq", "lux-strict-pq-bridge":
p := bridge.LuxStrictPQBridgeProfile
return &p, nil
case "classical-compat", "bridge-classical-compat-unsafe":
p := bridge.BridgeClassicalCompat
return &p, nil
default:
return nil, fmt.Errorf("unknown bridge profile %q (valid: strict-pq, classical-compat)", name)
}
}
// evmChains maps chain names to their Teleporter chain IDs.
// All use the generic EVMPlugin (eth_getLogs + eth_call).
var evmChains = map[string]uint64{
+55 -1
View File
@@ -9,6 +9,7 @@ import (
"fmt"
"math/big"
"github.com/luxfi/bridge"
"github.com/luxfi/crypto"
"github.com/luxfi/crypto/secp256k1"
"github.com/luxfi/crypto/threshold"
@@ -41,10 +42,23 @@ type Signer struct {
signer threshold.Signer
aggregator threshold.Aggregator
groupKey threshold.PublicKey
// profile pins the bridge security posture. opnet-watcher produces
// Teleporter-compatible ECDSA signatures and is therefore inherently
// a classical-compat path; every signing call gates through
// profile.RefuseClassicalAdmin and increments
// bridge_classical_compat_total on success. Strict-PQ profiles
// refuse the operation outright.
profile *bridge.BridgeProfile
}
// NewSigner creates a single-key signer for development.
// In production, use NewThresholdSigner with CGGMP21 key shares.
//
// The Signer is constructed with the BridgeClassicalCompat profile by
// default — opnet-watcher signs Teleporter-compatible ECDSA / threshold
// ECDSA. SetProfile may pin a different profile (e.g. strict-PQ during
// migration drills, which will refuse every sign call).
func NewSigner(keyBytes []byte) (*Signer, error) {
if len(keyBytes) != 32 {
return nil, fmt.Errorf("signer: key must be 32 bytes, got %d", len(keyBytes))
@@ -53,11 +67,14 @@ func NewSigner(keyBytes []byte) (*Signer, error) {
if err != nil {
return nil, fmt.Errorf("signer: invalid key: %w", err)
}
return &Signer{key: key}, nil
p := bridge.BridgeClassicalCompat
return &Signer{key: key, profile: &p}, nil
}
// NewThresholdSigner creates a threshold signer using the given scheme and key share.
// For Teleporter compatibility (ECDSA.recover), use threshold.SchemeCMP.
//
// Defaults to BridgeClassicalCompat; SetProfile overrides.
func NewThresholdSigner(schemeID threshold.SchemeID, shareBytes []byte, groupKeyBytes []byte) (*Signer, error) {
scheme, err := threshold.GetScheme(schemeID)
if err != nil {
@@ -84,14 +101,28 @@ func NewThresholdSigner(schemeID threshold.SchemeID, shareBytes []byte, groupKey
return nil, fmt.Errorf("signer: create aggregator: %w", err)
}
p := bridge.BridgeClassicalCompat
return &Signer{
scheme: scheme,
signer: signer,
aggregator: aggregator,
groupKey: groupKey,
profile: &p,
}, nil
}
// SetProfile pins the bridge security profile on this Signer. Called
// from main.go after the --profile flag is parsed. A strict-PQ profile
// causes every subsequent SignRaw / SignDeposit / SignBacking /
// SignTransaction call to refuse with ErrBridgeProfileForbidden; the
// operator must explicitly opt into classical-compat for opnet-watcher
// to produce Teleporter-compatible signatures.
func (s *Signer) SetProfile(p *bridge.BridgeProfile) {
if p != nil {
s.profile = p
}
}
// Address returns the Ethereum-style address derived from the signer's public key.
func (s *Signer) Address() [20]byte {
if s.key != nil {
@@ -114,7 +145,16 @@ func (s *Signer) IsThreshold() bool {
// SignRaw signs a raw hash. In single-key mode, uses the local key.
// In threshold mode, uses the threshold signing protocol.
// Returns a 65-byte [R || S || V] signature with V = 27 or 28.
//
// SECURITY GATE: opnet-watcher produces secp256k1 ECDSA signatures
// (Teleporter-compatible). Under a strict-PQ bridge profile, this call
// refuses with ErrBridgeProfileForbidden. Under classical-compat, the
// call proceeds and the bridge_classical_compat_total counter is
// incremented.
func (s *Signer) SignRaw(hash []byte) ([]byte, error) {
if err := s.profile.RefuseClassicalAdmin(); err != nil {
return nil, fmt.Errorf("signer: %w", err)
}
if s.key != nil {
return s.signSingleKey(hash)
}
@@ -124,7 +164,14 @@ func (s *Signer) SignRaw(hash []byte) ([]byte, error) {
// SignTransaction signs a raw EIP-155 transaction and returns the RLP-encoded signed tx.
// CRITICAL-03: All transactions are signed locally, never via eth_sendTransaction.
// NEW-02: Accepts an explicit tx-signing key so threshold signers can still submit txs.
//
// SECURITY GATE: EIP-155 transactions are ECDSA secp256k1. Under a
// strict-PQ profile this refuses; under classical-compat it proceeds
// and increments bridge_classical_compat_total{primitive="admin"}.
func (s *Signer) SignTransaction(unsignedRLP []byte, chainID uint64) ([]byte, error) {
if err := s.profile.RefuseClassicalAdmin(); err != nil {
return nil, fmt.Errorf("signer: %w", err)
}
if s.key == nil {
return nil, fmt.Errorf("signer: transaction signing requires single-key mode (use Relay.txKey for threshold)")
}
@@ -237,7 +284,14 @@ func (s *Signer) SignBacking(srcChainID uint64, totalBacking *big.Int, timestamp
}
// signEthMessage computes keccak256(data), wraps with EIP-191, signs.
//
// SECURITY GATE: EIP-191 messages are ECDSA secp256k1 (Teleporter
// expects ECDSA.recover()). Strict-PQ refuses; classical-compat
// proceeds and increments bridge_classical_compat_total.
func (s *Signer) signEthMessage(data []byte) ([]byte, error) {
if err := s.profile.RefuseClassicalAdmin(); err != nil {
return nil, fmt.Errorf("signer: %w", err)
}
msgHash := crypto.Keccak256(data)
prefix := []byte("\x19Ethereum Signed Message:\n32")
+129
View File
@@ -0,0 +1,129 @@
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package main
import (
"errors"
"math/big"
"testing"
"github.com/luxfi/bridge"
)
// makeTestKey returns a deterministic 32-byte dev key for signing
// tests. Test-only; never reused outside this file.
func makeTestKey() []byte {
k := make([]byte, 32)
for i := range k {
k[i] = byte(i + 1)
}
return k
}
// TestSigner_RefusesECDSAUnderStrictPQ — the spec test: a signer
// configured with the strict-PQ bridge profile MUST refuse every
// classical-admin sign call. SignRaw / SignDeposit / SignBacking /
// signEthMessage all consult the gate and return
// ErrBridgeProfileForbidden.
func TestSigner_RefusesECDSAUnderStrictPQ(t *testing.T) {
s, err := NewSigner(makeTestKey())
if err != nil {
t.Fatalf("NewSigner: %v", err)
}
p := bridge.LuxStrictPQBridgeProfile
s.SetProfile(&p)
// SignRaw must refuse.
if _, err := s.SignRaw(make([]byte, 32)); err == nil {
t.Errorf("SignRaw under strict-PQ = nil, want refusal")
} else if !errors.Is(err, bridge.ErrBridgeProfileForbidden) {
t.Errorf("SignRaw err = %v, want ErrBridgeProfileForbidden", err)
}
// SignDeposit must refuse (routes through signEthMessage gate).
var recipient [20]byte
if _, err := s.SignDeposit(1, 42, recipient, big.NewInt(1)); err == nil {
t.Errorf("SignDeposit under strict-PQ = nil, want refusal")
} else if !errors.Is(err, bridge.ErrBridgeProfileForbidden) {
t.Errorf("SignDeposit err = %v, want ErrBridgeProfileForbidden", err)
}
// SignBacking must refuse.
if _, err := s.SignBacking(1, big.NewInt(1000), 1234567890); err == nil {
t.Errorf("SignBacking under strict-PQ = nil, want refusal")
} else if !errors.Is(err, bridge.ErrBridgeProfileForbidden) {
t.Errorf("SignBacking err = %v, want ErrBridgeProfileForbidden", err)
}
// SignTransaction must refuse.
if _, err := s.SignTransaction(make([]byte, 9), 96369); err == nil {
t.Errorf("SignTransaction under strict-PQ = nil, want refusal")
} else if !errors.Is(err, bridge.ErrBridgeProfileForbidden) {
t.Errorf("SignTransaction err = %v, want ErrBridgeProfileForbidden", err)
}
}
// TestSigner_ProceedsUnderClassicalCompat — the default profile is
// BridgeClassicalCompat; SignDeposit / SignBacking succeed and emit the
// counter. The classical-compat assertion is the dual of
// TestSigner_RefusesECDSAUnderStrictPQ.
func TestSigner_ProceedsUnderClassicalCompat(t *testing.T) {
s, err := NewSigner(makeTestKey())
if err != nil {
t.Fatalf("NewSigner: %v", err)
}
// Default profile from NewSigner is BridgeClassicalCompat; verify
// SignDeposit actually returns a signature.
var recipient [20]byte
sig, err := s.SignDeposit(1, 42, recipient, big.NewInt(1))
if err != nil {
t.Fatalf("SignDeposit under classical-compat = %v, want nil", err)
}
if len(sig) != 65 {
t.Errorf("signature len = %d, want 65", len(sig))
}
}
// TestSigner_ClassicalCompatTotalIncrements — every signing call under
// classical-compat increments bridge_classical_compat_total{primitive="admin"}.
func TestSigner_ClassicalCompatTotalIncrements(t *testing.T) {
s, err := NewSigner(makeTestKey())
if err != nil {
t.Fatalf("NewSigner: %v", err)
}
before := bridge.ClassicalCompatTotal()["admin"]
var recipient [20]byte
if _, err := s.SignDeposit(1, 42, recipient, big.NewInt(1)); err != nil {
t.Fatalf("SignDeposit: %v", err)
}
after := bridge.ClassicalCompatTotal()["admin"]
if after <= before {
t.Errorf("classical-compat counter did not advance: before=%d after=%d", before, after)
}
}
// TestSigner_SetProfile_StrictPQEvenAfterConstruction — verify the
// profile pointer is observed on subsequent sign calls, not snapshotted
// at construction time.
func TestSigner_SetProfile_StrictPQEvenAfterConstruction(t *testing.T) {
s, err := NewSigner(makeTestKey())
if err != nil {
t.Fatalf("NewSigner: %v", err)
}
// First call under default (classical-compat) succeeds.
var recipient [20]byte
if _, err := s.SignDeposit(1, 42, recipient, big.NewInt(1)); err != nil {
t.Fatalf("classical-compat SignDeposit: %v", err)
}
// Flip to strict-PQ; next call must refuse.
p := bridge.LuxStrictPQBridgeProfile
s.SetProfile(&p)
if _, err := s.SignDeposit(1, 43, recipient, big.NewInt(1)); err == nil {
t.Errorf("strict-PQ SignDeposit = nil, want refusal")
} else if !errors.Is(err, bridge.ErrBridgeProfileForbidden) {
t.Errorf("err = %v, want ErrBridgeProfileForbidden", err)
}
}
+332
View File
@@ -0,0 +1,332 @@
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// Package bridge — the top-level Lux bridge / teleport audit gate.
//
// This file is the canonical home for BridgeProfile in the bridge module.
// It mirrors github.com/luxfi/consensus/config.BridgeProfile so bridge-layer
// callers (cmd/bridge, opnet-watcher, app/server, teleport relays) can pin a
// posture without taking the consensus dependency.
//
// One and only one way to label a bridge:
//
// bridge.LuxStrictPQBridgeProfile — production E2E-PQ
// bridge.BridgeClassicalCompat — classical-compat (e.g. Ethereum L1)
//
// Every bridge entry point — every inbound proof verification, every
// outbound signing call — MUST hold a *BridgeProfile and consult the
// matching Refuse* gate before touching a classical primitive (ecrecover,
// bls.Verify, KZG, Groth16, pairing precompile).
//
// The destination-side ChainSecurityProfile is OUT OF SCOPE for this file:
// a Lux strict-PQ chain may still operate a classical-compat bridge to
// Ethereum; the BRIDGE is the wrapper around that I/O and is labelled
// non-E2E-PQ. The chain's strict-PQ posture is unchanged.
//
// Integration sites (each MUST consult a profile):
//
// opnet-watcher/signer.go — ECDSA + threshold ECDSA producers
// for Teleporter (BridgeClassicalCompat)
// opnet-watcher/relay.go — outbound transaction submission
// cmd/bridge/api.go — bridge_getProfile RPC + per-route
// metadata labelling
// (future) coreth/precompile/warp — BLS-aggregate AWM verifier
// (BridgeClassicalCompat by construction)
package bridge
import (
"errors"
"fmt"
"sync/atomic"
)
// BridgeProfileID is the wire byte that identifies a bridge profile.
// The values MUST match github.com/luxfi/consensus/config.BridgeProfileID;
// the two enums are kept in lockstep by the audit-gate test in this file.
type BridgeProfileID uint32
const (
BridgeProfileIDNone BridgeProfileID = 0x00
BridgeProfileIDLuxStrictPQ BridgeProfileID = 0x01
BridgeProfileIDClassicalCompat BridgeProfileID = 0x90
)
// String returns the canonical wire name of the bridge-profile ID.
func (p BridgeProfileID) String() string {
switch p {
case BridgeProfileIDNone:
return "none"
case BridgeProfileIDLuxStrictPQ:
return "lux-strict-pq-bridge"
case BridgeProfileIDClassicalCompat:
return "bridge-classical-compat-unsafe"
default:
return fmt.Sprintf("bridge-profile(0x%08x)", uint32(p))
}
}
// Finality / proof / auth schemes are kept as opaque string constants
// inside the bridge module. The full uint8 dispatch table lives in the
// consensus package; here we only need to label / refuse, not classify.
//
// Strings match the consensus String() output 1:1 (the audit-gate test
// in profile_test.go pins this).
const (
SchemePulsarM65 = "pulsar-m-65"
SchemePulsarM87 = "pulsar-m-87"
SchemeMLDSA65 = "ml-dsa-65"
SchemeMLDSA87 = "ml-dsa-87"
SchemeBLS12381 = "bls12-381"
PolicySTARKFRISHA3PQ = "stark-fri-sha3-pq"
PolicyGroth16BN254 = "groth16-bn254-classical-forbidden-in-pq"
PolicyKZG = "plonk-kzg-classical-forbidden-in-pq"
AuthMLDSA87 = "ml-dsa-87"
AuthMLDSA65 = "ml-dsa-65"
AuthMultisigMLDSA = "multisig-ml-dsa"
AuthECDSAUnsafe = "ecdsa-unsafe-classical-forbidden-in-pq"
AuthBLSUnsafe = "bls-unsafe-classical-forbidden-in-pq"
HashSuiteSHA3NIST = "sha3-nist"
)
// BridgeProfile mirrors github.com/luxfi/consensus/config.BridgeProfile
// for use at the bridge-package layer. See the consensus package for
// the canonical definition + validation logic.
type BridgeProfile struct {
ProfileID uint32
Name string
SourceFinalityScheme string
DestFinalityScheme string
ProofPolicyID string
BridgeAdminScheme string
BridgePauseScheme string
HashSuiteID string
AllowsClassicalAdmin bool
AllowsBLSAggregate bool
AllowsKZGCommitment bool
AllowsGroth16Wrap bool
AllowsPairingPrecompile bool
PostQuantumEndToEnd bool
}
// Typed errors. Mirrors consensus/config's error surface; the same
// sentinel values can be checked across both packages.
var (
ErrBridgeProfileNil = errors.New("BridgeProfile: nil profile")
ErrBridgeProfileForbidden = errors.New("BridgeProfile: primitive forbidden by profile")
)
// LuxStrictPQBridgeProfile is the canonical Lux strict-PQ bridge
// posture. See consensus/config.LuxStrictPQBridgeProfile.
var LuxStrictPQBridgeProfile = BridgeProfile{
ProfileID: uint32(BridgeProfileIDLuxStrictPQ),
Name: "LUX_STRICT_PQ_BRIDGE",
SourceFinalityScheme: SchemePulsarM65,
DestFinalityScheme: SchemePulsarM65,
ProofPolicyID: PolicySTARKFRISHA3PQ,
BridgeAdminScheme: AuthMLDSA87,
BridgePauseScheme: AuthMultisigMLDSA,
HashSuiteID: HashSuiteSHA3NIST,
AllowsClassicalAdmin: false,
AllowsBLSAggregate: false,
AllowsKZGCommitment: false,
AllowsGroth16Wrap: false,
AllowsPairingPrecompile: false,
PostQuantumEndToEnd: true,
}
// BridgeClassicalCompat is the explicit classical-compat profile. See
// consensus/config.BridgeClassicalCompat.
var BridgeClassicalCompat = BridgeProfile{
ProfileID: uint32(BridgeProfileIDClassicalCompat),
Name: "BRIDGE_CLASSICAL_COMPAT_UNSAFE",
SourceFinalityScheme: SchemeBLS12381,
DestFinalityScheme: SchemePulsarM65,
ProofPolicyID: PolicySTARKFRISHA3PQ,
BridgeAdminScheme: AuthECDSAUnsafe,
BridgePauseScheme: AuthECDSAUnsafe,
HashSuiteID: HashSuiteSHA3NIST,
AllowsClassicalAdmin: true,
AllowsBLSAggregate: true,
AllowsKZGCommitment: true,
AllowsGroth16Wrap: true,
AllowsPairingPrecompile: true,
PostQuantumEndToEnd: false,
}
// IsPostQuantumEndToEnd is the audit-visible label.
func (p *BridgeProfile) IsPostQuantumEndToEnd() bool {
return p.PostQuantumEndToEnd
}
// RefuseClassicalAdmin is the gate every ecrecover / ECDSA-verify call
// site MUST consult. Returns nil iff the profile allows classical
// admin; emits the bridge_classical_compat_total counter increment.
func (p *BridgeProfile) RefuseClassicalAdmin() error {
if p == nil {
return ErrBridgeProfileNil
}
if p.AllowsClassicalAdmin {
incClassicalCompat("admin", p.Name)
return nil
}
return fmt.Errorf("%w: profile %s forbids classical admin (ecrecover/ECDSA/secp256k1)",
ErrBridgeProfileForbidden, p.Name)
}
// RefuseBLSAggregate gates bls.Verify call sites.
func (p *BridgeProfile) RefuseBLSAggregate() error {
if p == nil {
return ErrBridgeProfileNil
}
if p.AllowsBLSAggregate {
incClassicalCompat("bls_aggregate", p.Name)
return nil
}
return fmt.Errorf("%w: profile %s forbids BLS aggregate verification",
ErrBridgeProfileForbidden, p.Name)
}
// RefuseKZGCommitment gates KZG verification call sites.
func (p *BridgeProfile) RefuseKZGCommitment() error {
if p == nil {
return ErrBridgeProfileNil
}
if p.AllowsKZGCommitment {
incClassicalCompat("kzg", p.Name)
return nil
}
return fmt.Errorf("%w: profile %s forbids KZG commitments",
ErrBridgeProfileForbidden, p.Name)
}
// RefuseGroth16Wrap gates Groth16 verification call sites.
func (p *BridgeProfile) RefuseGroth16Wrap() error {
if p == nil {
return ErrBridgeProfileNil
}
if p.AllowsGroth16Wrap {
incClassicalCompat("groth16", p.Name)
return nil
}
return fmt.Errorf("%w: profile %s forbids Groth16 wrappers",
ErrBridgeProfileForbidden, p.Name)
}
// RefusePairingPrecompile gates BN254 pairing-precompile use.
func (p *BridgeProfile) RefusePairingPrecompile() error {
if p == nil {
return ErrBridgeProfileNil
}
if p.AllowsPairingPrecompile {
incClassicalCompat("pairing", p.Name)
return nil
}
return fmt.Errorf("%w: profile %s forbids pairing-precompile use",
ErrBridgeProfileForbidden, p.Name)
}
// classicalCompatCounters tracks bridge_classical_compat_total per
// primitive. Each call to a Refuse* method that returns nil under a
// classical-compat profile increments the matching counter; operators
// scrape these via the bridge's /metrics endpoint (Prometheus format).
//
// The counter is intentionally simple — atomic uint64 keyed by string —
// so the bridge module stays free of a Prometheus client dependency.
// cmd/bridge wires the counters into its /metrics handler.
type classicalCompatCounter struct {
admin atomic.Uint64
blsAgg atomic.Uint64
kzg atomic.Uint64
groth16 atomic.Uint64
pairing atomic.Uint64
lastProfile atomic.Value // string
}
var classicalCompatCounters classicalCompatCounter
func incClassicalCompat(primitive, profileName string) {
classicalCompatCounters.lastProfile.Store(profileName)
switch primitive {
case "admin":
classicalCompatCounters.admin.Add(1)
case "bls_aggregate":
classicalCompatCounters.blsAgg.Add(1)
case "kzg":
classicalCompatCounters.kzg.Add(1)
case "groth16":
classicalCompatCounters.groth16.Add(1)
case "pairing":
classicalCompatCounters.pairing.Add(1)
}
}
// ClassicalCompatTotal returns the current count of classical-compat
// gate traversals broken down by primitive. The result map is a
// snapshot; subsequent increments are not reflected.
func ClassicalCompatTotal() map[string]uint64 {
return map[string]uint64{
"admin": classicalCompatCounters.admin.Load(),
"bls_aggregate": classicalCompatCounters.blsAgg.Load(),
"kzg": classicalCompatCounters.kzg.Load(),
"groth16": classicalCompatCounters.groth16.Load(),
"pairing": classicalCompatCounters.pairing.Load(),
}
}
// ProfileMetadata is the JSON record block-explorer + RPC consumers see
// for a single bridge profile. Stable contract; new fields append.
type ProfileMetadata struct {
ProfileID string `json:"profile_id"`
Name string `json:"name"`
PostQuantumEndToEnd bool `json:"post_quantum_end_to_end"`
SourceFinalityScheme string `json:"source_finality_scheme"`
DestFinalityScheme string `json:"dest_finality_scheme"`
ProofPolicyID string `json:"proof_policy_id"`
BridgeAdminScheme string `json:"bridge_admin_scheme"`
BridgePauseScheme string `json:"bridge_pause_scheme"`
HashSuiteID string `json:"hash_suite_id"`
}
// Metadata returns the JSON-serialisable view of this profile. Used by
// cmd/bridge/api.go for the bridge_getProfile RPC and by block-explorer
// metadata writers on every deposit / withdrawal record.
func (p *BridgeProfile) Metadata() ProfileMetadata {
if p == nil {
return ProfileMetadata{Name: "nil-profile"}
}
return ProfileMetadata{
ProfileID: fmt.Sprintf("0x%02x", p.ProfileID),
Name: p.Name,
PostQuantumEndToEnd: p.PostQuantumEndToEnd,
SourceFinalityScheme: schemeWireName(p.SourceFinalityScheme),
DestFinalityScheme: schemeWireName(p.DestFinalityScheme),
ProofPolicyID: p.ProofPolicyID,
BridgeAdminScheme: p.BridgeAdminScheme,
BridgePauseScheme: p.BridgePauseScheme,
HashSuiteID: p.HashSuiteID,
}
}
// schemeWireName returns the upper-snake wire name used by the explorer
// metadata. The lower-kebab consensus String() is for log messages;
// explorer JSON uses the upper-snake form documented in HIP-0078.
func schemeWireName(s string) string {
switch s {
case SchemePulsarM65:
return "PULSAR_M_65"
case SchemePulsarM87:
return "PULSAR_M_87"
case SchemeMLDSA65:
return "ML_DSA_65"
case SchemeMLDSA87:
return "ML_DSA_87"
case SchemeBLS12381:
return "BLS_12_381"
}
return s
}
+285
View File
@@ -0,0 +1,285 @@
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package bridge
import (
"encoding/json"
"errors"
"testing"
)
// TestBridgeProfileID_String pins the wire-name table.
func TestBridgeProfileID_String(t *testing.T) {
cases := []struct {
id BridgeProfileID
want string
}{
{BridgeProfileIDNone, "none"},
{BridgeProfileIDLuxStrictPQ, "lux-strict-pq-bridge"},
{BridgeProfileIDClassicalCompat, "bridge-classical-compat-unsafe"},
}
for _, c := range cases {
if got := c.id.String(); got != c.want {
t.Errorf("BridgeProfileID(0x%x).String() = %q, want %q", uint32(c.id), got, c.want)
}
}
}
// TestLuxStrictPQBridgeProfile_Fields — every field of the canonical
// strict-PQ profile is pinned.
func TestLuxStrictPQBridgeProfile_Fields(t *testing.T) {
p := LuxStrictPQBridgeProfile
if p.ProfileID != 0x01 {
t.Errorf("ProfileID = 0x%x, want 0x01", p.ProfileID)
}
if p.Name != "LUX_STRICT_PQ_BRIDGE" {
t.Errorf("Name = %q, want LUX_STRICT_PQ_BRIDGE", p.Name)
}
if p.SourceFinalityScheme != SchemePulsarM65 {
t.Errorf("SourceFinalityScheme = %s, want pulsar-m-65", p.SourceFinalityScheme)
}
if p.DestFinalityScheme != SchemePulsarM65 {
t.Errorf("DestFinalityScheme = %s, want pulsar-m-65", p.DestFinalityScheme)
}
if p.ProofPolicyID != PolicySTARKFRISHA3PQ {
t.Errorf("ProofPolicyID = %s, want stark-fri-sha3-pq", p.ProofPolicyID)
}
if p.BridgeAdminScheme != AuthMLDSA87 {
t.Errorf("BridgeAdminScheme = %s, want ml-dsa-87", p.BridgeAdminScheme)
}
if p.BridgePauseScheme != AuthMultisigMLDSA {
t.Errorf("BridgePauseScheme = %s, want multisig-ml-dsa", p.BridgePauseScheme)
}
if !p.PostQuantumEndToEnd {
t.Errorf("PostQuantumEndToEnd = false, want true")
}
}
// TestBridgeProfile_LuxStrictPQ_AllForbidBitsTrue — every classical-allow
// gate is false on the canonical strict-PQ profile.
func TestBridgeProfile_LuxStrictPQ_AllForbidBitsTrue(t *testing.T) {
p := LuxStrictPQBridgeProfile
if p.AllowsClassicalAdmin {
t.Errorf("AllowsClassicalAdmin = true, want false on strict-PQ")
}
if p.AllowsBLSAggregate {
t.Errorf("AllowsBLSAggregate = true, want false on strict-PQ")
}
if p.AllowsKZGCommitment {
t.Errorf("AllowsKZGCommitment = true, want false on strict-PQ")
}
if p.AllowsGroth16Wrap {
t.Errorf("AllowsGroth16Wrap = true, want false on strict-PQ")
}
if p.AllowsPairingPrecompile {
t.Errorf("AllowsPairingPrecompile = true, want false on strict-PQ")
}
}
// TestBridgeClassicalCompat_Fields — classical-compat is explicitly
// labelled non-E2E-PQ and every classical gate is open.
func TestBridgeClassicalCompat_Fields(t *testing.T) {
p := BridgeClassicalCompat
if p.ProfileID != 0x90 {
t.Errorf("ProfileID = 0x%x, want 0x90", p.ProfileID)
}
if p.PostQuantumEndToEnd {
t.Errorf("PostQuantumEndToEnd = true, want false")
}
if !p.AllowsClassicalAdmin {
t.Errorf("AllowsClassicalAdmin = false, want true")
}
if !p.AllowsBLSAggregate {
t.Errorf("AllowsBLSAggregate = false, want true")
}
}
// TestBridgeWithdraw_RefusesECDSAUnderStrictPQ — the spec test name:
// a withdraw handler under strict-PQ MUST refuse ECDSA admin.
func TestBridgeWithdraw_RefusesECDSAUnderStrictPQ(t *testing.T) {
p := LuxStrictPQBridgeProfile
if err := p.RefuseClassicalAdmin(); err == nil {
t.Fatalf("withdraw under strict-PQ did not refuse ECDSA")
} else if !errors.Is(err, ErrBridgeProfileForbidden) {
t.Errorf("err = %v, want ErrBridgeProfileForbidden", err)
}
compat := BridgeClassicalCompat
if err := compat.RefuseClassicalAdmin(); err != nil {
t.Fatalf("withdraw under classical-compat refused ECDSA: %v", err)
}
}
// TestBridgeProfile_RefuseGates_StrictPQRefusesEachPrimitive — each
// Refuse* gate refuses under strict-PQ; each allows under classical.
func TestBridgeProfile_RefuseGates_StrictPQRefusesEachPrimitive(t *testing.T) {
strict := LuxStrictPQBridgeProfile
compat := BridgeClassicalCompat
for name, fn := range map[string]struct {
strict func() error
compat func() error
}{
"RefuseClassicalAdmin": {strict.RefuseClassicalAdmin, compat.RefuseClassicalAdmin},
"RefuseBLSAggregate": {strict.RefuseBLSAggregate, compat.RefuseBLSAggregate},
"RefuseKZGCommitment": {strict.RefuseKZGCommitment, compat.RefuseKZGCommitment},
"RefuseGroth16Wrap": {strict.RefuseGroth16Wrap, compat.RefuseGroth16Wrap},
"RefusePairingPrecompile": {strict.RefusePairingPrecompile, compat.RefusePairingPrecompile},
} {
if err := fn.strict(); err == nil {
t.Errorf("strict-PQ %s = nil, want refusal", name)
} else if !errors.Is(err, ErrBridgeProfileForbidden) {
t.Errorf("strict-PQ %s err = %v, want ErrBridgeProfileForbidden", name, err)
}
if err := fn.compat(); err != nil {
t.Errorf("classical-compat %s = %v, want nil", name, err)
}
}
}
// TestBridgeProfile_ClassicalCompatTotal_Increments — every Refuse*
// traversal under classical-compat increments the matching counter.
func TestBridgeProfile_ClassicalCompatTotal_Increments(t *testing.T) {
before := ClassicalCompatTotal()
compat := BridgeClassicalCompat
_ = compat.RefuseClassicalAdmin()
_ = compat.RefuseBLSAggregate()
_ = compat.RefuseKZGCommitment()
_ = compat.RefuseGroth16Wrap()
_ = compat.RefusePairingPrecompile()
after := ClassicalCompatTotal()
if after["admin"] != before["admin"]+1 {
t.Errorf("admin counter = %d, want %d", after["admin"], before["admin"]+1)
}
if after["bls_aggregate"] != before["bls_aggregate"]+1 {
t.Errorf("bls_aggregate counter = %d, want %d", after["bls_aggregate"], before["bls_aggregate"]+1)
}
if after["kzg"] != before["kzg"]+1 {
t.Errorf("kzg counter = %d, want %d", after["kzg"], before["kzg"]+1)
}
if after["groth16"] != before["groth16"]+1 {
t.Errorf("groth16 counter = %d, want %d", after["groth16"], before["groth16"]+1)
}
if after["pairing"] != before["pairing"]+1 {
t.Errorf("pairing counter = %d, want %d", after["pairing"], before["pairing"]+1)
}
}
// TestBridgeProfile_NilReceiver — gates on a nil profile must refuse,
// not panic.
func TestBridgeProfile_NilReceiver(t *testing.T) {
var p *BridgeProfile
for name, fn := range map[string]func() error{
"RefuseClassicalAdmin": p.RefuseClassicalAdmin,
"RefuseBLSAggregate": p.RefuseBLSAggregate,
"RefuseKZGCommitment": p.RefuseKZGCommitment,
"RefuseGroth16Wrap": p.RefuseGroth16Wrap,
"RefusePairingPrecompile": p.RefusePairingPrecompile,
} {
if err := fn(); err == nil {
t.Errorf("nil-receiver %s = nil, want ErrBridgeProfileNil", name)
} else if !errors.Is(err, ErrBridgeProfileNil) {
t.Errorf("nil-receiver %s err = %v, want ErrBridgeProfileNil", name, err)
}
}
}
// TestBridgeProfile_Metadata_JSON pins the JSON contract for the
// bridge_getProfile RPC. The fields and the snake-case wire names are
// the public API; auditors filter on these.
func TestBridgeProfile_Metadata_JSON(t *testing.T) {
p := LuxStrictPQBridgeProfile
md := p.Metadata()
b, err := json.Marshal(md)
if err != nil {
t.Fatalf("Marshal: %v", err)
}
got := string(b)
wantSubstrings := []string{
`"profile_id":"0x01"`,
`"name":"LUX_STRICT_PQ_BRIDGE"`,
`"post_quantum_end_to_end":true`,
`"source_finality_scheme":"PULSAR_M_65"`,
`"dest_finality_scheme":"PULSAR_M_65"`,
`"proof_policy_id":"stark-fri-sha3-pq"`,
`"bridge_admin_scheme":"ml-dsa-87"`,
`"bridge_pause_scheme":"multisig-ml-dsa"`,
}
for _, s := range wantSubstrings {
if !contains(got, s) {
t.Errorf("Metadata JSON missing %q\nfull: %s", s, got)
}
}
}
// TestBridgeClassicalCompat_Metadata_JSON pins the classical-compat
// label is visible to RPC + explorer consumers.
func TestBridgeClassicalCompat_Metadata_JSON(t *testing.T) {
p := BridgeClassicalCompat
md := p.Metadata()
b, err := json.Marshal(md)
if err != nil {
t.Fatalf("Marshal: %v", err)
}
got := string(b)
wantSubstrings := []string{
`"profile_id":"0x90"`,
`"name":"BRIDGE_CLASSICAL_COMPAT_UNSAFE"`,
`"post_quantum_end_to_end":false`,
`"source_finality_scheme":"BLS_12_381"`,
`"dest_finality_scheme":"PULSAR_M_65"`,
}
for _, s := range wantSubstrings {
if !contains(got, s) {
t.Errorf("Metadata JSON missing %q\nfull: %s", s, got)
}
}
}
// TestBridgeProfile_SchemeWireName covers the upper-snake mapping.
func TestBridgeProfile_SchemeWireName(t *testing.T) {
cases := []struct {
in, want string
}{
{SchemePulsarM65, "PULSAR_M_65"},
{SchemePulsarM87, "PULSAR_M_87"},
{SchemeMLDSA65, "ML_DSA_65"},
{SchemeMLDSA87, "ML_DSA_87"},
{SchemeBLS12381, "BLS_12_381"},
{"unknown-scheme", "unknown-scheme"},
}
for _, c := range cases {
if got := schemeWireName(c.in); got != c.want {
t.Errorf("schemeWireName(%q) = %q, want %q", c.in, got, c.want)
}
}
}
// TestBridgeProfile_IsPostQuantumEndToEnd_Accessor pins the public
// accessor.
func TestBridgeProfile_IsPostQuantumEndToEnd_Accessor(t *testing.T) {
if !LuxStrictPQBridgeProfile.IsPostQuantumEndToEnd() {
t.Errorf("strict-PQ IsPostQuantumEndToEnd() = false, want true")
}
if BridgeClassicalCompat.IsPostQuantumEndToEnd() {
t.Errorf("classical-compat IsPostQuantumEndToEnd() = true, want false")
}
}
func contains(s, substr string) bool {
for i := 0; i+len(substr) <= len(s); i++ {
if s[i:i+len(substr)] == substr {
return true
}
}
return false
}