Commit Graph
112 Commits
Author SHA1 Message Date
zeekay cb77fab979 Ed25519 keygen is real: sol_address comes from KeygenEd25519 and nothing else
luxfi/threshold v1.12.7 added FROST(Ed25519, SHA-512), so Solana and TON finally
have a signable curve. The sessions named "EdDSA" were calling KeygenTaproot —
BIP-340 over secp256k1 — whose key is also 32 bytes, sails through the
len(EDDSAPubKey)==32 gate, and base58-encodes into a real-looking Solana address
the ring can never sign for.

⚠ A LIVE WRONG-ADDRESS BUG went with it: handlers_wallets served
addresses["ton"] = *wallet.SolAddress. A TON address is not a base58 pubkey — it
is workchain plus a hash of the wallet contract's StateInit, base64url with a
CRC16 — so the API was handing out an address that decodes to nothing. Deleted
rather than guessed; TON returns when its encoding is real.

The keygen tests used to call frost.KeygenEd25519 directly, which validated the
LIBRARY and not the daemon: reintroducing KeygenTaproot left every package
green. They now go through the production session, and so does a test that
constructs the real signing session and proves the caller's bytes reach the
protocol unhashed. Both original bugs reintroduced together fail 11 tests.

secp256k1 keygen changes from fail-open to fail-closed, and that is the one
behavioural change to the existing EVM/BTC path. An empty pubkey previously
meant any of nil config, marshal failure, kvstore.Put failure or keyinfo-save
failure — and was returned as SUCCESS, announcing addresses while fewer than
t+1 nodes hold a share. Applying fail-closed to Solana while leaving the
higher-value leg fail-open is incoherent.

Also: the KMS share classifier still keyed off the retired "frost:" prefix; the
CGGMP21 session published its own local failure onto the same first-wins subject
as the ring's verdict, so one node's failure could beat the ring's success; and
one 120s deadline shared across two sequential ceremonies starved the cheap leg.

NOT YET SPENDABLE. ConsensusKeyInfoStore.Save is the only RegisterKey caller and
hardcodes "secp256k1", and TriggerSign reads the curve from that field, so no
product entry point can reach Ed25519 signing. The address is correct; signing
it needs a wire-contract change shared with lux/wallet.
2026-08-06 13:23:18 -07:00
zeekay cbbecd063d a peer is named by its address, so every node agrees who is in the room
Keygen reached round 2 and every node refused every message, including from
legitimate peers, so each attempt sat until the 300s timeout and every crypto
deposit answered 503 "the custody service is not accepting new addresses".

The handler admits a message only from an id in its OWN party set, and the
fleet had built three sets that disagreed:

  partyIDs=["mpc-node-0:keygen:1","mpc-node-1:keygen:1","peer-2:keygen:1"]

while node-2 called itself mpc-node-2:keygen:1. Nobody answers to "peer-2", so
its share never arrived and the ceremony could not complete.

The synthetic name is what an unprefixed --peer produces: the deployment passes
addresses only, self is dropped by address, and everything left took a
POSITIONAL id. The comment above that line already said such an id "cannot
match a real node id by construction" — it was describing this bug without
following it through.

So a peer is now named the same way self is recognised: the first DNS label,
which IS the node id under StatefulSet naming
(mpc-node-2.mpc-node-headless.ns.svc.cluster.local -> mpc-node-2). Deriving both
sides by one rule is what makes the sets agree.

A bare IP has no label to read and keeps the positional id — the caller must
then use nodeID@host:port, which states the name outright. The test asserts the
invariant that was violated rather than the helper's return value: the id a node
derives for a peer must equal the id that peer uses for itself.
2026-08-05 16:36:04 -07:00
zeekayandHanzo Dev 5eaf0304b0 mpcd: a node is not its own peer, and too many ready is not too few
KEYGEN has been dead on the hanzo-mpc fleet — every request answered
503 "peers not ready" while /healthz reported `signing_quorum: true`
beside `ready: false`, and the counters said `ready=5 expected=4` in a
THREE node fleet. Both halves of that arithmetic were wrong.

SELF WAS COUNTED TWICE. A StatefulSet renders one command for every
replica, so the natural spelling hands each node the whole ensemble —
including itself. `peerMap[nodeID] = listenAddr` adds self by id, and the
matching `--peer` then adds it AGAIN under a synthetic `peer-N` id (an
unprefixed address gets a positional id by construction, so it can never
match a real node id, and filterSelf — which compares ids — could never
see it). Three nodes therefore reported four expected members. mpcd now
drops a --peer that names this node, matched by address: equal to
--listen, or a host whose FIRST DNS LABEL equals the node id (compared
whole, so mpc-node-10 is never mpc-node-1).

AND THE GATE WAS AN EQUALITY. `readyCount == len(peers)+1` reads an
OVERSHOOT as not-ready, so once the count passed the expected total
nothing could bring it back — permanently, across restarts. Signing
survived only because it already used >=. A readiness gate must fail
closed on too few, never on too many; both sites are >= now, so the
double-count could not have wedged keygen even before the fix above.

Verified by exercising isSelfAddr over the fleet's real addresses: the
live 3-node template on mpc-node-0 keeps 2 peers, expected total 3.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-04 16:39:45 -07:00
zeekayandHanzo Dev e9d5915722 style: goimports all 16 unformatted files, not just the one CI named
golangci-lint failed the lint job on

    pkg/wallet/usage.go:53:1: File is not properly formatted (goimports)

goimports over pkg/, cmd/ and internal/ finds SIXTEEN such files. The linter
reports the first and stops, so fixing only the named one buys exactly one more
run before it names pkg/api/server.go, and so on for sixteen round trips.

All of them are alignment-only: gofmt aligns a var block's `=` to its longest
name, and these blocks had been edited without re-aligning. No import was added,
removed or reordered.

The dashboard/ work in this tree is untouched; all sixteen were clean.

    goimports -l ./pkg ./cmd ./internal   16 -> 0
    go build ./...                        OK
    go vet ./pkg/... ./cmd/...            OK
    go test wallet/policy/registry/transport/zap   all ok

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-02 10:14:42 -07:00
zeekayandHanzo Dev b30bb58a22 test(mpcd): point the dispatcher round-trip at a scheme thresholdd still serves
TestEmbeddedDispatcher_BLSRoundTrip and _AuthGate drove "bls". thresholdd
stopped serving that scheme — trusted-dealer threshold-BLS custody was ripped
for security (H-2), and luxfi/threshold v1.12.5's dispatcher exposes exactly
cggmp21, frost, pulsar, corona, magnetar and doerner (pkg/thresholdd/zap_schema.go;
there is no bls.go). So both tests had been failing on every run:

    bls.keygen: zapclient: unknown procedure "bls.keygen"
    wrong-id: error zapclient: unknown procedure "bls.keygen" does not name unauthorized

The second line is the damaging one. _AuthGate asserts a wrong NodeID is refused
with "unauthorized"; it was instead getting "unknown procedure" back from a
scheme lookup that failed before auth was ever consulted. The auth gate has not
actually been exercised for as long as the scheme has been gone.

FROST is a live production scheme on the same ZAP wire and keygens as fast, so
the calls move to it and every assertion is kept verbatim — pubkey non-empty,
3 shares, non-empty signature, verify true, forgery rejected, wrong NodeID
refused with "unauthorized". Nothing is skipped, softened or dropped; the
round-trip now lands on a surface that exists. _UnknownScheme still drives
"bogus" and still proves the unknown-procedure contract.

Renamed to TestEmbeddedDispatcher_FROSTRoundTrip so the name states what it runs.

go test ./... is green across the module.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-27 12:25:56 -07:00
zeekayandHanzo Dev 72a2ae1371 mpcd: report the threshold and party set keygen actually produced
api.KeygenResult carried only wallet_id, pubkeys and addresses — no threshold,
no participants. Keygen accepts no per-request threshold (the ring's own
--threshold governs), so a client had no way to learn what it actually got.

That is not theoretical. luxfi/kms pkg/keys/manager.go validates req.Threshold
(>=2, parties >= threshold), never sends it, then stores
`Threshold: blsResult.Threshold, Parties: len(blsResult.Participants)`. Its
client struct already declares both json fields, so with mpcd never emitting
them every validator key set on record reads Threshold:0 Parties:0 while the
caller had asked for 3-of-5 and passed validation. Ask for 3-of-5, silently get
the ring's config, and record zero.

Populate both from what the node is actually configured for: b.threshold, and a
new ConfiguredParticipants() (peers + self, sorted). Configured — not
GetReadyPeersIncludeSelf — because readiness answers "who is up right now",
while a key's party set is a property of the key and must not change because a
pod restarted; sorted so every node reports an identical list for the same key.

Additive and backward compatible: both fields are omitempty, and the KMS client
already decodes them. Clients can now compare against what they requested and
fail closed.

Tests: ConfiguredParticipants covers ring sizes, order normalization,
cross-node determinism, and a peerIDs aliasing guard.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-25 16:38:17 -07:00
zeekayandHanzo Dev b19863787b fix(mpcd): canonical EVM-ready threshold-ECDSA signatures (low-S + recovery id)
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]>
2026-07-21 08:32:35 -07:00
zeekayandHanzo Dev 6cf38d2e48 fix(mpcd): wire --threshold into CGGMP21 keygen degree (was silently 1-of-n)
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]>
2026-07-14 15:15:28 -07:00
91cca33dba fix(address): left-pad compressed-pubkey coords in pubKeyToEVMAddress (case 33) (#20)
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]>
2026-07-07 21:54:53 -07:00
zeekay 4508cb3f2a security(mpcd): wire fail-closed strict-PQ chain-profile gate — Red M-2
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).
2026-06-26 21:38:59 -07:00
zeekay 255faab1cd fix(transport): propagate peer Ed25519 keys to consensus identity store
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.
2026-06-26 21:18:02 -07:00
zeekay 133131d943 feat(mpcd): HTTP POST /sign endpoint (idempotent, t-of-n quorum-gated) 2026-06-26 21:18:02 -07:00
zeekay 184193bae6 scrub branding + corona from docs/comments/tests
- Genericize all -brand references in comments, LLM.md, and
  FIREBLOCKS_PARITY.md: ' MPC API'→'MPC API', drop
  ~/work/liquidity/openapi/mpc.yaml paths, 'Liquid operator'→'the operator',
  'liquid-mpc'→'the mpc cluster'.
- Genericize tenant fixtures/examples: liquidity→acme, mlc→globex across
  treasury_test.go + db model comments (cross-tenant isolation tests keep
  their semantics with neutral org names).
- Rename corona→corona in docs (active Go code had none).
lux OSS names no white-label tenant. Build + api/webauthn tests green.
2026-06-10 22:55:37 -07:00
zeekay a326cc6418 mpc: rip zapauth bearer-token gate (decomplect)
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).
2026-06-06 21:06:17 -07:00
zeekay ba4995dcf5 WIP: session checkpoint 2026-06-05 (codec rip in flight) 2026-06-05 15:33:58 -07:00
zeekay da277ae96a fix: gofmt -s across repo (CI format check) 2026-06-02 11:39:20 -07:00
zeekayandGitHub b25d9422d0 build(mpc): GOEXPERIMENT=jsonv2 alongside runtimesecret per SCALE_STANDARD (#13)
* 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.
2026-05-29 21:05:40 -07:00
zeekay f11c155be5 mpc: drop Corona identifiers — wire API renamed to Corona
The two MPC files that still carried Corona names:
  cmd/mpcd/main.go        — comment ref to luxfi/corona
  pkg/api/handlers_kms.go — ValidatorKeySet wire schema

handlers_kms.go renames:
  ValidatorKeySet.CoronaWalletID   → CoronaWalletID
  ValidatorKeySet.CoronaPublicKey  → CoronaPublicKey
  JSON tags coronaWalletId/PubKey  → coronaWalletId/PubKey
  local `coronaWalletID` and the per-validator suffix
  "validator-<id>-corona"          → "validator-<id>-corona"
  router switch `case "corona":`   → `case "corona":`

cmd/mpcd/main.go: comment "luxfi/corona and luxfi/corona" 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.
2026-05-14 09:34:28 -07:00
zeekay 3cfea562f9 mpcd: gate embedded threshold dispatcher with bearer-token + loopback bind
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).
2026-05-13 16:46:22 -07:00
zeekay 7bcad9343d mpcd: embed luxfi/threshold/pkg/thresholdd dispatcher (six schemes)
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 / Corona, via luxfi/corona/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".
2026-05-13 15:56:56 -07:00
zeekay d40098c959 feat(zapauth): JWKS-bearer gate on KMS-facing ZAP server (LP-103)
New pkg/zapauth verifies a JWT presented in OpAuthHello (0x00EF)
BEFORE the X25519+ML-KEM-768 hybrid handshake. Per-peer claims are
attached by ZAP peer ID and consulted on every opcode dispatch.

Backwards compatible: defaults to auth=off so v1.13.x clients keep
working. ZAP_AUTH_REQUIRED=true rejects unauthenticated peers; the
flip happens after every KMS deploys with bearer minting (luxfi/kms
v1.9.0 pkg/iamclient).

- pkg/zapauth/{verifier,verifier_test}.go — Verifier, JWKS cache,
  AuthHello frame, stdlib-only signature verification (RS/ES/EdDSA).
- pkg/api/zap_kms_server.go — KMSZapConfig, StartKMSZAPWith, per-op
  checkAuth gate, OpAuthHello handler.
- pkg/api/zap_kms_server_auth_test.go — required+advisory+missing+bad
  token + nil-verifier integration tests against a self-contained
  ES256 JWKS endpoint.
- cmd/mpcd/main.go — wires ZAP_JWKS_URL / ZAP_EXPECTED_ISS /
  ZAP_EXPECTED_AUDIENCES / ZAP_AUTH_REQUIRED env vars.
- docs/zap-auth.md, LLM.md — protocol reference + LLM map entry.

Tests: pkg/zapauth (13/13), pkg/api KMSZAP (10/10 — 5 original +
5 new auth integration), pkg/zap (existing untouched).
2026-05-02 09:30:48 -07:00
zeekay c5c266cb5a fix(mpcd): default --kms-zap-listen to :9653 to match operator zapPort
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.
2026-05-01 01:36:46 -07:00
zeekay aa697bfc10 feat(mpcd): expose ZAP server on :9970 for KMS handshake
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).
2026-05-01 01:35:13 -07:00
zeekay c6eb3ae230 test(mpcd): standalone 1-of-1 integration smoke + cmd/mpcd/README
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.
2026-04-28 13:42:36 -07:00
zeekay 8c985309ec mpcd: author missing buildHSMSignerConfig helper (refs #227)
Commit 3bb399c ("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 3bb399c 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.
2026-04-28 01:14:50 -07:00
zeekay c6098b226d Merge kms-nonce-bind-2026-04-28
# Conflicts:
#	CHANGELOG.md
2026-04-28 00:58:33 -07:00
zeekay fdc21dc925 neutral: drop 'Lux'/'Hanzo' brand prefixes in user-facing strings
MPC and KMS are protocols, not products. The Go module path
github.com/luxfi/mpc remains canonical (location), but user-facing
strings (--help, README, comments, banners) are brand-neutral.
'Lux MPC' → 'MPC', 'Lux KMS' → 'KMS', 'Hanzo MPC' → 'MPC'.
2026-04-25 21:46:16 -07:00
zeekay 6b3567eeb4 feat(zap): adopt canonical port 9999 across MPC + transport
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.
2026-04-25 21:18:39 -07:00
zeekay 631cbbabc5 fix(mpc): HasSigningQuorum uses operator-facing threshold semantics
Follow-up to e08dba1. 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).
2026-04-20 18:52:49 -07:00
zeekay e08dba1faa fix(mpc): /healthz reports quorum — not all-peers-ready (P0-1)
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.
2026-04-20 18:48:31 -07:00
zeekay ed17f62c6a feat(ui): embedded MPC admin dashboard — @hanzo/gui + go:embed
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.
2026-03-16 10:00:00 -07:00
zeekay 13112c10dd fix: remove postgres/valkey/legacy mode — consensus+ZapDB only
- Removed --mode, --api-db, --api-kv, --db flags
- Removed legacy NATS/Consul transport (runNode, ~200 lines)
- Removed standalone API server (runAPIOnly, ~200 lines)
- Removed Consul helpers, NATS connection, interactive prompts
- Dashboard now uses ZapDB (SQLite) instead of postgres
- pkg/db: removed postgres backend, removed valkey client
- Consensus mode is the only mode. Zero external deps.
2026-04-13 04:14:40 -07:00
zeekay ccc78cbfd9 fix: /healthz only — removed legacy health endpoints 2026-04-09 18:31:27 -07:00
zeekay 729e6db3ff feat: add /healthz endpoint (platform health check standard)
Add /healthz alias on the internal MPC API (port 9800) alongside
the existing /health endpoint. Both return the same response.
2026-04-09 18:04:32 -07:00
zeekay 1bb8e1b585 fix: address red team findings — auth, TLS, KMS fallback, AAD
M-1:  compose.yml commented section now enforces JWT_SECRET required syntax
M-3:  TLS client config uses TOFU certificate pinning instead of blind
      InsecureSkipVerify — SHA-256 fingerprint pinned on first connect,
      mismatches rejected on subsequent connections
M-4:  TLSOnlyListener plaintext escape hatch gated behind both
      ALLOW_PLAINTEXT=true AND ENVIRONMENT=development
M-5:  KMS fallback to unencrypted storage is now log.Fatal in production
      (ENVIRONMENT != "development"), including Put-time failures
M-6:  isKeyShare() now checks org: prefix, key-type prefixes (frost:,
      bls:, sr25519:, tfhe:), and hex wallet IDs instead of returning true
M-9:  AES-GCM Encrypt/Decrypt accept optional AAD variadic parameter
      for context-binding; existing callers remain backward compatible
M-12: Internal /keygen endpoint requires org_id in request body (400 if
      missing); orgID propagated to GenerateKeyMessage and audit log
2026-04-07 14:06:10 -07:00
zeekay f581b655fe security: harden internal MPC API, enforce TLS-only transport, restrict egress
- 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.
2026-04-07 14:00:47 -07:00
zeekay 4b8f108cf9 feat: Base framework + consensus default + minimal ops
- Mount chi handlers on Hanzo Base (lifecycle, health, auth framework)
- Default mode: consensus (ZAP p2p) — NATS/Consul deprecated
- compose.yml: 2 services (api + dashboard), SQLite, no external DB
- config.yaml: consensus mode, SQLite-first
- Makefile: simplified build targets, docker compose format
- K8s: kustomization.yaml for idempotent deploys
- Removed listenAddr from NewServer (Base handles listening)
- Legacy NATS/Consul preserved but not default
2026-04-07 01:18:53 -07:00
zeekay b0c3737f74 fix: remove /api/ prefix from all routes — use /v1/ only
Subdomain routing (mpc.lux.network/v1/) not path prefix (/api/v1/).
Updated: server routes, KMS routes, main.go forwarding, compose,
K8s dashboard config, ingressroute, landing page HTML.
2026-04-07 00:01:47 -07:00
zeekay 08f01d8eda security: fix 15 custody audit findings, add regression + fuzz tests
Fixes all findings from the Blue Team defensive security review of the
 →  custody architecture transition.

HIGH:
- S-1: Implement P-256 ECDH + AES-256-GCM encryption of passkey backup shards
- W-2: Reject unparseable WebAuthn public keys (was silently skipping verification)
- O-1: Add Ed25519 binary signature verification package (pkg/integrity)

MEDIUM:
- W-1: Validate attestation is a real P-256 curve point, not raw attestation object
- W-3: Add origin validation to all WebAuthn flows (register, verify, trade approve)
- N-2: Remove plaintext TCP fallback from MPC node transport (TLS mandatory)
- K-1: Fail-closed on empty/whitespace orgID (prevents cross-tenant key leakage)
- A-1: Make AuditEntry immutable (Update/Delete return error)
- A-2: Add MPC-level signing audit log (wallet, message hash, intent, trigger)
- O-3: Invalidate backup shards after key reshare (stale shares are useless)
- O-2: Reject env/file password providers in production (require cloud HSM)

LOW:
- T-1: Add error handling + MarkSignFailed to signing goroutine
- T-2: Verify caller authorization in handleTradeSubmit
- K-2: Secret erasure stub now calls runtime.GC() instead of no-op
- N-1: Add VerifyConnection callback to reject peers without TLS certs

Tests: 69 unit + 7 fuzz targets, ~900K fuzz executions, 19/19 packages green.
Makefile: add -tags json1 for SQLite JSON function support in ORM tests.
2026-04-01 09:00:06 -07:00
zeekay f390b97876 fix: per-org MPC isolation — orgID end-to-end threading + security hardening
- M-1 CRITICAL: Remove legacy key share fallback (cross-tenant leak)
- M-2 HIGH: orgID now required string on all Node methods (not variadic)
- M-3 HIGH: Key material zeroing after signing sessions
- Thread orgID through MPCBackend interface, all API handlers
- Reshare + TFHE sessions use OrgScopedKey for reads/writes
- Remove hardcoded JWT default (fail-hard if not set)
- Add warning log for empty orgID legacy path
- Regression tests: TestGetKeyShareWithFallback_M1_CrossTenantLeak
2026-03-24 16:55:54 -07:00
zeekay f711f5ec13 fix: goimports formatting with local-prefixes (matches golangci-lint) 2026-03-22 13:17:50 -07:00
zeekay 72b680d0ae security: add orgID multi-tenancy isolation, rename badger to zapdb
Multi-tenancy isolation (addresses Dragonfire review MPC audit):
- Add OrgID field to all event messages (keygen, sign, reshare)
- Scope session dedup keys as orgID-walletID-txID
- Namespace KVStore keys with org prefix (org:<orgID>:<key>)
- Backward-compatible fallback to legacy unscoped keys
- New org_kvstore.go with OrgScopedKey/GetKeyShareWithFallback

Naming cleanup:
- Rename all badger references to zapdb in code, config, tests
- config.yaml: badger_password → zapdb_password
- AppConfig: BadgerPassword → ZapDBPassword
- Fix MPCiumVersion typo → MPCVersion
2026-03-22 12:02:30 -07:00
zeekay 518408ebcc feat: BLS threshold + security hardening + ZapDB KMS-backing
Security (from red team review):
- Replay protection: request_id + timestamp freshness on sign API
- Ed25519 message auth re-enabled on all protocol messages
- DKG timeout (5min) + signing timeout (30s) with watchdog
- Dedup map TTL cleanup (10min expiry, 2min sweep)

BLS threshold signing:
- KeyTypeBLS + keygen/signing sessions
- TrustedDealer pattern for BLS DKG
- CBOR serialization for BLS config
- 8 tests

ZapDB KMS-backing:
- HSM PasswordProvider wired to ZapDB init
- Supports: AWS KMS, GCP KMS, Azure KV, env, file
- Default env for backward compat
- 19 tests

Also: sr25519 sessions, fixed SignWireMessage build break
2026-03-19 01:15:33 -07:00
zeekay 0eafffe096 Wire HSM threshold attestation into MPC daemon
- Add --hsm-attest / MPC_HSM_ATTEST flag for share attestation
- Add --hsm-signer / --hsm-signer-key-id flags for co-signing
- KeyShareVault initialized when attestation enabled
- Bump github.com/luxfi/hsm v1.0.0 → v1.1.0
2026-03-02 23:52:08 -08:00
zeekay 45721d9485 feat: multi-chain address derivation (BTC, SOL), fix policy engine
- Derive Bitcoin P2PKH address from secp256k1 ECDSA pubkey
- Derive Solana address from Ed25519 pubkey (base58)
- Fix loadPolicies: gracefully return empty list instead of error
  when no policies exist (was causing "failed to load policies" error)
- Filter enabled policies in-memory (JSONB boolean filter unreliable)
- Add BTC/SOL address fields to KeygenResult and wallet creation
- Update /info endpoint with full chain support matrix
- Map EVM-compatible chains (Lux, XRP) and Ed25519 chains (TON)
2026-02-24 23:19:15 -08:00
zeekay 45b89f9f86 fix: derive eth_address from 32-byte ECDSA keys, return int for connected_peers
- /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)
2026-02-24 23:02:25 -08:00
zeekay f8a232a907 fix: sign keygen/sign messages in consensus mode, fix API proxy paths
- 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
2026-02-23 22:44:17 -08:00
zeekay a7152aaa0b fix(ci): goimports formatting, no-panic logger test, remove unconvert
- 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())
2026-03-01 18:40:56 -08:00
zeekay fa1466abd9 refactor(kvstore): rename Badger→ZapDB types, clean up package API
- git mv badger.go→zapdb.go, badger_backup.go→zap_backup.go
- BadgerKVStore→Store, BadgerConfig→Config, NewBadgerKVStore→New
- BadgerBackupExecutor→Backup, NewBadgerBackupExecutor→NewBackup
- BadgerBackupMeta→BackupMeta, BadgerBackupVersionInfo→BackupVersion
- Config fields: DBPath→Path, EncryptionKey→Key, BackupEncryptionKey→BackupKey, BackupDir→Dir
- Backup fields: BackupEncryptionKey→Key, BackupDir→Dir
- Store fields: BackupExecutor→Exec, db→DB (exported, fixes e2e compile)
- Updated all callers: transport/factory, transport/state, backup/s3,
  cmd/mpcd/main, cmd/lux-mpc-cli/recovery, scripts/migration/add-key-type,
  e2e/base_test, pkg/kvstore/backup_test
2026-03-01 18:33:01 -08:00
zeekay 0c2a896d67 feat(mpc): key listing, KMS backup/restore, API-only backend, CGGMP21 config serialization 2026-03-01 18:12:18 -08:00