mirror of
https://github.com/luxfi/bridge.git
synced 2026-09-24 00:26:46 +00:00
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:
+118
-3
@@ -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
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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")
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user