luxfi/threshold v1.12.7 added FROST(Ed25519, SHA-512), so Solana and TON finally have a signable curve. The sessions named "EdDSA" were calling KeygenTaproot — BIP-340 over secp256k1 — whose key is also 32 bytes, sails through the len(EDDSAPubKey)==32 gate, and base58-encodes into a real-looking Solana address the ring can never sign for. ⚠ A LIVE WRONG-ADDRESS BUG went with it: handlers_wallets served addresses["ton"] = *wallet.SolAddress. A TON address is not a base58 pubkey — it is workchain plus a hash of the wallet contract's StateInit, base64url with a CRC16 — so the API was handing out an address that decodes to nothing. Deleted rather than guessed; TON returns when its encoding is real. The keygen tests used to call frost.KeygenEd25519 directly, which validated the LIBRARY and not the daemon: reintroducing KeygenTaproot left every package green. They now go through the production session, and so does a test that constructs the real signing session and proves the caller's bytes reach the protocol unhashed. Both original bugs reintroduced together fail 11 tests. secp256k1 keygen changes from fail-open to fail-closed, and that is the one behavioural change to the existing EVM/BTC path. An empty pubkey previously meant any of nil config, marshal failure, kvstore.Put failure or keyinfo-save failure — and was returned as SUCCESS, announcing addresses while fewer than t+1 nodes hold a share. Applying fail-closed to Solana while leaving the higher-value leg fail-open is incoherent. Also: the KMS share classifier still keyed off the retired "frost:" prefix; the CGGMP21 session published its own local failure onto the same first-wins subject as the ring's verdict, so one node's failure could beat the ring's success; and one 120s deadline shared across two sequential ceremonies starved the cheap leg. NOT YET SPENDABLE. ConsensusKeyInfoStore.Save is the only RegisterKey caller and hardcodes "secp256k1", and TriggerSign reads the curve from that field, so no product entry point can reach Ed25519 signing. The address is correct; signing it needs a wire-contract change shared with lux/wallet.
mpcd — MPC Daemon
mpcd is a threshold-signature daemon. It speaks ZAP wire protocol over
TLS 1.3 directly between peers — no NATS, no Consul, no PostgreSQL in the
critical path. The daemon is the same binary in every deployment shape;
the operating mode is selected entirely by flags.
Operating modes
There is exactly one start command (mpcd start) and the topology is
determined by the --peer and --threshold flags:
| Topology | Flags | Use |
|---|---|---|
| Standalone (1-of-1 self-loop) | --threshold 1 (no --peer) |
Smoke tests, dev sandbox |
| Networked t-of-n cluster | --threshold t --peer ... (n−1 peers) |
Production |
In all cases mpcd runs the consensus-embedded transport
(pkg/transport). The "consensus" here is local PoA membership over the
node's own Ed25519 identities — it is intentionally not the Lux mainnet
chain, because key shares must never leave the cluster.
Standalone (1-of-1)
mpcd start \
--node-id solo \
--listen :9999 \
--api :9800 \
--data /var/lib/mpcd \
--threshold 1
Starts mpcd with itself as the only quorum member. /healthz will report
signing_quorum: true immediately. Keygen and signing of fresh wallets
require ≥2 parties — CGGMP21 and FROST cannot produce shares with a
single party — so this mode exists only for boot-up smoke tests, e2e
harness fixtures, and verifying TLS / membership / API plumbing. For real
signing, run a multi-node cluster.
Networked (t-of-n)
# Node 0
mpcd start --node-id node0 --listen :9999 --api :9800 \
--data /data/node0 --threshold 2 \
--peer [email protected]:9999 --peer [email protected]:9999
# Node 1
mpcd start --node-id node1 --listen :9999 --api :9800 \
--data /data/node1 --threshold 2 \
--peer [email protected]:9999 --peer [email protected]:9999
# Node 2 — same pattern
The daemon expects the cluster size n = len(peers) + 1 and rejects start
when threshold > n. See pkg/transport/registry.go::HasSigningQuorum for
the quorum arithmetic.
Smoke test
go test -tags=integration ./cmd/mpcd/... -count=1 -timeout 90s
standalone_test.go builds mpcd, runs it with --threshold 1 on free
ports and a fresh data directory, polls /healthz until quorum holds,
and asserts the response shape. It does not exercise keygen — see e2e/
for that.