Both protocol adapters shipped a Verify that returned false unconditionally —
cggmp21 with "For now, return false", frost with "doesn't support ECDSA". That
is not a safe default dressed up as one. A verifier that always fails is
indistinguishable from a verifier that works and was handed a bad signature, so
a caller using it as a signing-path health check sees a permanent, unexplained
failure, and a caller treating "unsupported" as skippable gets no verification
at all.
Both now verify. FROST is Schnorr rather than ECDSA, but its group key is still
a secp256k1 point and *ecdsa.PublicKey is only how this package's interface
carries one, so the point is recovered and handed to the FROST verifier.
pkg/protocol/curvekey.go holds the conversion both adapters need, replacing two
hand-rolled decompressions (a Tonelli-Shanks square root in cggmp21, a separate
helper in frost). CurvePoint goes the other way, and it validates rather than
assumes: an (X, Y) that is not on the curve is REJECTED, not silently corrected.
Compression keeps only the parity of Y, so a conversion that trusts X and
parity alone will happily turn an inconsistent key into a valid point and then
verify signatures against a key the caller never named. X=1 is a real X
coordinate on secp256k1, which is how the test that found this was written.
X is left-padded with FillBytes; big.Int drops leading zeros, so about one key
in 256 would otherwise encode short and fail to convert at all.
Bumps luxfi/threshold to v1.12.5 for pkg/quorum.
Co-authored-by: Hanzo Dev <[email protected]>
mpcd implements only CGGMP21 (secp256k1 ECDSA), FROST (ed25519/ristretto), and
LSS signing sessions — there is no bls12-381 threshold signing session. The KMS
validator "bls" slot is secp256k1/CGGMP21; the "corona" slot is ed25519/FROST.
Neither is bls12-381 aggregation nor post-quantum. A "post-quantum" claim over
classical ed25519 is materially misleading.
- deployments/customer-3of5/topology.yaml: the `bls` entry asserted
curve=bls12-381, protocol=BLS-Threshold for "Lux consensus (validator
signing)". Corrected to secp256k1/CGGMP21 (what is actually custodied) with a
note that mpcd cannot threshold-sign bls12-381 and that Lux consensus BLS is a
native per-validator key, never custodied here. (Customer manifest — highest
priority.)
- pkg/api/handlers_kms.go: the ValidatorKeySet doc called the custodied key a
"Corona PQ-threshold key" — it is ed25519/FROST, not PQ and not the Corona
R-LWE lattice protocol. Corrected.
- README.md: added a "Validator key slots vs. engine protocols" note so the
secp256k1/ed25519 custody slots are not read as the bls12-381/lattice engine
protocols of the same name.
Labeling only — no crypto change. The live 2-of-3 mpcd ECDSA is sound.
Co-authored-by: Hanzo Dev <[email protected]>
The signing sessions emitted a raw, possibly high-S S with a recovery byte that
was then dropped at every serialization boundary. luxfi/evm ecrecover rejects a
high-S signature (EIP-2 "invalid sender"), and without a propagated recovery id
a caller must guess v (wrong guess => wrong sender). This is what broke the KMS
/sign output.
- signing_session.go + signing_session_lss.go: produce the signature through
the shared threshold helper s.signature.SigEthereum() (EIP-2 low-S + recovery
id in ONE place), then self-verify the recovery id against the wallet's own
public key via CalculateRecoveryByte and FAIL CLOSED on mismatch — never emit
a signature that binds to the wrong address.
- pkg/api SignResult: carry V (recovery id "0"/"1").
- cmd/mpcd TriggerSign: propagate V from the signing session's signature_recovery
and assemble the canonical 65-byte r‖s‖v so KMS /sign never returns an empty
signature.
- handlers_kms REST: include v in the response.
Requires the non-mutating, tested SigEthereum in threshold v1.12.4; bumps
luxfi/threshold v1.12.0 -> v1.12.4 (MVS pulls its transitive updates). Builds
GOWORK=off; pkg/api + pkg/mpc tests green.
Co-authored-by: Hanzo Dev <[email protected]>
The luxfi/{age,mpc,pq} tags were force-updated upstream, leaving the
recorded go.sum hashes pointing at pre-retag content. Refresh the
checksums at the SAME pinned versions (no version bumps) so the root
module and the e2e module build. All are GOPRIVATE-covered first-party
modules, so no sumdb authority is bypassed.
Unblocks: e2e/live_http_sign_test.go builds mpcd for the live 3-node
consensus ceremony.
Co-authored-by: Hanzo Dev <[email protected]>
docker buildx build --push emits a per-arch OCI index; docker manifest
create rejects those ('ghcr.io/luxfi/mpc:vX-amd64 is a manifest list').
Both arch images for v1.17.12 built+pushed fine — only the merge step
failed. Switch to 'docker buildx imagetools create' (same as build.yml)
which composes indices correctly.
Co-authored-by: Hanzo Dev <[email protected]>
A post-v1.17.9 commit stripped the BuildKit gh_token secret + GOPRIVATE=hsm
+ GOSUMDB=off + in-build go.sum regen from the Dockerfile, and the
--secret id=gh_token,env=GH_TOKEN (UNIVERSE_PAT) from docker.yml, on the
false premise that all luxfi/* modules are public. luxfi/hsm is PRIVATE
and absent from proxy.golang.org (all versions 404), so the alpine
builder's plain 'go mod download' fell back to unauthenticated direct git
-> exit 128. Every tag since (v1.17.10/11) failed here, independent of the
threshold + dep fixes.
Restore the v1.17.9 Dockerfile verbatim (authed direct git for private
first-party modules, proxy for public, regenerate-not-bypass go.sum) and
re-add the secret to docker.yml, preserving the ARC lux-build-amd64
routing. Combined with the coherent dep graph (geth v1.17.12), this is
the full green-build set for the CGGMP21 threshold fix.
Co-authored-by: Hanzo Dev <[email protected]>
The luxfi module ecosystem was re-tagged: luxfi/geth wiped its entire
v1.16.x line (v1.16.77, v1.16.98 gone) leaving only v1.17.12, and
luxfi/pq v1.0.3 was re-published. This broke `go mod download` for every
dependent pinning the old graph — including this repo, so v1.17.10 (the
threshold fix) failed to build even though its go.mod was untouched.
Move onto the coherent post-re-tag set: crypto v1.19.27 -> precompile
v0.5.60 -> geth v1.17.12 (all existing tags), regenerating go.sum. mpc
imports 0 geth and 0 precompile packages directly, so this is a pure
transitive-graph repair; threshold stays v1.12.0 (default build needs it;
the experimental_tfhe path is build-tag gated and never compiled in the
Docker image). Verified: go mod download clean, readonly build of
./cmd/mpc + ./cmd/mpcd compiles with no missing go.sum, threshold unit
test green.
Co-authored-by: Hanzo Dev <[email protected]>
SECURITY: every wallet keyed by these rings was a 1-of-n key — any single
node's share could produce a valid signature, with no threshold protection
against single-node compromise. This affects real value: the bridge-mpc
`bridge-oracle` (LUX↔ZOO corridor signer), zoo-treasury-v1, and the
hanzo-dao wallet were all keyed at CGGMP21 polynomial degree 0.
Root cause: two disconnected "threshold" values. The --threshold flag
(c.Int("threshold")) drove only the HasSigningQuorum health gate and status
display. The keygen DEGREE was read separately from viper key
"mpc_threshold" (pkg/eventconsumer → CreateKeyGenSession → cmp.Keygen), which
nothing ever set — and mpcd never calls config.InitViperConfig, so
viper.AutomaticEnv was off and even an MPC_THRESHOLD env would not have been
read. viper.GetInt("mpc_threshold") was therefore always 0 → degree 0 →
1-of-n.
Fix: bridge the flag to the keygen degree at consensus start via
viper.Set("mpc_threshold", keygenDegreeForThreshold(threshold)). --threshold
is the number of signers required (matches HasSigningQuorum); cmp.Keygen
needs degree+1 signers, so degree = threshold-1. --threshold=3 → degree 2 →
3-of-5; --threshold=2 → degree 1 → 2-of-3. keygenDegreeForThreshold is a
named, unit-tested function so the security-critical off-by-one is explicit.
Takes effect for NEW keygens only; existing 1-of-n wallets must be re-keyed
to gain threshold security (the live bridge-oracle needs a coordinated
migration — do not re-key in place).
Co-authored-by: Hanzo Dev <[email protected]>
Follow-up to #19 (emit 33-byte compressed keygen pubkey). pubKeyToEVMAddress's
case-33 assembled the EVM preimage as append(x.Bytes(), y.Bytes()...). big.Int.
Bytes() drops leading zero bytes, so a coordinate < 2^248 (X or Y with a high
zero byte, ~1/256 of keys) produced xyBytes<64 → the len!=64 guard returned an
EMPTY address for an otherwise-valid key. Now FillBytes zero-pads each coordinate
into a fixed 32-byte slice (case 32 already did this; case 33 did not — it was
latent until #19 made keygen actually emit 33-byte keys).
Verified: pubkey 03·005056e0… (X has a leading 00 byte) → old path xyBytes=63 →
empty; fixed path xyBytes=64 → 0x7242EFb11FB1A669486F10649C9B73739731a5e6, byte-
identical to luxfi/crypto DecompressPubkey+PubkeyToAddress.
Co-authored-by: hanzo-dev <[email protected]>
The CGGMP21 keygen session serialized the group public key as the bare 32-byte
X-coordinate (PublicPoint().XScalar()), dropping the Y parity. pubKeyToEVMAddress
and pubKeyToBtcAddress then GUESS even-Y (0x02 prefix) when handed a 32-byte key,
so for every ODD-Y key (~50%) they derive the WRONG address — the sibling curve
point — which does NOT match the key the ring actually signs with. A threshold
signature over a digest then recovers to a different address than keygen reported,
making the wallet unusable (funds sent to the reported address are unspendable).
Fix: serialize PublicPoint().MarshalBinary() (33-byte compressed, 0x02/0x03 || X)
— exactly what signing_session_lss.go already uses for recovery — so keygen's
address == the signing key's address. pubKeyToEVMAddress (case 33),
pubKeyToBtcAddress (case 33), and CalculateRecoveryByte all already accept the
33-byte compressed form; the 32-byte path stays only as a legacy fallback.
Root-caused live: direct ring keygen returned evm_address 0x84e0…f894 for
ecdsa_pub_key X=5b0e9ad0…; a direct threshold sign recovered pubkey 03·5b0e9ad0…
(odd Y) → real address 0x10B8…15A9. The +1 parity bit is the whole bug.
Co-authored-by: hanzo-dev <[email protected]>
go.mod requires go 1.26.4 (and cc v0.3.0 requires >= 1.26.4). The alpine
golang image pins GOTOOLCHAIN=local, so 1.26.3-alpine refuses to build:
'go.mod requires go >= 1.26.4 (running go 1.26.3; GOTOOLCHAIN=local)'.
Match the base image to the go directive — hermetic, no toolchain download.
Two fixes that make the clean-proxy Docker image build of main succeed:
1. luxfi/cc is now a PUBLISHED public module (immutable proxy+sumdb).
Pin v0.3.0 (converged 6-vendor attestation verifier) and drop the local
'=> ../cc' replace so the Docker 'COPY . .' context (no ../cc) resolves
cc from the proxy. Also unblocks luxfi/dkg (dealerless threshold dep),
now public.
2. go.sum had two poisoned zip hashes (luxfi/age v1.4.0, luxfi/pq v1.0.3)
recorded from re-tagged 'direct' GitHub instead of the immutable
proxy+sumdb — the clean-proxy Dockerfile rejected them (SECURITY ERROR).
Restored the authentic sum.golang.org values.
Verified (fresh modcache, immutable proxy+sumdb, GOWORK=off, default tags):
go build ./... = 0, go test ./cmd/... ./pkg/... = 33 ok.
(2 BLS-dispatcher + 1 CAS-race failures pre-exist on v1.17.0 from the
threshold v1.12.0 dealerless bump; unrelated to this dep change —
confirmed identical failure with cc reverted to ../cc.)
luxfi/* and hanzoai/* are PUBLIC Go modules. The GOPRIVATE / GONOSUMDB /
GOPROXY=direct env in Dockerfile + release.yml routed fetches around
proxy.golang.org + sum.golang.org, "working" only by skipping checksum
verification — which poisons go.sum with force-republished git bytes that no
longer match the immutable checksum db. Drop the bypass; resolve every
luxfi/hanzoai checksum from the public proxy + sumdb instead.
Poisoned deps resolved (strip luxfi/hanzoai go.sum lines, then go mod tidy):
- github.com/luxfi/edwards25519 v0.1.0 -> v0.1.1 (the agl/ed25519 replace
target, in both go.mod and e2e/go.mod): v0.1.0 was force-republished, its zip
hash diverges from the sum.golang.org pin and no copy verifies; v0.1.1 is the
clean patch the proxy + sumdb agree on.
- github.com/luxfi/age v1.4.0, github.com/luxfi/zap v0.8.1: version healthy, but
go.sum carried poisoned zip hashes — corrected to the sumdb canonical values.
- e2e/go.sum: also completed to the full canonical checksum set (hack-era builds
with GOPROXY=direct skip go.sum completion).
Verified with GOWORK=off (matching the .dockerignore'd workspace + CI):
- go mod verify -> all modules verified (main + e2e)
- CGO_ENABLED=0 go build ./... clean (main + e2e)
- go test ./... -> 35 ok / 0 FAIL; signing, threshold, keygen, attestation, kms,
ristretto255 curve, sr25519, and policy-gated sign-flow tests all pass. No
crypto regression from the edwards25519 curve-lib bump.
luxfi/* and hanzoai/* Go modules are public; GONOSUMDB/GOPRIVATE routed them
direct and bypassed sum.golang.org, so a force-republished tag could poison the
build. Resolve via the public proxy+sumdb (canonical immutable hashes).
Consolidate the hanzoai/mpc repo into luxfi/mpc (the canonical, deployed MPC —
v1.16.0, running as ghcr.io/luxfi/mpc across all clusters). hanzoai/mpc (legacy
v1.0.6) carried 5 chain-specific threshold-signing examples luxfi/mpc lacked;
they already import github.com/luxfi/mpc/pkg/threshold, so this is a clean port.
hanzoai/mpc is being archived — nothing imports it and the deployments already
run luxfi/mpc.
The canonical attestation verifier (cc/attest + pkg/attestation/nvidia) was
extracted into the orthogonal leaf module github.com/luxfi/cc so mpc, the TEE
custody extension, and the confidential-AI system can share ONE verifier
coupled to none of their domains.
- pkg/attestation/composite.go, pkg/kms/release.go: import the leaf
github.com/luxfi/cc/attest directly; map the NVIDIA issuer to the real
KindNVTrust (was the KindNRAS stub).
- pkg/attestation/nvidia + cc/attest/{sev,tdx,verifier,...}.go: moved to the
leaf (clean stdlib/go-sev-guest leaves, zero mpc-internal imports).
- cc/attest now a thin TRANSITION ADAPTER: a zero-logic re-export of the leaf
(type aliases ⇒ identical types, so the kms.CompositeAttestation release
gate is satisfied from either import path) plus the deprecated KindNRAS
symbol that pinned github.com/luxfi/threshold v1.9.9 still references. To be
removed once threshold republishes against the leaf (its main already has)
and mpc's threshold/protocols/tfhe dep is resolved.
go.mod: require github.com/luxfi/cc (local replace ../cc until tagged);
threshold stays v1.9.9 (cannot bump: main drops protocols/tfhe that pkg/mpc
needs). go vet ./... clean; attestation/kms/scheduler tests green.
The embedded threshold dispatcher's Sign_Ctx_Profile consults a
thresholdd.ChainProfileResolver before allowing the single-party dealer
shortcut on ctx-bound signing (pulsar/magnetar sign_ctx). mpcd built the
ZapServer with NO resolver, so the gate fell to its documented fail-OPEN
branch — every chainID, strict-PQ chains included, slipped the gate.
Wire the node's chain-profile posture into the dispatcher BEFORE Start:
- --chain-profile (MPC_CHAIN_PROFILE), default strict-pq → fail-closed.
Any non-empty chainID not allow-listed resolves to ProfileStrictPQ and
the dealer shortcut is REFUSED.
- --legacy-chains (MPC_LEGACY_CHAINS): explicit allow-list of chainIDs
permitted the shortcut (dev / non-PQ chains co-served by a strict node).
Resolver is the canonical thresholdd.NewStaticChainProfileResolver — no
bespoke type, no luxfi/consensus/config dependency.
Residual: thresholdd.RefuseUnderStrictPQ short-circuits empty chainID to
PASS before consulting the resolver; that short-circuit lives inside the
threshold gate (threshold owns the listener, read-only here). The resolver
wired here is already fail-closed for "" (ResolveChainProfile("") ==
ProfileStrictPQ), so the residual closes the instant the threshold gate
routes "" through the resolver — a one-line change in
threshold/pkg/thresholdd/profile.go (drop `|| chainID == ""`).
Tests: TestStrictPQGate_FailClosed / _LegacyDefaultPermissive drive the
real gate proving refuse-by-default + allow-list opt-out + empty/unknown
fail-closed at the resolver boundary.
Also drops the stale "bls" scheme from the dispatcher start log (BLS
custody was ripped in H-2).
Dep-checksum tail left uncommitted after 5c4d5cb removed bls_keygen_session.go.
- main go.sum: luxfi/zap v0.8.1 + luxfi/age v1.4.0 h1 refresh (upstream same-version
tag re-cut; go mod verify = all modules verified, build green).
- e2e/go.mod + e2e/go.sum: go mod tidy churn (nats.go v1.50.0 -> v1.52.0 within v1,
added indirects) for the e2e test module.
- go.work.sum: workspace checksum sync.
GOWORK=off go build ./... exit 0; go mod verify clean.
BLS is consensus-only. The mpcd validator-key custody path generated a
threshold-BLS group key via a TRUSTED DEALER (blsThreshold.TrustedDealer:
one node, the lowest sorted party ID, generated the entire group secret
key and all shares, then distributed them) — a single point of full-key
compromise, with no DKG and no rogue-key PoP. That violates the
public/leaderless/permissionless invariant "no single node can sign or
extract a key".
Orphan-safe: this path was incomplete (no proof/sign-off doc unlike
cmp/frost; no deployment manifest provisions validator-BLS; BLS keygen
had no dispatch caller) and NOT live custody. A validator's real BLS
consensus key is its own NATIVE per-validator crypto/bls key (PoP +
aggregation, the consensus fast-path), held at the node — never
threshold-custodied here. Corona PQ-threshold custody (dealerless DKG)
stays. So a validator keyset = {native BLS (consensus, no custody) +
corona PQ-threshold (custodied)}.
Delete: bls_keygen_session.go, bls_signing_session.go, bls_config_marshal.go,
bls_session_test.go, signing_handler_bls.go. Remove KeyTypeBLS (mpc + types),
SessionTypeBLS, CreateBLS{KeyGen,Sign}Session, the BLS signing/keytype
dispatch cases, the keygen-result BLSPubKey field, and the bls: key-share
prefix. handlers_kms.go: drop the BLS-custody half of ValidatorKeySet and
the generate/sign/rotate BLS branches; KMS custodies corona only.
Verified: GOWORK=off go build ./... + go vet green; pkg/mpc, pkg/api,
pkg/types tests pass. Zero residual refs to deleted BLS symbols.
Consensus mode learned only its OWN Ed25519 public key: peer keys arrive in
ReadySignal.PublicKey during the transport identity handshake (sendIdentity) but
were discarded — never fed to the ConsensusIdentityStore that VerifyWireMessage
consults. Result: every inbound CGGMP21/FROST/signing wire message from a peer
was dropped ("public key not found for node …") and keygen/signing stalled to a
60s timeout. The HTTP /keygen and /sign endpoints could not work in any real
multi-node ensemble.
Fix (orthogonal, no import cycle): transport.Config.OnPeerIdentity observer is
invoked (notifyPeerIdentity, with self/empty/length guards + defensive copy)
whenever a peer key is learned — from the MsgMPCReady handshake in handleMessage
and handleIncoming. The acceptor now also replies with its own identity so the
key exchange is symmetric on the single tie-broken connection. main.go wires the
observer to ConsensusIdentityStore.AddPeerPublicKey. This is the consensus-mode
analog of the legacy NATS path's peers.json + *_identity.json seeding.
Proof: e2e/live_http_sign_test.go (//go:build integration) builds mpcd from the
repo root, starts a real 3-node 2-of-3 consensus ensemble, and over the live
HTTP API proves: distributed CGGMP21 keygen (real evm address), a real threshold
signature, idempotency cached-match, anti-oracle 409 on key reuse with new
content, 400 on empty key, and no plaintext key in any response or node log.
Unit coverage for the observer guards in pkg/transport/peer_identity_test.go.
Also fixes a pre-existing standalone_test.go bug: it set only ZAPDB_PASSWORD but
the env HSM provider reads MPC_PASSWORD — set both so the node starts.
Bump e2e submodule's luxfi/threshold pin (v1.5.5 → v1.9.9) to drop the only
remaining corona puller in this workspace. Removes the residual
'github.com/luxfi/corona v0.2.0/go.mod' hash line from go.work.sum.
Align go.work go directive to 1.26.4 to match both workspace siblings
(../threshold and ./e2e). No source code in this repo imported corona;
this was workspace-graph hygiene only. R-LWE threshold primitive is now
referenced exclusively via luxfi/corona upstream.
The Corona-rename WIP bumped cmd/mpcd to the luxfi/threshold thresholdd
NewZapServer/ZapServerConfig API but left go.mod pinned at the predating
v1.8.2, leaving the module unbuildable. Forward the dep set within v1.x:
luxfi/threshold v1.8.2 → v1.9.9 (thresholdd.NewZapServer surface)
luxfi/corona v0.4.1 → v0.7.6 (pulled by threshold)
luxfi/zap v0.2.0 → v0.8.1 (threshold v1.9.9 uses zap.NodeConfig.TLS)
luxfi/lattice/v7 v7.1.0 → v7.1.4
go build ./... is green again; pkg/webauthn tests pass.
lux OSS names no PAD-2 / liveness vendor. Decomplect the verifier into a
pluggable extension point so a white-label deployment (e.g. liquidity)
registers its own provider without forking:
- pkg/webauthn: add Verifier interface + Binding (vendor-neutral per-call
input) + Ed25519EnvelopeVerifier (OSS default, delegates to the audited
VerifyLiveness) + RegisterVerifier/LookupVerifier registry. Tests cover
the default verifier, the registry, and a custom non-Ed25519 scheme.
- pkg/api: resolve the active Verifier at boot from MPC_LIVENESS_PROVIDER_ID
(registered custom wins; else Ed25519 from MPC_LIVENESS_PUBKEY_ED25519;
else nil → enroll fails closed 503). handleBiometricEnroll calls
s.livenessVerifier.Verify(...) — no vendor code in lux.
- Rename MPC__* env vars → MPC_LIVENESS_*; drop every
identifier/comment across code + LLM.md. Zero /onyxd in lux/mpc.
Also finish the half-done decomplect from a326cc6 'rip zapauth gate' that
left pkg/api/zap_kms_server.go calling a removed checkAuth and missing the
encoding/json import — restore the import, drop the orphaned guard.
(Pre-existing, unrelated: cmd/mpcd still references thresholdd.NewZapServer
from the same in-flight WIP — not touched here.)
Replaces non-canonical scale-set / org-prefixed labels with the
existing labels every arcd host registers with. Matches evo for
amd64 and spark for arm64. No new labels added.
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.