zeekay 8273105926 The signing curve is derived from the network, not stated by the caller
Solana addresses were correct and receive-only: ConsensusKeyInfoStore.Save was
the only RegisterKey caller and hardcoded "secp256k1", and TriggerSign read the
curve from that field, so no product entry point could reach Ed25519 signing.

The hardcode is not fixed, it is UNREPRESENTABLE. transport.KeyInfo.KeyType is
deleted — the record carries one public key per curve and a PublicKeyFor(curve)
— and RegisterKey lost its keyType parameter, so there is no longer a place to
state a curve wrongly. A scalar KeyType was the wrong shape anyway: one keygen
ceremony produces BOTH a secp256k1 and an Ed25519 key, so "what curve is this
wallet" has no single answer.

The request carries the network instead, and ONE table maps it to a curve, with
NO default arm — KeyTypeForNetwork returns ErrUnknownNetwork rather than
guessing. That is the point: the old default returned secp256k1 for anything
unrecognized, which is how a Solana request would have been signed with the
wrong key.

/sign's 32-byte clamp becomes the SCHEME's rule rather than the endpoint's:
secp256k1 signs a digest and requires exactly 32 bytes, Ed25519 signs the
message at any length. /sign no longer accepts key_type at all — two ways to say
the curve is the misuse being removed — and Network is bound into the
idempotency fingerprint, so the same bytes for SOL and ETH conflict instead of
sharing one cached signature from a different key.

⚠ The trap this nearly shipped with: every pre-existing KeyInfo record has EMPTY
public keys, because the old Save wrote "", "". A naive "does this wallet have
this curve" gate would have refused every existing wallet on every curve. The
check is gated on RecordsCurves() — a record naming no curve denies none, and
the missing share stays the thing that fails closed.

Verified end to end, not by mirroring: a Solana request through
ConsensusMPCBackend.TriggerSign with a real 2-of-3 FROST ring produces a
signature crypto/ed25519.Verify accepts, over a 63-byte message so any
reintroduced clamp or pre-hash fails it; ETH/EVM/BTC/LUX still route to
secp256k1 and must RECOVER to the wallet's key. Four mutations tried, all
caught.

Behavioral risk worth naming: an unrecognized chain string now fails its
transaction closed rather than signing with a default. That is correct, and the
real fix is validating chain at POST /v1/transactions.
2026-08-06 15:59:30 -07:00
2025-05-02 20:37:09 +07:00
2025-12-31 22:21:37 -08:00

mpc

MPC

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

Architecture

luxfi/mpc is a production MPC node (mpcd) that performs distributed key generation and threshold signing across a cluster. Each node holds a key share; t of n nodes must cooperate to produce a signature. The full private key never exists on any single machine, in memory or on disk.

Protocols

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 (Corona) 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".

Validator key slots vs. engine protocols (truthful naming). The KMS validator-key API (pkg/api/handlers_kms.go; hanzo KMS) provisions two slots per validator whose NAMES do not describe their cryptography: the "bls" slot is secp256k1 signed via CGGMP21 threshold ECDSA — NOT bls12-381 aggregation — and the "corona" slot is ed25519 signed via FROST — NOT the Corona R-LWE lattice protocol and NOT post-quantum. The bls12-381, Pulsar/Corona lattice, and double-lattice schemes in the table above are thresholdd engine capabilities, not the validator-custody path. Lux consensus BLS is a native per-validator bls12-381 key (PoP + aggregation), never threshold-custodied by this service.

Threshold Model

t >= floor(n/2) + 1

A 2-of-3 cluster tolerates 1 compromised or offline node. A 3-of-5 cluster tolerates 2. The threshold is configurable at key generation time.

Transport Modes

Consensus (default): Peer-to-peer ZAP protocol with built-in PoA consensus. No external dependencies. Nodes discover each other via --peer flags. This is the production path.

Legacy: NATS pub/sub + Consul service discovery. Deprecated but still supported via --mode=legacy.

Packages

cmd/mpcd/            Daemon binary (CLI, API server, node lifecycle)
pkg/mpc/             Core MPC node -- session management, key generation, signing
  keygen_session.go    CGGMP21 distributed key generation
  signing_session.go   CGGMP21 threshold signing (secp256k1)
  signing_session_frost.go  FROST threshold signing (Ed25519)
  bls_keygen_session.go     BLS key generation
  bls_signing_session.go    BLS threshold signing
  sr25519_keygen_session.go SR25519 key generation
  sr25519_signing_session.go SR25519 threshold signing
  reshare_session.go   Key resharing (rotate shares without changing public key)
  recovery.go          Key share recovery
  tfhe_session.go      FHE threshold decryption sessions
pkg/api/             HTTP API (key generation, signing, key info, health)
pkg/transport/       P2P transport (consensus mode ZAP, legacy NATS)
pkg/messaging/       PubSub abstraction over transport layer
pkg/db/              Key share storage (SQLite default, PostgreSQL optional)
pkg/kvstore/         Encrypted key-value store (AES-256, age encryption)
pkg/keyinfo/         Key metadata management
pkg/identity/        Node identity (Ed25519 keypair, mutual authentication)
pkg/backup/          Encrypted periodic backups
pkg/encryption/      AES-256-GCM encryption for key material at rest
pkg/kms/             KMS integration for secret management
pkg/hsm/             HSM provider abstraction (env, AWS, GCP)
pkg/client/          Go client library for MPC API
pkg/event/           Event types (keygen, sign, reshare)
pkg/eventconsumer/   Event processing pipeline
pkg/protocol/        Wire protocol messages
pkg/settlement/      Trade settlement signing
pkg/smart/           Smart contract transaction construction
pkg/custody/         Custody policy engine
pkg/integrity/       Key share integrity verification
pkg/txtracker/       Transaction lifecycle tracking

Security Properties

  • Key shares encrypted at rest with AES-256-GCM (key from HSM/KMS)
  • Inter-node messages authenticated with Ed25519 signatures
  • No key share leaves the node unencrypted
  • Resharing rotates shares without changing the public key or requiring all nodes
  • Backup files encrypted with age (modern, audited encryption)
  • Secret key bytes zeroed from memory after use (pkg/mpc/secret.go)

Quick Start

# Node 0
mpcd start --node-id node0 --listen :9999 --api :9800 \
  --threshold 2 --peer node1:9999 --peer node2:9999

# Node 1
mpcd start --node-id node1 --listen :9999 --api :9801 \
  --threshold 2 --peer node0:9999 --peer node2:9999

# Node 2
mpcd start --node-id node2 --listen :9999 --api :9802 \
  --threshold 2 --peer node0:9999 --peer node1:9999

Generate a Wallet

curl -X POST http://localhost:9800/v1/keygen \
  -H "Content-Type: application/json" \
  -d '{"wallet_id": "w-001", "key_type": "secp256k1"}'

Sign a Transaction

curl -X POST http://localhost:9800/v1/sign \
  -H "Content-Type: application/json" \
  -d '{"wallet_id": "w-001", "message": "0xdeadbeef...", "key_type": "secp256k1"}'

Go Client

import "github.com/luxfi/mpc/pkg/client"

c := client.New("http://localhost:9800")
result, err := c.CreateWallet("w-001", "secp256k1")
sig, err := c.Sign("w-001", txHash, "secp256k1")

Configuration

config.yaml or environment variables (config keys uppercased — Viper AutomaticEnv):

mode: consensus
environment: local          # mainnet | testnet | local
mpc_threshold: 2
max_concurrent_keygen: 2
db_path: "."                # SQLite (default), or postgres:// URL
backup_enabled: true
backup_period_seconds: 300
backup_dir: backups

All secrets are sourced from KMS via --hsm-provider=env|aws|gcp. No plaintext secrets in config.

Deployment

Docker Compose

docker compose up

Kubernetes

cd k8s && kubectl kustomize . | kubectl apply -f -

K8s manifests are in k8s/ with Kustomize overlays. Production deployment uses cloudbuild.yaml for CI/CD to GHCR.

Testing

go test ./...       # 331 test functions

# End-to-end (3-node cluster)
cd e2e && make test

Papers

Dependencies

License

Lux Ecosystem License v1.2. See LICENSE.

S
Description
▼ LUX multi-party compute nodes / wallet service using CGGMP21, FROST protocol
Readme
47 MiB
Languages
Go 83.8%
TypeScript 14%
Shell 0.9%
Solidity 0.7%
HCL 0.2%
Other 0.2%