Trust at the network boundary (NetworkPolicy + ZAP wire), not in the
application envelope. Companion to luxfi/keys v1.2.0 (LoadMnemonic
identity-free) and luxfi/kms v1.12.0 (iamclient gone).
Killed:
- pkg/zapauth/ (JWKS verifier + AuthHelloFrame parser)
- pkg/api/zap_kms_server.go: KMSZapConfig, auth+authRequired fields,
handleAuthHello, respondAuthHello, checkAuth, StartKMSZAPWith
- cmd/mpcd: ZAP_JWKS_URL, ZAP_EXPECTED_ISS, ZAP_EXPECTED_AUDIENCES,
ZAP_AUTH_REQUIRED — all gone.
Note: mpc still has a pre-existing build block from upstream luxfi/geth
v1.16.79 → precompile pseudo-version (unrelated to this change).
Per CTO directive: kill any alias, any shim. R-LWE threshold sig is
named Corona across all live code. Forward-only.
Sweep covers Go + TS + Sol + k8s YAML + JSON + LP markdown + skills
+ docs. Skipped: papers/audits/legal/licenses/blog/HISTORY (immutable
historical record).
Canonical S3 SDK across the stack is the Hanzo S3 fork at
github.com/hanzos3/go-sdk (package `s3`). Imports use an explicit
`s3 "github.com/hanzos3/go-sdk"` alias to avoid the `go-sdk` last-path-
segment mismatch with `package s3`.
`pkg/backup/s3.go` swept: `minio.Client` → `s3.Client`, `minio.New` →
`s3.New`, `minio.Options` → `s3.Options`, `minio.MakeBucketOptions` →
`s3.MakeBucketOptions`, `minio.PutObjectOptions` → `s3.PutObjectOptions`.
Field name `s3Client *s3.Client` is fine (no shadow with the package
qualifier).
Dropped stale `luxfi/zapdb v1.0.0` entries in go.work.sum that were
blocking go mod tidy (their hashes no longer match the proxy after
zapdb's own sweep).
No behavioral changes.
* build(mpc): GOEXPERIMENT=jsonv2 alongside runtimesecret per SCALE_STANDARD
Per SCALE_STANDARD.md §2 (~/work/hanzo/hips/docs/SCALE_STANDARD.md) —
every Go production Dockerfile that emits JSON to a client builds with
GOEXPERIMENT=jsonv2. Verified on hanzoai/zip's json_bench_test.go:
- Edge POST roundtrip: -12% time, -23% allocs
- Marshal-only: -22% time
- Unmarshal-only: -19% time, -25% allocs
GOEXPERIMENT is comma-separated, so jsonv2 composes cleanly with the
pre-existing runtimesecret experiment used by the MPC data plane.
* ci: switch workflows to lux-build (luxfi-scoped ARC pool)
Cross-org runs-on dispatch failure: hanzo-build-linux-amd64 scale set is
registered to github.com/hanzoai and silently rejects luxfi/* jobs.
Switch to lux-build (githubConfigUrl=github.com/luxfi, max 20).
* test/zapauth: deterministic invalid-signature test (was flaky)
TestZapAuthInvalidSignature flipped one base64 char of the signature
segment. ECDSA accepts a wrong sig with probability ≈ 1/2^6 per
single-char flip (alphabet is 64), making the test ~1.5% flake rate
in CI.
Fix: replace the entire signature segment with all-A's. Still
decodable as base64url but cryptographically guaranteed-invalid
bytes — Verify rejection is now deterministic.
Verified: ran -count=10 ./pkg/zapauth/ — all pass.
* deps: bump accel/crypto/corona/threshold/fhe/lens/math to current tags
* api/db: decomplect — KeccakAddress on Wallet / TreasuryWallet / KeygenResult
Wave 5 of the workspace-wide eth* naming purge.
pkg/db/models.go — Wallet + TreasuryWallet:
- New canonical KeccakAddress *string field with json `keccakAddress`
- EthAddress kept with json `ethAddress` as `// Deprecated:` alias
- NormalizeAddresses() method on both structs mirrors the two fields
via the package-private mirrorAddrPtrPair helper
pkg/api/server.go — KeygenResult (backend response):
- New canonical KeccakAddress string field with json `keccak_address`
- EthAddress kept with json `eth_address` as `// Deprecated:` alias
- NormalizeAddresses() method mirrors both fields
pkg/api/handlers_wallets.go, handlers_treasury.go, handlers_bridge.go,
handlers_mpc.go — all sites that constructed wallet rows from MPC
backend results now:
1. Call result.NormalizeAddresses() on the backend response so both
KeccakAddress / EthAddress carry the same value before copy
2. Copy both fields onto the persisted Wallet / TreasuryWallet row
3. Call wal.NormalizeAddresses() to ensure DB row has both
4. Read from the canonical KeccakAddress at response-construction
sites (toWalletResponse, treasury response, bridge response)
The "ethereum"/"lux"/"xrp" labels in addresses map (handleGetWalletAddresses)
are per-chain DISPLAY labels keyed by chain name a client knows;
the values are the Keccak-derived 20-byte address. Map keys NOT
decomplected here — they're per-chain UI labels, not internal naming.
Effect on the wire:
- API consumers reading wallet.ethAddress see the same value as
wallet.keccakAddress
- New consumers can switch to keccakAddress today
- ORM persistence stores both fields (eventually consistent via
NormalizeAddresses calls at handler boundaries)
Tests: pkg/api unit tests pass (1.8s); pkg/db has no test files.
Deprecation alias retention means downstream KMS / dashboards
keep working at every step of the migration.
Per CLAUDE.md x.x.x+1.
* strip KeccakAddress + EthAddress dual-field bridge (no aliases)
Forward-only completion of the workspace strip-aliases cascade. The
canonical JSON field is evmAddress, the Go field is EVMAddress. No
NormalizeAddresses helpers, no mirrorAddrPtrPair, no deprecated
KeccakAddress, no deprecated EthAddress.
Touched:
pkg/api/server.go KeygenResult drops KeccakAddress +
EthAddress + NormalizeAddresses
pkg/db/models.go Wallet drops KeccakAddress + EthAddress
TreasuryWallet drops KeccakAddress +
EthAddress
Drops NormalizeAddresses + mirrorAddrPtrPair
pkg/api/handlers_wallets.go rip mirror calls; only EVMAddress
populated on Wallet rows
pkg/api/handlers_treasury.go rip mirror calls on TreasuryWallet
pkg/api/handlers_mpc.go rip NormalizeAddresses calls; rename
filter("ethAddress=") → ("evmAddress=")
pkg/api/handlers_bridge.go local var keccakAddress → evmAddr
pkg/api/handlers_smart_wallets.go wallet.EthAddress → wallet.EVMAddress
pkg/api/treasury_test.go mock KeygenResult EthAddress → EVMAddress
pkg/api/zap_kms_server_test.go same
cmd/mpcd/main.go pubKeyToEthAddress → pubKeyToEVMAddress,
\"eth_address\" → \"evm_address\",
local var ethAddr → evmAddr, KeygenResult
EthAddress → EVMAddress field
Deps: bump luxfi/metric v1.5.1 → v1.5.7 to pick up ExponentialBuckets
(via transitive luxfi/threshold dep).
The value IS \"EVM-runtime account address\" — that's the data model.
The Keccak256(secp256k1 pubkey) derivation is HOW.
Forward-only completion of the workspace strip-aliases cascade. The
canonical JSON field is evmAddress, the Go field is EVMAddress. No
NormalizeAddresses helpers, no mirrorAddrPtrPair, no deprecated
KeccakAddress, no deprecated EthAddress.
Touched:
pkg/api/server.go KeygenResult drops KeccakAddress +
EthAddress + NormalizeAddresses
pkg/db/models.go Wallet drops KeccakAddress + EthAddress
TreasuryWallet drops KeccakAddress +
EthAddress
Drops NormalizeAddresses + mirrorAddrPtrPair
pkg/api/handlers_wallets.go rip mirror calls; only EVMAddress
populated on Wallet rows
pkg/api/handlers_treasury.go rip mirror calls on TreasuryWallet
pkg/api/handlers_mpc.go rip NormalizeAddresses calls; rename
filter("ethAddress=") → ("evmAddress=")
pkg/api/handlers_bridge.go local var keccakAddress → evmAddr
pkg/api/handlers_smart_wallets.go wallet.EthAddress → wallet.EVMAddress
pkg/api/treasury_test.go mock KeygenResult EthAddress → EVMAddress
pkg/api/zap_kms_server_test.go same
cmd/mpcd/main.go pubKeyToEthAddress → pubKeyToEVMAddress,
\"eth_address\" → \"evm_address\",
local var ethAddr → evmAddr, KeygenResult
EthAddress → EVMAddress field
Deps: bump luxfi/metric v1.5.1 → v1.5.7 to pick up ExponentialBuckets
(via transitive luxfi/threshold dep).
The value IS \"EVM-runtime account address\" — that's the data model.
The Keccak256(secp256k1 pubkey) derivation is HOW.
Wave 5 of the workspace-wide eth* naming purge.
pkg/db/models.go — Wallet + TreasuryWallet:
- New canonical KeccakAddress *string field with json `keccakAddress`
- EthAddress kept with json `ethAddress` as `// Deprecated:` alias
- NormalizeAddresses() method on both structs mirrors the two fields
via the package-private mirrorAddrPtrPair helper
pkg/api/server.go — KeygenResult (backend response):
- New canonical KeccakAddress string field with json `keccak_address`
- EthAddress kept with json `eth_address` as `// Deprecated:` alias
- NormalizeAddresses() method mirrors both fields
pkg/api/handlers_wallets.go, handlers_treasury.go, handlers_bridge.go,
handlers_mpc.go — all sites that constructed wallet rows from MPC
backend results now:
1. Call result.NormalizeAddresses() on the backend response so both
KeccakAddress / EthAddress carry the same value before copy
2. Copy both fields onto the persisted Wallet / TreasuryWallet row
3. Call wal.NormalizeAddresses() to ensure DB row has both
4. Read from the canonical KeccakAddress at response-construction
sites (toWalletResponse, treasury response, bridge response)
The "ethereum"/"lux"/"xrp" labels in addresses map (handleGetWalletAddresses)
are per-chain DISPLAY labels keyed by chain name a client knows;
the values are the Keccak-derived 20-byte address. Map keys NOT
decomplected here — they're per-chain UI labels, not internal naming.
Effect on the wire:
- API consumers reading wallet.ethAddress see the same value as
wallet.keccakAddress
- New consumers can switch to keccakAddress today
- ORM persistence stores both fields (eventually consistent via
NormalizeAddresses calls at handler boundaries)
Tests: pkg/api unit tests pass (1.8s); pkg/db has no test files.
Deprecation alias retention means downstream KMS / dashboards
keep working at every step of the migration.
Per CLAUDE.md x.x.x+1.
TestZapAuthInvalidSignature flipped one base64 char of the signature
segment. ECDSA accepts a wrong sig with probability ≈ 1/2^6 per
single-char flip (alphabet is 64), making the test ~1.5% flake rate
in CI.
Fix: replace the entire signature segment with all-A's. Still
decodable as base64url but cryptographically guaranteed-invalid
bytes — Verify rejection is now deterministic.
Verified: ran -count=10 ./pkg/zapauth/ — all pass.
Cross-org runs-on dispatch failure: hanzo-build-linux-amd64 scale set is
registered to github.com/hanzoai and silently rejects luxfi/* jobs.
Switch to lux-build (githubConfigUrl=github.com/luxfi, max 20).
Per SCALE_STANDARD.md §2 (~/work/hanzo/hips/docs/SCALE_STANDARD.md) —
every Go production Dockerfile that emits JSON to a client builds with
GOEXPERIMENT=jsonv2. Verified on hanzoai/zip's json_bench_test.go:
- Edge POST roundtrip: -12% time, -23% allocs
- Marshal-only: -22% time
- Unmarshal-only: -19% time, -25% allocs
GOEXPERIMENT is comma-separated, so jsonv2 composes cleanly with the
pre-existing runtimesecret experiment used by the MPC data plane.
- docker.yml: drop ubuntu-24.04-arm. Both arches now build on
hanzo-build-linux-amd64 via docker buildx + QEMU for the arm64 leg
(QEMU is acceptable for Docker builds per policy).
- release.yml: drop the imaginary hanzo-build-linux-arm64 runner. Both
arches cross-compile on hanzo-build-linux-amd64 with GOOS=linux
GOARCH=<arch> (CGO_ENABLED=0).
Same pattern as the threshold fhe + protocol zapdb fixes. Local-direct
hash differs from proxy.golang.org hash; CI uses proxy + sum.golang.org
and was failing on `GOWORK=off go mod download` with checksum mismatch.
Pin canonical proxy hash (h1:zuSrBtg... / h1:tFuuyXvI...).
Docker build was failing on `pnpm install` because corepack pulled the
latest pnpm (v11.x) which exits non-zero when build scripts (esbuild)
are present without explicit approval. Pin to v10.16.1 (stable, ships
the same `onlyBuiltDependencies` shape) and declare the esbuild build
script in package.json so future v11+ upgrades stay non-interactive.
Also swap `2>/dev/null || pnpm install` for `|| pnpm install --no-frozen-lockfile`
so we don't swallow real diagnostics — fall back to a controlled lockfile
update only when --frozen-lockfile fails.
Single LICENSING.md file referencing the canonical three-tier IP and
licensing strategy at github.com/luxfi/.github/blob/main/profile/README.md.
LICENSE file is unchanged; this only adds a navigational pointer.
Renames:
- LUX_ALLOW_FAKE_TFHE_FOR_TESTING_ONLY -> ALLOW_FAKE_TFHE_FOR_TESTING_ONLY
(test opt-in for the placeholder UNSAFE TFHE primitive)
- LUX_MPC_CONFIG -> MPC_CONFIG (docs/compose env)
- LUX_MPC_MODE -> MODE (Viper config key 'mode' override)
The brand prefix was noise; the env var names speak for themselves.
README/LLM/config docs updated to reflect that Viper has no env prefix
configured (AutomaticEnv reads uppercased config keys directly), so the
prior 'LUX_MPC_*' documentation was inaccurate.
Note: file format magic in pkg/kvstore/zap_backup.go remains
"LUX_MPC_BACKUP" — that's an on-disk format identifier, not an env var.
Renaming it would invalidate every existing backup file.
BREAKING: external callers/operators must update their environment
variable names. Per CLAUDE.md no-backwards-compatibility rule, the old
forms are removed.
Renames:
- LUX_ALLOW_FAKE_TFHE_FOR_TESTING_ONLY -> ALLOW_FAKE_TFHE_FOR_TESTING_ONLY
(test opt-in for the placeholder UNSAFE TFHE primitive)
- LUX_MPC_CONFIG -> MPC_CONFIG (docs/compose env)
- LUX_MPC_MODE -> MODE (Viper config key 'mode' override)
The brand prefix was noise; the env var names speak for themselves.
README/LLM/config docs updated to reflect that Viper has no env prefix
configured (AutomaticEnv reads uppercased config keys directly), so the
prior 'LUX_MPC_*' documentation was inaccurate.
Note: file format magic in pkg/kvstore/zap_backup.go remains
"LUX_MPC_BACKUP" — that's an on-disk format identifier, not an env var.
Renaming it would invalidate every existing backup file.
BREAKING: external callers/operators must update their environment
variable names. Per CLAUDE.md no-backwards-compatibility rule, the old
forms are removed.
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.