The two MPC files that still carried Ringtail names:
cmd/mpcd/main.go — comment ref to luxfi/ringtail
pkg/api/handlers_kms.go — ValidatorKeySet wire schema
handlers_kms.go renames:
ValidatorKeySet.RingtailWalletID → CoronaWalletID
ValidatorKeySet.RingtailPublicKey → CoronaPublicKey
JSON tags ringtailWalletId/PubKey → coronaWalletId/PubKey
local `ringtailWalletID` and the per-validator suffix
"validator-<id>-ringtail" → "validator-<id>-corona"
router switch `case "ringtail":` → `case "corona":`
cmd/mpcd/main.go: comment "luxfi/corona and luxfi/ringtail" was
collapsed to just "luxfi/corona" — the dispatcher's not-yet-impl
stub now references only the live R-LWE library.
Wire break, no compat layer (forwards-perfection policy):
KMS callers must re-issue requests with the new field names.
Build: go build ./... PASS.
Tests: pkg/api PASS 1.374s.
Red round 5 finding B1. The embedded dispatcher on --threshold-listen
was an anonymous local signing oracle:
1. Any process on the host could POST `{"method":"frost.sign",...}`
and obtain signatures with no auth gate (only the API on :9800
enforced MPC_INTERNAL_API_KEY).
2. An operator typo `MPC_THRESHOLD_LISTEN=0.0.0.0:7300` exposed it
cluster-wide.
3. Any SSRF inside mpcd pivoted into the dispatcher.
Fix mirrors the existing internal-API auth pattern (cmd/mpcd/main.go
line ~456):
- Auth token: read MPC_THRESHOLD_AUTH_TOKEN; if unset, derive a
deterministic per-cluster token from the node Ed25519 seed
(SHA-256(seed || "mpc-threshold-rpc") hex). All nodes share the
same key without extra config; log warns when derivation is used.
Server.SetAuthToken installs the gate.
- Loopback bind: refuse non-loopback addr unless
MPC_THRESHOLD_ALLOW_REMOTE=1 — operators must explicitly
acknowledge the exposure. Bare `:port` (= 0.0.0.0) is NOT loopback.
Also picks up luxfi/threshold v1.6.10 which removes the pulsar/corona
in-memory-token surface (Red HIGH B2) — pulsar.*/corona.* now return a
typed "not yet implemented" error.
Tests:
- TestIsThresholdLoopback locks the loopback predicate (8 cases:
127.0.0.1, [::1], localhost OK; 0.0.0.0, :port, RFC1918 refused).
- TestEmbeddedDispatcher_AuthGate proves 401 on missing/wrong token,
200 on valid token through the dispatcher mpcd installs.
4 tests pass in cmd/mpcd (was 2, +2).
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".
Cancel was documented as "cancels a pending session belonging to the
caller's org" but the contract did not name HOW the implementation
enforces that. A loose implementation could:
- read session, transition state, then check org_id (TOCTOU window)
- distinguish "not yours" from "not found" in the error message
(enumeration oracle leaks the existence of cross-org sessions)
- skip the org predicate when a concurrent legitimate caller also
has the row open (race condition lets the wrong-org call win)
Tightened doc names the three required behaviors:
- orgID derived from ctx Claims, not caller-supplied
- SELECT-FOR-UPDATE + WHERE org_id = ctx.OrgID; mismatch returns
ErrSessionNotFound (same opaque response as unknown id)
- row-level CAS with the org predicate in WHERE, so two callers
cannot race the state transition
- idempotency on already-cancelled scoped to the SAME org
go.work bumped to 1.26.3 to satisfy the per-module go directive.
* ringtail: fix lux/mpc go.sum corruption (luxfi/edwards25519)
The recorded go.sum hash for github.com/luxfi/[email protected] was
stale — the proxy now serves go.mod content (`module github.com/luxfi/
edwards25519\n\ngo 1.21\n`) that hashes to MBoV+bPE..., while go.sum had
the older tFuuyXvI... value (matching whatever go.mod content was first
published, evidently `module github.com/agl/ed25519`). Result: every Go
build in the workspace, including ringtail's dkg tests, errored with a
checksum mismatch SECURITY ERROR.
Fix: remove the two stale edwards25519 lines and re-run go mod tidy so
the current proxy content is re-pinned. New hashes:
h1:YPoT831TZMslvNyy/KuTtpHi4BGZWgpwak06RCaYLzo= (zip)
h1:MBoV+bPEz1tSMADPMSUaYdZ+agY3syG8B6z7RmJBH0A= (mod)
Verified: `cd ~/work/lux/ringtail && go test ./dkg/...` passes
TestDKG_2of3, TestDKG_3of5, TestDKG_InvalidParams, TestDKG_MissingData
(env override GOENV=/dev/null + GOPROXY=https://proxy.golang.org,direct
to bypass the unrelated GONOPROXY=hanzoai/* setting in the user's global
go env that prevents the proxy from serving the cached but git-deleted
hanzoai/[email protected] — that's a separate workspace-config issue,
not a go.sum corruption).
* fix(build): enable CGO so go-sqlite3 driver registers
The /v1/mpc/wallets HTTP API was 503'ing because CGO_ENABLED=0
left mattn/go-sqlite3 unregistered, so the wallet tables couldn't
open. Workaround was seeding wallets into TA's user_wallets table.
Switch to CGO=1 with statically-linked external linker so the binary
still runs in a scratch/alpine runtime without sqlite-libs at runtime.
Users/Webhooks/Whitelist/APIKeys/Devices/Login/Operations and the
webauthn.ts shim referenced API methods, hooks, and types that were
never implemented in lib/api.ts or hooks/use-mpc.ts. None are routed
in App.tsx — pure dead code that broke `pnpm build` (and therefore
the Docker UI stage).
Routed pages remain: Dashboard, Wallets, Vaults, Policies,
Settlements, Bridge, Payments, Audit, Settings.
Unblocks ghcr.io/luxfi/mpc:v1.13.2 publish for #70 ZAP rollout.
CI Docker workflow has been failing across v1.10.x-v1.13.0 with go.sum
checksum mismatch on github.com/luxfi/[email protected].
Root cause: the v0.1.0 git tag was rewritten upstream after publication.
- proxy.golang.org has the original commit dff0e9e2 (module path:
github.com/agl/ed25519, satisfying replace directive) -> h1:tFuu...
- live git tag now points to eee5e2c2 (module path: luxfi/edwards25519)
-> h1:MBoV... which is what was stale in go.sum.
CI hits proxy.golang.org by default and gets tFuu..., mismatching the
stale MBoV... in go.sum. sum.golang.org records confirm tFuu... as the
authoritative hash.
Fix: rewrite both go.sum lines for luxfi/[email protected] to the
proxy.golang.org / sum.golang.org authoritative values. No code change,
no go.mod change.
Verified locally with GOPRIVATE temporarily cleared:
GOPROXY=https://proxy.golang.org GOSUMDB=sum.golang.org go mod download
go build ./... (clean)
go vet ./... (clean)
go test -short ./pkg/webauthn/... ./pkg/db/... (pass)
The Liquid operator already provisions a Service exposing port 9653
on every MPC StatefulSet (spec.zapPort=9653). Match that default so
no operator/CR change is required for v1.13.0 rollout.
Adds threshold-signing transport so KMS can use mpcd as MPC backend.
Reuses ML-KEM-768 hybrid handshake from luxfi/kms/pkg/zap (#55) by
vendoring the same construction into mpc/pkg/zap — wire constants
(opcodes 0x00F0/0x00F1, HKDF info "kms/zap/v1/session", direction
tags) stay in lockstep with KMS.
New surface:
- pkg/zap: vendored handshake + session (forward-only convergent)
- pkg/api/zap_kms_server.go: KMS-facing ZAP node (opcodes
0x0001/0x0010/0x0011/0x0012/0x0020) wired onto MPCBackend
- cmd/mpcd: --kms-zap-listen flag (default :9970), enabled out of the
box; default-disable by passing empty string
Existing pkg/api/zap_server.go (dashboard event bus, opcodes 0x0060+)
is unchanged — different consumer, different port.
Closes the secrets-only mode gap on mainnet (#70).
TDX.Verify and NRAS.Verify panicked when reached during stage 1 of
issue #222, taking the whole MPC process down whenever Dispatch was
called from a request-handling goroutine. The fail-loud guarantee is
preserved by returning ErrNotImplemented (a hard refusal — callers
must not paper over it) so the release gate still refuses, but the
process survives. Updates verifier_test.go's stub coverage from
TestStubsPanic to TestStubsReturnNotImplemented and adds Dispatch-path
coverage for the same behaviour.
Document mpcd's two topology modes explicitly in cmd/mpcd/README.md:
standalone (1-of-1 self-loop) for smoke tests / dev sandboxes, and
networked (t-of-n) for production. The same binary handles both — only
flags differ.
Add cmd/mpcd/standalone_test.go behind the integration build tag. It
builds mpcd, runs it with --threshold=1 and no peers, polls /healthz
until signing_quorum=true, and asserts the response shape (mode,
threshold, version). Keygen is intentionally NOT exercised — CGGMP21
and FROST cannot produce shares with a single party.
Run: go test -tags=integration ./cmd/mpcd/... -count=1 -timeout 90s
Honest scope-limit: mpc trunk has a pre-existing build break in
pkg/policy (undefined fhethr.* symbols) and pkg/api (handlers_approval
references s.approval that's not on the Server struct). The smoke test
itself is correct and passes type-check against cmd/mpcd; it will go
green once the upstream policy/api regression is resolved.
Red Final N1 fix. Release() previously called only Attestation.Verify(nonce)
and ReleasePolicy.Permits() before sealing the session key. Neither step
performs the actual SEV-SNP / TDX / NRAS chain verification — AMD KDS
lookup, ARK→ASK→VCEK chain, NRAS JWT — leaving the #222 go-sev-guest
verifier as dead code on the release path. A worker that fabricates a
CompositeAttestation envelope whose Verify(nonce) returns true and whose
RIM/HW/issuer membership matches policy could harvest a session key
without ever presenting AMD-signed evidence.
Wire-in:
- kms.CompositeAttestation gains VerifyEvidence(ctx, opts...) returning
[]*attest.VerifiedReport. *attestation.CompositeAttestation already
implements this method (composite.go:320 → attest.Dispatch per blob).
- ReleaseRequest gains optional Ctx + AttestOptions for context plumbing
+ per-call policy (e.g. attest.WithExpectedReportData(nonce[:])).
- Release() calls VerifyEvidence between the cheap nonce check and the
policy permit step. Any chain failure → ErrAttestationChain joined
with ErrPolicyRefused via errors.Join. No silent acceptance.
- New sentinel ErrAttestationChain.
Tests (go test -v -short -race -count=1 -timeout 60s ./pkg/kms/...):
- all 14 existing TestRelease_/TestNonce_/TestAAD_ remain green
- NEW TestRelease_RefusesOnChainFailure: Verify(nonce)=true but
VerifyEvidence returns ErrChainInvalid → MUST surface
ErrAttestationChain wrapped under ErrPolicyRefused
- NEW TestRelease_AcceptsOnChainPass: paired happy-path so the
chain-step is the only difference, not a wiring regression
Also pulled in cherry-pick of canonical go-sev-guest sums (baaeca8) so
cc/attest builds reproducibly outside the dirty local cache.
Stale luxfi/edwards25519 v0.1.0 sums caused checksum mismatch on every
go build. Replaced with sumdb canonical hashes from proxy.golang.org.
h1:zuSrBtgV4... -> h1:YPoT831TZMs... (zip)
h1:tFuuyXvI+... -> h1:MBoV+bPEz1t... (go.mod)
Verified via: go mod download -json github.com/luxfi/[email protected]
Same drift class as the luxfi/log + base sum churn from this week.
Tests: 28 packages that previously halted at SECURITY ERROR now
compile/run. Remaining failures (api/client/mpc/policy build, attestation
setup, custody json_extract) are unrelated drift classes for separate
tickets.
The threshold-FHE wallet keygen path (pkg/mpc/tfhe_session.go:154,165
production caller via pkg/mpc/node.go:583) was wired to
luxfi/threshold/protocols/tfhe.NewKeyGenerator + GenerateKeys. Per #197,
that primitive is a fail-loud placeholder: every party stores the full
master key, PartialDecrypt returns an HMAC tag, and CombineShares ignores
partials and runs single-party decryption. Upstream guards every entry
point with guardUnsafe() that panics unless
LUX_ALLOW_FAKE_TFHE_FOR_TESTING_ONLY=1 is set — by design.
Default production builds must not be able to reach the placeholder
primitive even with the env var set. Real-threshold wiring (luxfi/lattice
Shamir/LWE) is tracked as a multi-week separate task per LP-137 §2.6.
Changes:
- pkg/mpc/tfhe_session.go: //go:build experimental_tfhe — entire
tfheKeygenSession + tfheComputeSession + TFHESession interface gated
behind the tag.
- pkg/mpc/tfhe_node.go (new, experimental_tfhe): Node.CreateTFHEKeyGenSession
+ CreateTFHEComputeSession constructing the experimental sessions.
- pkg/mpc/tfhe_node_stub.go (new, !experimental_tfhe): stub Node methods
that return ErrTFHENotImplemented from every entry point. This is what
the production binary ships with.
- pkg/mpc/tfhe_errors.go (new, unconditional): ErrTFHENotImplemented
symbol with a description pointing at LP-137 §2.6.
- pkg/mpc/node.go: removed the unconditional CreateTFHEKeyGenSession +
CreateTFHEComputeSession definitions and the unused luxfi/fhe import.
Test pass:
go test ./pkg/mpc/... ./pkg/policy/... -short -count=1 -race -timeout 60s
ok github.com/luxfi/mpc/pkg/mpc 2.016s
ok github.com/luxfi/mpc/pkg/policy 1.477s
91 PASS, 0 FAIL (default build, no env var set)
go build -o /tmp/mpcd_final ./cmd/mpcd/
/tmp/mpcd_final version # mpcd version 0.3.3 — no panic, no UNSAFE TFHE reach
go build -tags experimental_tfhe ./pkg/mpc/... # opt-in build OK
Note: pkg/policy/main_test.go sets LUX_ALLOW_FAKE_TFHE_FOR_TESTING_ONLY=1
because RealThresholdDecryptor's dispatcher tests exercise the placeholder
PartialDecrypter — same TestMain pattern threshold/protocols/tfhe uses.
These are orchestration-shape tests; they verify no threshold security
property because the underlying scheme has none.
Companion bumps required for the branch to compile:
- luxfi/threshold v1.6.0 -> v1.6.4 (FHECiphertext / FHEThresholdShare /
ShareAggregator exports landed in v1.6.x)
- google/go-sev-guest -> go.mod (cc/attest/sev.go direct import)
- pkg/api/server.go: missing approval + tieredWallets fields added to
satisfy in-tree handler references
Per LP-137 §2.7, ReleasePolicy now exposes RequireSEVSNP / RequireTDX /
RequireNVNRAS boolean flags matching the C++ AttestationBaseline.require_*
pattern. Closes the asymmetry where the C++ side enforced explicit issuer
presence but the Go side only checked RIM + AllowedHardware membership.
Permits() default-deny: if a flag is true and the corresponding canonical
issuer string ("amd.sev.snp", "intel.tdx", "nvidia.nras.v1") is absent
from att.EvidenceIssuers(), the policy refuses release.
NewReleasePolicyStrict bundles the production posture (all three flags
true) so deployments cannot accidentally ship a wide-open policy. Tests
opt out via NewReleasePolicy + leaving the flags false.
Interface change: CompositeAttestation gains EvidenceIssuers() []string.
Wire-stable strings exported as IssuerSEVSNP / IssuerTDX / IssuerNVNRAS
matching cc/attest dispatch keys.
Tests: 4 new cases - happy path (all 3 issuers present + valid),
missing SEV-SNP, missing TDX, missing NVNRAS (incl. nil-issuer edge).
All 19 pkg/kms tests pass with -race -count=1 -short -timeout 60s.
Stale luxfi/edwards25519 v0.1.0 sums caused checksum mismatch on every
go build. Replaced with sumdb canonical hashes from proxy.golang.org.
h1:zuSrBtgV4... -> h1:YPoT831TZMs... (zip)
h1:tFuuyXvI+... -> h1:MBoV+bPEz1t... (go.mod)
Verified via: go mod download -json github.com/luxfi/[email protected]
Same drift class as the luxfi/log + base sum churn from this week.
Tests: 28 packages that previously halted at SECURITY ERROR now
compile/run. Remaining failures (api/client/mpc/policy build, attestation
setup, custody json_extract) are unrelated drift classes for separate
tickets.
Commit af4139d ("deployment-mode flag + audit dispatcher") referenced
buildHSMSignerConfig at the HSM signer wiring site but the helper file
was never staged — same pattern as the missing airgap files repaired
under #208. The current main.go works around the gap by passing nil to
hsm.NewSigner, which loses the operator-supplied --hsm-signer-key-id and
prevents per-provider config promotion.
cmd/mpcd/hsm_signer_config.go:
- validates provider name against the luxfi/hsm v1.1.3 factory list
(aws/gcp/azure, zymbit, yubihsm, nitrokey, pkcs11, kmip, coldcard,
foundation, keystone, ngrave, gridplus/lattice, ledger, trezor,
tr31, mldsa, local) and the documented aliases.
- promotes --hsm-signer-key-id into the canonical config key for each
cloud KMS provider (aws.key_id, gcp.crypto_key, azure.key_name).
- leaves vendor-specific surface (slot/pin/library, vault URL,
transport) to the factory's MPC_HSM_<P>_* env-var fallback so the
CLI flag surface stays minimal.
cmd/mpcd/main.go: route --hsm-signer through the helper and surface
config errors before reaching hsm.NewSigner.
cmd/mpcd/hsm_signer_config_test.go: covers cloud-KMS key-id promotion,
hardware-wallet empty-map case, alias acceptance, case-insensitive
provider names, and rejection of empty + unknown providers. Helper
+ tests pass under `go test -race -count=1` in isolation.
go.sum: refresh github.com/luxfi/edwards25519 v0.1.0 hashes to match
proxy.golang.org. Pre-existing checksum drift blocked all `go build`
attempts on this branch and on main.
Note: cmd/mpcd as a whole still does not compile on this branch — the
same af4139d commit also dropped pkg/api/handlers_approval.go without
adding the Server.approval field, and pkg/policy/fhe_threshold_decryptor.go
references symbols that never landed. Both are out of scope for #227 and
will be repaired under follow-up tickets in the same #208-style cleanup.
Adds the canonical TEE attestation verifier package at cc/attest. This
is the home for chain validation + trust anchor management; the
luxcpp/crypto/attestation parser primitives feed bytes here BEFORE
their measurement output is trusted.
Stage 1 — SEV-SNP — PRODUCTION:
- cc/attest/sev.go: SEVSNP{} verifier built on
github.com/google/go-sev-guest v0.14.1. Live VCEK fetch from AMD
KDS, ARK→ASK→VCEK chain validation, signature verify against the
leaf, MEASUREMENT/REPORT_DATA/HOST_DATA/CHIP_ID extraction,
caller-supplied policy gates (WithExpectedReportData,
WithExpectedMeasurement). 197 LOC.
- testdata/: real-vendor Milan attestation report + VCEK leaf
committed verbatim from the upstream go-sev-guest test corpus.
Tests replay the AMD KDS responses via SimpleGetter so CI never
hits the network.
- 9 SEV-SNP tests passing (happy path, dispatch, length reject,
tampered signature, wrong report_data, wrong measurement,
deterministic composite hash, unknown kind, stub-panic guard).
Stage 2 — TDX — STUB (#222 stage 2):
- cc/attest/tdx.go: TDX{}.Verify panics with ErrNotImplemented so
any caller routing TDX evidence during stage 1 fails LOUD. Plan
documented inline: go-tdx-guest + Intel PCS + QE/TDX module
identity pinning.
Stage 3 — NRAS — STUB (#222 stage 3):
- cc/attest/nras.go: NRAS{}.Verify panics with ErrNotImplemented.
Plan: lift the existing pkg/attestation/nvidia NRAS client into
the cc/attest contract with a JWKS cache.
Wire-in:
- pkg/attestation/composite.go gains VerifyEvidence(ctx, opts...)
which dispatches each Evidence blob to cc/attest by Issuer.
Callers (kms release gate, scheduler) MUST invoke this before
accepting parser output.
Doc:
- cc/attest/doc.go states the production/stub matrix and the
layering contract between the C-ABI parser and this package.
Original implementation completed by 2025-12-25. Source tree was lost
in a laptop-theft data-loss event in early 2026. Re-published 2026-04-28
from memory and audit recovery.
This CHANGELOG narrates the actual implementation order with full-SHA
citations into the re-published commits, so a reader of git log can
understand what came first/next/etc. without any commit timestamps
being rewritten.
Phases covered: KMS release nonce binding (HMAC-SHA256 with durable
NonceStore + 7 regression tests), NRAS JWS hardening (alg x key-type
strict binding, alg=none rejection), NRAS SkipSignature kill-switch
deletion, mpcd air-gap recovery (Coldcard / Foundation BBQr /
Keystone+NGRAVE UR), and the luxfi/hsm v1.1.3 dependency bump.
Closes Blue audit B4. Four nonce-binding gaps in the ReleaseGate are
closed in one pass:
1. Nonce derivation is now HMAC-SHA256(rootKey, "kms/release/v1" ||
epoch || jobID || rand32). A nonce issued for jobID A cannot be
replayed under jobID B even if the random component is identical.
2. Issue() persists (jobID, nonce, epoch, expires) in a NonceStore.
Release() looks up the stored nonce and constant-time-compares it
against req.Nonce. The forged-nonce path now refuses with
ErrNonceMismatch instead of silently accepting any 32 bytes.
3. AAD now binds the issued nonce: epoch || jobID || teePub || nonce.
Two Release() calls with the same (epoch, jobID) but different
issuances produce distinct AADs; sealed keys are non-fungible
across issuances even after a process restart.
4. State (epoch + pending nonces + consumed-set) is durable via the
NonceStore interface. Two implementations:
- MemoryNonceStore for tests / single-shot dev
- DatabaseNonceStore for prod (luxfi/database; pair with encdb +
ZapDB for at-rest encryption per Hanzo Base pattern)
Replay window default 1h; pending TTL 1h. GC reclaims both.
Tests cover all five required scenarios + nonce-binding + AAD-binding
asserts. -race clean.
Backwards-incompatible change to NewLocalReleaseGate / NewMPCReleaseGate
/ NewHSMReleaseGate signatures; no compat shim per audit guidance.
luxfi/mpc CI publishes ghcr.io/luxfi/mpc; the legacy mpc-api image
is gone. Pinning the SHA-tagged release ensures rollouts are
deterministic (matches operator pattern).
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.
Consolidating to single canonical ARC fleet. Killing duplicate lux-build
scale set. Hanzo ARC provides identical capacity (max 30 on DO + max 30
ARM on GKE T2A) across hanzo/lux/zoo/liquidity repos — one and only one
way to run self-hosted CI.
No behavior change for this workflow.
Co-authored-by: Hanzo Dev <[email protected]>
v1.1.2 renames EnvProvider default env var from
LUX_MPC_PASSWORD to MPC_PASSWORD. The binary now reads the
canonical name used by the K8s operator injection.
Follow-up to 8156f2e. 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.
CI lint was failing because ui/dist/ doesn't exist in a clean checkout.
Default build compiles with no embedded assets. Production builds that
want the bundled dashboard must build with -tags embedui after
`pnpm --dir ui build`.