mirror of
https://github.com/luxfi/mpc.git
synced 2026-09-24 00:26:24 +00:00
mpcd: embed luxfi/threshold/pkg/thresholdd dispatcher (six schemes)
mpcd is the production threshold daemon. With this commit it embeds
the JSON-RPC dispatcher from luxfi/threshold/pkg/thresholdd on a new
--threshold-listen flag (default 127.0.0.1:7300, MPC_THRESHOLD_LISTEN
env var). The dispatcher exposes every supported scheme:
cggmp21 (secp256k1 ECDSA, CGGMP21 fork)
frost (secp256k1 Schnorr, RFC 9591)
bls (BLS12-381 Shamir/Lagrange)
sr25519 (Ristretto255, via mpc node's own session — separate path)
pulsar (M-LWE / FIPS 204 ML-DSA-65 threshold, via luxfi/corona/threshold)
corona (R-LWE / Ringtail, via luxfi/ringtail/threshold)
doerner (reserved; upstream protocol non-functional, returns explicit error)
Wire format matches teleport/mpc/src/signers/rpc.ts byte-for-byte:
POST / {"jsonrpc":"2.0","id":N,"method":"<scheme>.<op>","params":{...}}
Methods per scheme: keygen / sign / verify. Public-key handles are
returned by keygen and bound on sign/verify.
Decomplecting note: the dispatcher carries zero policy. Profile
gating, auth, audit, and rate-limiting live on mpcd's existing API
surface (and on the cluster ingress in front of it). The threshold
listener is loopback by default; production exposure happens via the
KMS-facing ZAP server or the gateway.
There is no separate thresholdd daemon to deploy. The luxfi/threshold
repo's cmd/thresholdd remains as a developer / CI binary that imports
the same package — one wire, one implementation, two startup paths.
Tests: TestEmbeddedDispatcher_BLSRoundTrip (full keygen → sign →
verify → forgery rejection) and TestEmbeddedDispatcher_UnknownScheme
(-32601 method-not-found contract) both pass. README updated with the
new protocol table; full surface documented in
luxfi/threshold/LLM.md "thresholdd — unified JSON-RPC dispatcher".
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# MPC
|
||||
|
||||
Threshold signing engine -- CGGMP21 (ECDSA), FROST (EdDSA), BLS, and SR25519. No full private key reconstruction, ever.
|
||||
Threshold signing engine -- CGGMP21 (ECDSA), FROST (EdDSA/Schnorr), BLS, SR25519, and post-quantum lattice signatures (Pulsar M-LWE, Corona R-LWE, and double-lattice composition) via the embedded `thresholdd` dispatcher. No full private key reconstruction, ever.
|
||||
|
||||
```
|
||||
go get github.com/luxfi/mpc
|
||||
@@ -12,12 +12,24 @@ go get github.com/luxfi/mpc
|
||||
|
||||
### Protocols
|
||||
|
||||
| Protocol | Curve | Key Type | Use |
|
||||
|----------|-------|----------|-----|
|
||||
| Protocol | Curve / Lattice | Key Type | Use |
|
||||
|----------|-----------------|----------|-----|
|
||||
| CGGMP21 | secp256k1 | `secp256k1` | Bitcoin, Ethereum, Lux C-Chain, all EVM L2s, XRPL |
|
||||
| FROST | Ed25519 | `ed25519` | Solana, TON, Polkadot, Cardano, Substrate chains |
|
||||
| BLS | BLS12-381 | `bls` | Lux consensus, beacon chain, aggregated signatures |
|
||||
| SR25519 | Ristretto255 | `sr25519` | Substrate/Polkadot native |
|
||||
| Pulsar | M-LWE (FIPS 204 / ML-DSA-65 threshold) | `pulsar` | Post-quantum threshold signing — bridge anchors, strict-PQ profile |
|
||||
| Corona | R-LWE (Ringtail) | `corona` | Post-quantum threshold signing — orthogonal lattice family to Pulsar |
|
||||
| Double-lattice | Pulsar ⊕ Corona | `double-lattice` | Composed `u32_be(len) ‖ pulsar_sig ‖ corona_sig` — both must verify |
|
||||
|
||||
Pulsar, Corona, and double-lattice are exposed via the embedded
|
||||
threshold dispatcher (`--threshold-listen`, default `127.0.0.1:7300`),
|
||||
which is `luxfi/threshold/pkg/thresholdd`. The dispatcher is a JSON-RPC
|
||||
2.0 surface shared with `cggmp21` / `frost` / `bls` — one wire, six
|
||||
schemes — and is consumed by the teleport bus
|
||||
(`~/work/lux/teleport/mpc/src/signers/`). The wire format is documented
|
||||
in `~/work/lux/threshold/CLAUDE.md` under "thresholdd — unified
|
||||
JSON-RPC daemon".
|
||||
|
||||
### Threshold Model
|
||||
|
||||
|
||||
@@ -35,6 +35,8 @@ import (
|
||||
|
||||
uimpc "github.com/luxfi/mpc/ui"
|
||||
|
||||
"github.com/luxfi/threshold/pkg/thresholdd"
|
||||
|
||||
mpcapi "github.com/luxfi/mpc/pkg/api"
|
||||
"github.com/luxfi/mpc/pkg/backup"
|
||||
"github.com/luxfi/mpc/pkg/zapauth"
|
||||
@@ -117,6 +119,12 @@ func main() {
|
||||
Sources: cli.EnvVars("MPC_KMS_ZAP_LISTEN"),
|
||||
Value: ":9653",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "threshold-listen",
|
||||
Usage: "Embedded threshold JSON-RPC dispatcher listen address (luxfi/threshold/pkg/thresholdd surface: cggmp21/frost/pulsar/corona/bls/doerner). Empty disables. Default 127.0.0.1:7300 — process-local IPC; production fronts this via the cluster ingress + KMS.",
|
||||
Sources: cli.EnvVars("MPC_THRESHOLD_LISTEN"),
|
||||
Value: "127.0.0.1:7300",
|
||||
},
|
||||
// HSM / password provider flags
|
||||
&cli.StringFlag{
|
||||
Name: "hsm-provider",
|
||||
@@ -771,6 +779,39 @@ func runNodeConsensus(ctx context.Context, c *cli.Command) error {
|
||||
}
|
||||
}
|
||||
|
||||
// Start embedded threshold JSON-RPC dispatcher
|
||||
// (luxfi/threshold/pkg/thresholdd). One process, one wire, six
|
||||
// schemes (cggmp21/frost/pulsar/corona/bls/doerner) — consumed by
|
||||
// teleport/mpc and any other client that speaks the threshold bus.
|
||||
//
|
||||
// The dispatcher carries zero policy on purpose: profile gating,
|
||||
// auth, and audit live on the API surface above (and on the gateway
|
||||
// in front of mpcd in production). Default bind is loopback only;
|
||||
// network exposure happens via the cluster ingress.
|
||||
if thrAddr := c.String("threshold-listen"); thrAddr != "" {
|
||||
thrSrv, thrErr := thresholdd.NewServer()
|
||||
if thrErr != nil {
|
||||
logger.Error("Failed to build threshold dispatcher", thrErr)
|
||||
} else {
|
||||
thrListener, listenErr := net.Listen("tcp", thrAddr)
|
||||
if listenErr != nil {
|
||||
logger.Error("Failed to bind threshold dispatcher", listenErr, "addr", thrAddr)
|
||||
} else {
|
||||
thrHTTP := &http.Server{
|
||||
Handler: thrSrv,
|
||||
ReadHeaderTimeout: 5 * time.Second,
|
||||
}
|
||||
go func() {
|
||||
logger.Info("Threshold dispatcher starting", "addr", thrListener.Addr().String(), "schemes", "cggmp21,frost,pulsar,corona,bls,doerner")
|
||||
if err := thrHTTP.Serve(thrListener); err != nil && err != http.ErrServerClosed {
|
||||
logger.Error("Threshold dispatcher failed", err)
|
||||
}
|
||||
}()
|
||||
defer thrHTTP.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Start Dashboard API server (ZapDB-backed, no external dependencies)
|
||||
apiListenAddr := c.String("api-listen")
|
||||
jwtSecret := c.String("jwt-secret")
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
// Verifies the embedded threshold dispatcher (luxfi/threshold/pkg/thresholdd)
|
||||
// is reachable from mpcd's process and answers a full keygen → sign → verify
|
||||
// round-trip with at least one production scheme (BLS — fast keygen, no
|
||||
// upstream flakiness).
|
||||
//
|
||||
// This is the smallest possible test that proves the consolidation is
|
||||
// real: mpcd embeds the same package the standalone thresholdd CLI uses,
|
||||
// the wire shape matches what teleport/mpc/src/signers/rpc.ts speaks,
|
||||
// and the dispatcher refuses unknown schemes with -32601.
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/luxfi/threshold/pkg/thresholdd"
|
||||
)
|
||||
|
||||
// rpcCall posts a JSON-RPC 2.0 request against the embedded dispatcher
|
||||
// and unmarshals the result envelope. Mirrors the wire used by the
|
||||
// teleport mpc bus (signers/rpc.ts).
|
||||
func rpcCall(t *testing.T, url, method string, params any) (json.RawMessage, *struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}) {
|
||||
t.Helper()
|
||||
body, err := json.Marshal(map[string]any{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"method": method,
|
||||
"params": params,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("marshal: %v", err)
|
||||
}
|
||||
resp, err := http.Post(url, "application/json", bytes.NewReader(body))
|
||||
if err != nil {
|
||||
t.Fatalf("post: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != 200 {
|
||||
t.Fatalf("status %d", resp.StatusCode)
|
||||
}
|
||||
var env struct {
|
||||
Result json.RawMessage `json:"result"`
|
||||
Error *struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
} `json:"error"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&env); err != nil {
|
||||
t.Fatalf("decode: %v", err)
|
||||
}
|
||||
return env.Result, env.Error
|
||||
}
|
||||
|
||||
// TestEmbeddedDispatcher_BLSRoundTrip verifies the dispatcher mpcd
|
||||
// embeds answers a real BLS keygen → sign → verify round-trip on the
|
||||
// same wire teleport's PulsarSigner/CoronaSigner/Cggmp21Signer use.
|
||||
func TestEmbeddedDispatcher_BLSRoundTrip(t *testing.T) {
|
||||
srv, err := thresholdd.NewServer()
|
||||
if err != nil {
|
||||
t.Fatalf("NewServer: %v", err)
|
||||
}
|
||||
ts := httptest.NewServer(srv)
|
||||
defer ts.Close()
|
||||
|
||||
// keygen
|
||||
res, rpcErr := rpcCall(t, ts.URL, "bls.keygen", map[string]any{
|
||||
"threshold": 2,
|
||||
"participants": 3,
|
||||
})
|
||||
if rpcErr != nil {
|
||||
t.Fatalf("bls.keygen error %d: %s", rpcErr.Code, rpcErr.Message)
|
||||
}
|
||||
var kg struct {
|
||||
PublicKey string `json:"publicKey"`
|
||||
Shares []string `json:"shares"`
|
||||
}
|
||||
if err := json.Unmarshal(res, &kg); err != nil {
|
||||
t.Fatalf("unmarshal keygen: %v", err)
|
||||
}
|
||||
if kg.PublicKey == "" {
|
||||
t.Fatalf("empty publicKey")
|
||||
}
|
||||
if len(kg.Shares) != 3 {
|
||||
t.Fatalf("shares=%d want=3", len(kg.Shares))
|
||||
}
|
||||
|
||||
// sign
|
||||
msg := hex.EncodeToString([]byte("mpcd embedded dispatch smoke test"))
|
||||
res, rpcErr = rpcCall(t, ts.URL, "bls.sign", map[string]any{
|
||||
"messageHex": msg,
|
||||
"pubKeyHex": kg.PublicKey,
|
||||
})
|
||||
if rpcErr != nil {
|
||||
t.Fatalf("bls.sign error %d: %s", rpcErr.Code, rpcErr.Message)
|
||||
}
|
||||
var sg struct {
|
||||
SignatureHex string `json:"signatureHex"`
|
||||
}
|
||||
if err := json.Unmarshal(res, &sg); err != nil {
|
||||
t.Fatalf("unmarshal sign: %v", err)
|
||||
}
|
||||
if sg.SignatureHex == "" {
|
||||
t.Fatalf("empty signatureHex")
|
||||
}
|
||||
|
||||
// verify
|
||||
res, rpcErr = rpcCall(t, ts.URL, "bls.verify", map[string]any{
|
||||
"messageHex": msg,
|
||||
"signatureHex": sg.SignatureHex,
|
||||
"pubKeyHex": kg.PublicKey,
|
||||
})
|
||||
if rpcErr != nil {
|
||||
t.Fatalf("bls.verify error %d: %s", rpcErr.Code, rpcErr.Message)
|
||||
}
|
||||
var vr struct {
|
||||
OK bool `json:"ok"`
|
||||
}
|
||||
if err := json.Unmarshal(res, &vr); err != nil {
|
||||
t.Fatalf("unmarshal verify: %v", err)
|
||||
}
|
||||
if !vr.OK {
|
||||
t.Fatalf("verify: round-trip signature failed under embedded dispatcher")
|
||||
}
|
||||
|
||||
// forgery rejection — different message under same signature must fail
|
||||
wrong := hex.EncodeToString([]byte("forged"))
|
||||
res, rpcErr = rpcCall(t, ts.URL, "bls.verify", map[string]any{
|
||||
"messageHex": wrong,
|
||||
"signatureHex": sg.SignatureHex,
|
||||
"pubKeyHex": kg.PublicKey,
|
||||
})
|
||||
if rpcErr != nil {
|
||||
t.Fatalf("bls.verify(wrong) error %d: %s", rpcErr.Code, rpcErr.Message)
|
||||
}
|
||||
_ = json.Unmarshal(res, &vr)
|
||||
if vr.OK {
|
||||
t.Fatalf("verify: forgery accepted (different message)")
|
||||
}
|
||||
}
|
||||
|
||||
// TestEmbeddedDispatcher_UnknownScheme verifies the dispatcher returns
|
||||
// a JSON-RPC -32601 method-not-found for an unrecognized scheme — the
|
||||
// teleport bus relies on this contract to route fall-back signers.
|
||||
func TestEmbeddedDispatcher_UnknownScheme(t *testing.T) {
|
||||
srv, err := thresholdd.NewServer()
|
||||
if err != nil {
|
||||
t.Fatalf("NewServer: %v", err)
|
||||
}
|
||||
ts := httptest.NewServer(srv)
|
||||
defer ts.Close()
|
||||
|
||||
_, rpcErr := rpcCall(t, ts.URL, "bogus.keygen", map[string]any{
|
||||
"threshold": 1,
|
||||
"participants": 1,
|
||||
})
|
||||
if rpcErr == nil {
|
||||
t.Fatalf("expected JSON-RPC error for bogus.keygen, got success")
|
||||
}
|
||||
if rpcErr.Code != -32601 {
|
||||
t.Fatalf("expected -32601 method-not-found, got code=%d msg=%q", rpcErr.Code, rpcErr.Message)
|
||||
}
|
||||
}
|
||||
@@ -3,5 +3,6 @@ go 1.26.3
|
||||
// For local development with HSM: go work use ../hsm
|
||||
use (
|
||||
.
|
||||
../threshold
|
||||
./e2e
|
||||
)
|
||||
|
||||
@@ -509,6 +509,7 @@ github.com/luxfi/crypto v1.17.37/go.mod h1:vu+pCHdyPQZULzrA+K+lTFYrg8vOtNdu+/5Vk
|
||||
github.com/luxfi/crypto v1.17.38/go.mod h1:G2t1GQvPsrwnzwyVEj0LQDuX2AWZVI5kEAPyVeicc5o=
|
||||
github.com/luxfi/crypto v1.17.39/go.mod h1:mChLWmW4CLR1wAN6CeJTveCzUv0DTzGQnYgq01x3W0U=
|
||||
github.com/luxfi/crypto v1.17.43/go.mod h1:1M6uQ8I/7dzQwf5RdnKtqOWr0+iXRW0D9WfnMV+Cb1s=
|
||||
github.com/luxfi/crypto/ipa v1.2.4/go.mod h1:43J6f6rcfUMrZt4cQectMOZb6Ps+fAEj8ZTPC3Kk+gE=
|
||||
github.com/luxfi/czmq/v4 v4.2.2/go.mod h1:WFY9ldUbeU900P8bcIRr7Jim23veOhJL5A5zSpTogdU=
|
||||
github.com/luxfi/database v1.17.35/go.mod h1:dYxaF30KGJQzuxA3VH7+WGU5/dhwRyzLgzSk9F3wje0=
|
||||
github.com/luxfi/database v1.17.38/go.mod h1:q6BJNN+47VtkmkGrswQmC0ikERYAdlmZCYhGTXkF4mM=
|
||||
|
||||
Reference in New Issue
Block a user