Files
zeekay d56c21dbe8 feat(internal/mchain): Protocol enum — vocabulary for PQ + classical threshold protocols
Adds the missing primitive the bridge needs to opt swaps into a PQ-safe
threshold protocol (pulsar / corona / magnetar) instead of always going
through the daemon's classical defaults (cggmp21 / frost).

This PR is data-model only — establishes the vocabulary and the helpers
(IsPostQuantum, CompatibleCurve, ProtocolFor). The wire-plumbing
through mchain.Client.signViaDashboard + signing_driver gating off
bridge.BridgeProfile is the next focused PR; it touches the dashboard
call path consistently across auth.go and client.go and so wants its
own review.

Protocol catalogue mirrors mpcd's dispatch matrix
(luxfi/mpc/pkg/api/handlers_kms.go + luxfi/threshold/protocols/*):

  classical:
    cggmp21    secp256k1, default ECDSA
    frost      ed25519 / sr25519, threshold Schnorr/EdDSA
    bls        BLS12-381 aggregate (classical Beam in Warp 2.0)
    doerner    2-of-2 OT-based ECDSA

  post-quantum:
    pulsar-m-65    Module-LWE NIST L1 (~128-bit classical)
    pulsar-m-87    Module-LWE NIST L3 (~192-bit classical)
    corona         Ring-LWE (rank-1 MLWE collapse — production lane on
                   primary network, Warp 2.0 Pulse)
    magnetar       SLH-DSA / FIPS 205 hash-based (per-validator only —
                   true threshold is research-grade per Bonte-Smart-Tan
                   2023)

ProtocolDefault ("") is the wire sentinel for "let the daemon infer
from curve" — preserves backward compatibility with every current
caller. The empty-string contract is locked in a regression test.

Tests pass: ./internal/mchain (4.0s) + full repo green.

This is the foundation for the v1.2.0 PQ posture. The bridge's profile
gate (top-level profile.go) already names pulsar-m-65 / pulsar-m-87 /
ml-dsa-65 / ml-dsa-87 as the strict-PQ schemes; the Protocol enum is
the seam between that profile and the mpcd request body.
2026-05-28 20:25:44 -07:00

111 lines
3.0 KiB
Go

// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package mchain
import "testing"
func TestProtocol_String(t *testing.T) {
cases := []struct {
p Protocol
want string
}{
{ProtocolDefault, ""},
{ProtocolCGGMP21, "cggmp21"},
{ProtocolFROST, "frost"},
{ProtocolBLS, "bls"},
{ProtocolDoerner, "doerner"},
{ProtocolPulsarM65, "pulsar-m-65"},
{ProtocolPulsarM87, "pulsar-m-87"},
{ProtocolCorona, "corona"},
{ProtocolMagnetar, "magnetar"},
}
for _, c := range cases {
if got := string(c.p); got != c.want {
t.Errorf("Protocol(%q).String() = %q, want %q", c.p, got, c.want)
}
}
}
func TestProtocol_IsPostQuantum(t *testing.T) {
classical := []Protocol{
ProtocolDefault,
ProtocolCGGMP21,
ProtocolFROST,
ProtocolBLS,
ProtocolDoerner,
}
pq := []Protocol{
ProtocolPulsarM65,
ProtocolPulsarM87,
ProtocolCorona,
ProtocolMagnetar,
}
for _, p := range classical {
if p.IsPostQuantum() {
t.Errorf("Protocol(%q).IsPostQuantum() = true, want false", p)
}
}
for _, p := range pq {
if !p.IsPostQuantum() {
t.Errorf("Protocol(%q).IsPostQuantum() = false, want true", p)
}
}
}
func TestProtocol_CompatibleCurve(t *testing.T) {
// Every Pulsar / Corona / Doerner / CGGMP21 produces secp256k1
// signatures. Only FROST produces Ed25519.
secp := []Protocol{
ProtocolDefault,
ProtocolCGGMP21,
ProtocolDoerner,
ProtocolPulsarM65,
ProtocolPulsarM87,
ProtocolCorona,
// BLS and Magnetar conservatively map to secp256k1 by default —
// callers using them MUST take the dedicated code path (not
// through Curve), so this default is just "don't blow up".
ProtocolBLS,
ProtocolMagnetar,
}
for _, p := range secp {
if got := p.CompatibleCurve(); got != CurveSecp256k1 {
t.Errorf("Protocol(%q).CompatibleCurve() = %q, want secp256k1", p, got)
}
}
if got := ProtocolFROST.CompatibleCurve(); got != CurveEd25519 {
t.Errorf("Protocol(frost).CompatibleCurve() = %q, want ed25519", got)
}
}
func TestProtocolFor(t *testing.T) {
cases := []struct {
c Curve
want Protocol
}{
{CurveSecp256k1, ProtocolCGGMP21},
{CurveEd25519, ProtocolFROST},
{Curve("sr25519"), ProtocolDefault}, // unknown → daemon default
{Curve(""), ProtocolDefault},
}
for _, tc := range cases {
if got := ProtocolFor(tc.c); got != tc.want {
t.Errorf("ProtocolFor(%q) = %q, want %q", tc.c, got, tc.want)
}
}
}
// PQ vs classical wire-shape regression: the empty Protocol string is
// the documented "send no protocol field" sentinel. If anyone ever
// renames it to e.g. "default" the daemon would receive a literal
// "default" string and refuse the request. Lock the wire form.
func TestProtocol_DefaultIsEmptyWireString(t *testing.T) {
if string(ProtocolDefault) != "" {
t.Fatalf("ProtocolDefault must serialise to empty string; got %q. "+
"Changing this breaks the 'protocol omitted from wire' "+
"contract — every daemon would receive a literal value and "+
"likely reject it.", string(ProtocolDefault))
}
}