ZAP is the binary protocol — like HTTP=80 and HTTPS=443, ZAP=9999
across the Lux ecosystem. The DNS name (e.g. zap.kms.svc, zap.mpc.svc,
mpc-node-headless.lux-mpc.svc) disambiguates the service; the port is
always the same.
Changes:
- mpcd --listen default :9651 → :9999
- pkg/transport/{transport,factory}.go: comments and examples
- k8s/mpc-statefulset.yaml: ports + peer template
- k8s/network-policy.yaml: ingress/egress allow rules
- cloudbuild.yaml: EXPOSE
- deployments/customer-3of5/{compose.yml,k8s/*,topology.yaml}: full
template sweep — every peer hostname is distinct, port is uniform 9999
- README.md / LLM.md: example commands
luxd validator P2P (9631 mainnet, 9641 testnet, 9651 devnet) is a
DIFFERENT protocol on a DIFFERENT pod and is unchanged.
Follow-up to 0c38bbd. The CLI/CRD `--threshold` flag surfaces the
OPERATOR-facing "minimum signers required" count (e.g. threshold=2 for
a 2-of-3 ensemble, threshold=3 for 3-of-5). The CGGMP21 signing API
internally stores the polynomial degree (one below signers-required)
in keyInfo.Threshold — so the sign code continues to compare
`len(signerPeerIDs) >= keyInfo.Threshold + 1`. Those two surfaces are
off-by-one by protocol library convention.
HasSigningQuorum is operator-facing (used by /healthz) so it uses the
CRD/CLI semantics directly: quorum holds when `readyCount >= threshold`.
Scientist's repro: 3-node 2-of-3 ensemble (CRD threshold=2) with one
peer down — readyCount=2, threshold=2 → quorum holds, /healthz returns
200. With the previous commit the arithmetic was readyCount>=threshold+1
which would have required all 3 nodes up.
Tests updated accordingly (Test_HasSigningQuorum_3Node_2of3 now uses
threshold=2 matching the deployed CRD, Test_HasSigningQuorum_5Node_3of5
uses threshold=3, boundary test covers the new arithmetic).
Survivor nodes in a t-of-n ensemble now report healthy so long as they
still have enough ready peers (including self) to form a signing quorum
for their configured threshold. Previously, ArePeersReady() was the gate
and any single peer loss collapsed availability on the remaining nodes.
Repro (Scientist, 2026-04-20): scale liquid-mpc-mpc-node StatefulSet
from 3 → 2 replicas in the liquidity devnet; surviving pods returned HTTP
503 with log "Peers not ready expected=3 ready=2" even though 2 nodes
are sufficient for the 2-of-3 (threshold=1) signing quorum.
Fix:
- pkg/transport/registry.go: add HasSigningQuorum(threshold) which returns
true when readyCount >= threshold+1 (the CGGMP21/FROST signers-required
count used everywhere in pkg/mpc/node.go).
- pkg/mpc/registry.go: matching method on the legacy Consul-backed
registry plus interface addition to PeerRegistry.
- cmd/mpcd/main.go: /healthz handler switched to HasSigningQuorum(threshold)
for the 200/503 gate. ArePeersReady() is still reported in the JSON
body (as `ready`) for observability. `status` becomes
"healthy" / "healthy-reduced" / "degraded" so operators can see the
in-between state.
- cmd/mpcd/main.go: ConsensusPeerRegistry wrapper delegates the new
method to the underlying transport registry.
Keygen sessions deliberately continue to require ArePeersReady() — a
fresh wallet still needs every participant present. Only the /healthz
gate is relaxed, which matches the availability story for signing.
Tests: pkg/transport/registry_test.go covers
- Test_HasSigningQuorum_3Node_2of3 (the Scientist repro — survivors
still quorum-ok after one peer loss)
- Test_HasSigningQuorum_5Node_3of5 (larger ensemble, f=2 tolerance)
- Test_HasSigningQuorum_ArePeersReady_Decoupled (the two APIs do not
gate each other)
- Test_HasSigningQuorum_BoundaryCheck (pins arithmetic: required =
threshold+1, not threshold)
All 11 test cases pass; transport + mpc package suites clean.
Vite + React SPA with 9 pages (Dashboard, Wallets, Vaults, Policies,
Settlements, Bridge, Payments, Audit, Settings) served at /_/mpc/ via
go:embed. Replaces the separate Next.js dashboard pod — the UI now ships
inside the mpcd binary. Uses @tanstack/react-query, wouter hash router,
dark theme, inline styles. Build output: 272KB.
Matches the sshd/ssh, httpd/http unix pattern. Single Docker image
now ships both binaries: ENTRYPOINT is mpcd (the daemon); override
with 'mpc <subcommand>' for CLI ops (peers, identity, recover, etc).
Also aligns with lux/kms (single binary with subcommands) — operators
get the same shape across all Base-family daemons.
- Add bearer token auth to internal MPC API (port 9800): /keygen, /keys,
/backup now require Authorization header. Token from MPC_INTERNAL_API_KEY
env (KMS-synced) or derived from Ed25519 node identity.
- Replace DualModeListener with TLSOnlyListener: plaintext peer connections
are rejected. TLS 1.3 with PQ key exchange is mandatory.
- Add dedicated 20 RPM rate limiter on bridge signing and intent signing
endpoints (separate from global 100 RPM).
- Add body size limit (1MB), read/write timeouts, and 10 RPM rate limiter
to internal MPC API server.
- Restrict K8s NetworkPolicy egress for MPC nodes to peers, postgres,
valkey, DNS, and HTTPS only (was unrestricted).
- Wire cluster-api-key in api-deployment for authenticated proxy to
internal MPC API. Forward as Authorization: Bearer header.
- Remove hardcoded dev JWT_SECRET default from compose.yml (now required).
- Add internal-api-key to KMSSecret CRD and StatefulSet env.
- /health returns connected_peers as int (count) not string array
- /keygen derives eth_address from 32-byte keys (was >= 33, now >= 32)
- Skip initiator verification when no explicit key configured (consensus mode)
- ConsensusIdentityStore now uses node's own key as initiator key when
no explicit event_initiator_pubkey is configured (consensus mode)
- /keygen HTTP endpoint signs GenerateKeyMessage with node's Ed25519 key
- ConsensusMPCBackend.TriggerKeygen/TriggerSign sign messages before publish
- apiOnlyMPCBackend forwards to /keygen and /health (internal API paths)
instead of /api/v1/keygen and /api/v1/status (dashboard API paths)
- mpc-api deployment points to port 9800 (internal API, no auth required)
instead of port 8081 (dashboard API, auth required)
- /health response uses snake_case keys matching ClusterStatus JSON tags
- goimports -w on all pkg/api, pkg/db, cmd/mpcd files
- TestError_PanicsOnOddKeyValues → TestError_OddKeyValuesLogsWarning
(logger was intentionally changed to warn+sentinel instead of panic)
- int(threshold) → threshold (threshold is already int from c.Int())
- ConsensusIdentityStore.VerifyInitiatorMessage: implement real Ed25519
signature verification using the configured event_initiator_pubkey
instead of blindly accepting all messages
- cggmp21Aggregator.VerifyShare: implement ECDSA partial signature
verification using secp256k1 curve arithmetic (R' = u1*G + u2*Y_i,
check R'.x == r mod n) against the party's public key share
- frostAggregator.VerifyShare: implement Schnorr/BIP-340 partial
signature verification (compute R_i = s_i*G - c*Y_i, verify on curve)
- logger.Error: replace panic on odd keyValues with warning log and
sentinel value to avoid crashing production
- session.ProcessInboundMessage/ProcessOutboundMessage: replace panics
with error log and graceful no-op for unoverridden base methods
- eddsaReshareSession: implement ProcessInboundMessage (unmarshal wire
format, dedup, route to FROST party) and ProcessOutboundMessage (read
from party Messages channel, wrap in wire format, send)
- cggmp21ReshareSession: implement matching message handlers
- eddsaReshareSession.Reshare: extract actual Ed25519 public key from
FROST result via GetPublicKeyBytes() instead of empty placeholder
- Implement CREATE2 address prediction in PredictAccountAddress() using
keccak256(0xff ++ factory ++ salt ++ keccak256(initCode))[12:] formula.
Also add general-purpose PredictCreate2Address() with raw [20]byte params.
- Create pkg/hsm package with PasswordProvider interface and 5 implementations:
- AWSKMSProvider: decrypts ZAPDB_ENCRYPTED_PASSWORD via AWS KMS Decrypt API
with full SigV4 signing and EC2 instance metadata credential fallback
- GCPKMSProvider: decrypts via GCP Cloud KMS REST API with GCE metadata OAuth2
- AzureKVProvider: unwraps via Azure Key Vault REST API with MSI token auth
- EnvProvider: reads from LUX_MPC_PASSWORD/ZAPDB_PASSWORD env vars (default)
- FileProvider: reads from file (K8s mounted secrets)
- Add NewPasswordProvider factory (pkg/hsm/factory.go) driven by MPC_HSM_PROVIDER
env var (values: aws, gcp, azure, env, file)
- Wire HSM provider into mpcd consensus startup, replacing direct env var read
with graceful fallback chain: HSM provider -> viper config -> dev default
- Add --hsm-provider and --hsm-key-id CLI flags with MPC_HSM_PROVIDER/MPC_HSM_KEY_ID
env var sources
- 25 tests passing (9 smart + 16 HSM)
- Replace direct github.com/luxfi/zapdb/v4 imports with github.com/luxfi/database
abstraction in pkg/kvstore/ (zapdb.New + encdb.New for encryption)
- BadgerKVStore now uses database.Database interface internally — backend is
pluggable (zapdb/leveldb/memdb) without changing MPC code
- Remove custom badger_logger.go (database module handles its own logging)
- Update BadgerBackupExecutor to use database.Database instead of *badger.DB;
RestoreAllBackupsEncrypted loads into raw zapdb (values are already
individually encrypted by encdb)
- Rewrite backup_test.go to use KVStore interface instead of raw badger API
- Update migration script to use KVStore Put/Get/Delete/Keys instead of txn API
- Rename all user-visible "BadgerDB"/"Badger" strings to "ZapDB" in logs,
prompts, and comments across cmd/mpcd/main.go, pkg/kvstore/, pkg/transport/
- Fix backup_test.go type reference: badgerBackupExecutor→BadgerBackupExecutor
(resolves CI test and lint failures)
- Replace github.com/dgraph-io/badger/v4 with github.com/luxfi/zapdb/v4 v4.9.2
throughout pkg/kvstore/, pkg/transport/, cmd/mpcd/, e2e/, scripts/
- Rename BadgerPath/BadgerPassword → ZapDBPath/ZapDBPassword in FactoryConfig
- Remove local replace directives from go.mod:
- hanzoai/orm: now uses published v0.3.2 (tagged on GitHub)
- hanzoai/kv-go/v9: now uses published v9.17.2-hanzo.1
- Only remaining replace: agl/ed25519 → luxfi/edwards25519
- Add vendor/ directory (GOWORK=off go mod vendor) for reproducible builds
- Update Dockerfile to golang:1.26-alpine + -mod=vendor build flag
- Update CI/CD to use self-hosted linux/amd64 runners (no cross-compilation)
- Add k8s/valkey.yaml for session KV store
- Update k8s manifests: api-kv support, ZapDB password env, fresh secrets
- Dashboard API and MPC nodes all tested running on cluster (3/3 nodes healthy)
TriggerSign was subscribing to mpc_signing_result.{txID} but the
signing session publishes to mpc_signing_result.{walletID}. Also
fixed result parsing: R/S are []byte (base64 in JSON), not hex strings.
ConsensusKeyInfoStore.Get() was returning empty ParticipantPeerIDs,
causing "Not enough participants to sign" errors. Now populates from
peer registry's ready peers list.
- TriggerSign now looks up key type from keyInfoStore (was empty, causing
"CGGMP21 only supports Secp256k1" errors)
- Wire keyInfoStore into ConsensusMPCBackend
- Increase HTTP middleware timeout from 30s to 120s for MPC signing
CGGMP21 keygen returns 32-byte x-coordinate, not 33-byte compressed key.
Added secp256k1 point decompression (y² = x³ + 7 mod p) to recover the
full uncompressed public key for keccak256 address derivation.
mpcd api --db $DATABASE_URL --jwt-secret $JWT_SECRET --listen :8081
Runs the Dashboard API server without MPC transport, suitable for
cloud dashboard deployments where MPC operations are handled by
separate MPC nodes.
- Add POST /keygen endpoint for triggering MPC keygen via HTTP API
- Fix PubSub self-delivery: Publish now delivers to local subscribers
before broadcasting to peers (required for consensus mode keygen)
- Increase KeyGenTimeOut from 30s to 120s (Paillier prime generation
takes 6-15s per node, multi-round protocol needs more time)
- Add pubKeyToEthAddress helper for deriving ETH addresses from ECDSA keys
- Bump version to 0.3.3
- Add S3 backup manager (pkg/backup/s3.go) for periodic encrypted BadgerDB
backups with upload to S3-compatible storage (MinIO)
- Add 30s keepalive pings in transport layer to prevent idle read timeouts
- Increase read timeout from 30s to 5 minutes
- Add connection tie-breaking for simultaneous peer connections (lower node
ID keeps outbound) to prevent connection storms
- Add pointer-specific peer removal (removePeerConn) to prevent orphaned
goroutines from removing replacement connections
- Export BadgerBackupExecutor type for use by backup package
- Add /backup HTTP endpoint for on-demand backup triggers
- Bump version to 0.3.2
- Update .golangci.yml to v2 format with formatters section
- Fix goimports for local package grouping (tfhe_session.go, crypto_adapter.go, manager_test.go)
- Remove unnecessary int() conversion in mpcd main.go
luxfi/hsm v1.1.3 publishes the missing surface (AirgapTransport/Format,
plus coldcard/foundation/keystone/ngrave/gridplus/ledger/trezor signers
and pkcs11/yubihsm/nitrokey) that cmd/mpcd airgap_command.go was gated
on. The file-mediated ceremony orchestration stays as-is — Coldcard
microSD and BBQr/UR QR flows are the canonical product UX, not stubs.
What changes:
cmd/mpcd/airgap_command.go drop the v1.1.2 'gated downstream'
caveat from package doc; the surface
is published.
cmd/mpcd/airgap_command_test.go new round-trip test that asserts every
airgapProviders() name is constructible
via hsm.NewSigner (catches rename drift
or missing factory cases at `go test`,
not at first ceremony).
commit d83b328 ("Fireblocks-parity gas station + smart-transfer +
fee-bump") added a reference to airgapCommand() in cmd/mpcd/main.go
without committing the supporting source. cmd/mpcd has not built on any
descendant of feat/canonical-intent-cto since.
cmd/mpcd/airgap_command.go is the operator surface for airgapped
hardware wallet workflows (Coldcard, Foundation Passport, Keystone Pro,
NGRAVE Zero):
mpcd airgap devices — list reserved provider names
mpcd airgap fips-check <provider> — verify FIPS 140 validation
mpcd airgap sign --provider X ... — drive a file-mediated ceremony
The signing ceremony is file-mediated: the challenge is written to a
working directory, the operator hand-carries the file to the offline
device via microSD or QR, and the signed response is dropped back into
the same directory. Polled until --timeout (default 30m) or context
cancellation. crypto/rand session IDs, 0o600 file modes, automatic
cleanup of the session's req/resp pair on exit.
The provider list is the canonical reservation already documented in
luxfi/hsm: coldcard, foundation, keystone, ngrave. In-process Signer
construction for these names is gated on a downstream luxfi/hsm release
that exports the airgap factory cases; until then this subcommand owns
the ceremony orchestration directly. The /v1/mpc/sign path is
unaffected.
FIPS gating mirrors the policy table in luxfi/hsm/factory.go:
RequireFIPSProvider — personal hardware wallets are never FIPS 140
validated and fail closed at provisioning.
Stdlib + urfave/cli/v3 only. No new module deps.
Build: cmd/mpcd builds clean on this branch.
Smoke: airgap devices / fips-check {coldcard,aws} all return correctly.
Baseline: pre-existing pkg/api regulator + pkg/custody json_extract
failures present on parent commit, unchanged by this diff.
Adds the topology contract that lets a fund run mpcd as its own private
cluster — no Lux primary network registration, no shared M-Chain audit
ledger — using the same code path and the same operator surface.
Mode flag at cmd/mpcd/main.go:
--network=primary|private # registration target
--audit-store=mchain|local-worm|... # audit log destination
--registrar-url, --registrar-auth # primary-mode announcement
--audit-worm-path, --audit-mchain-url # backend selectors
pkg/audit:
- Append-only Merkle hash chain. Tampering with any historical event
invalidates every subsequent hash. Canonical bytes use fixed-order
concatenation (json.Marshal map ordering is non-deterministic).
- WORMDispatcher: NDJSON append, replay-on-start, fsync per Append.
- MChainDispatcher: batched POST of (batchHash, fromSeq, toSeq) to a
configurable anchor RPC.
- compositeDispatcher: fan-out to multiple legs with per-leg sealing.
- S3 Glacier Vault / Azure Immutable / GCS Object Lock dispatchers
fail closed — wired into the flag surface but gated behind the
cloud-audit build tag, no silent fallback.
pkg/identity/bootstrap.go: identity creation + (primary-mode only)
registrar announcement. Same on-disk format as the previous
loadOrGenerateIdentity (which is now removed); private mode is silent.
deployments/k8s-private: fund-only Kustomize overlay. Standalone
namespace, NetworkPolicy that blocks egress to anything outside the
fund's VPC, hardware-WORM PVC for the audit log, KMS-driven secret
template.
deployments/k8s-primary: env-var patch overlay on the existing lux-mpc
base. composite(local-worm,mchain) by default.
deployments/terraform/{aws,gcp,azure}: VPC + audit storage modules.
terraform validate passes; EKS/GKE/AKS + HSM modules are stubbed and
not yet plan-tested.
deployments/onprem/README.md: WORM storage + PKCS#11 HSM checklist.
Tests (14 total, all passing):
pkg/audit: 6/6 (chain, WORM replay, MChain batching, composite,
cloud stubs fail loud, double-seal rejected)
pkg/identity: 4/4 (private no-call, primary call, primary empty
URL no-op, primary registrar 503 surfaces)
internal/privatemode: 4/4 (no registrar traffic, restart, tamper-detect,
cloud stubs fail loud)
Companion runbook in lux/papers/lux-private-mpc-deployment/
(LaTeX, 5 pages, tectonic-clean).
Three new pure-Go modules close the last inline gaps between Fireblocks
and lux/mpc:
pkg/automation/gasstation.go auto top-up of operational wallets
pkg/automation/smarttransfer.go cheapest-route quoting across providers
pkg/automation/feebump.go RBF (BTC) + EIP-1559/legacy fee bump
Plus the parity matrix and migration runbook:
FIREBLOCKS_PARITY.md 67-feature matrix, 76% shipped
MIGRATION_FROM_FIREBLOCKS.md operational playbook, 4-6 weeks for $1B+
Stdlib + math/big only. 18/18 tests passing.