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 5215d42. 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.
- 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