50 Commits
Author SHA1 Message Date
zeekay 7ce84ffddf the binary serves the face it names
pkg/api embedded 56KB of genuine Geist and served it at /fonts/Geist-Variable.woff2, and the landing page's @font-face asked for it by name — so mpc.hanzo.ai painted its only pre-auth surface in a typeface the estate does not use. The bytes are Zen now; checked at the name table, family 'Zen', with the OFL notice the licence requires intact.

The dashboard was worse than wrong, it was blank: its layout published --font-geist-sans off the geist package while tailwind.config.ts asked for --font-zen-sans, so font-sans resolved to nothing and every screen fell back to the browser default. The docs layout imported Zen under the name GeistSans, which compiles and lies.
2026-08-28 11:36:40 -07:00
zeekay 008f3c7bea group imports the way .golangci.yml asks
The lint job runs goimports with local-prefixes github.com/luxfi/mpc, so a
luxfi/mpc import has to sit in its own group after the third-party ones. Three
files did not, and the job has been red on all three.

server.go also carried a trailing blank line left where a const was removed.
2026-08-21 01:19:30 -07:00
zeekay c130a582ad answer /favicon.ico, which a browser asks for on its own
The page names an SVG icon and that is what a current browser uses. One that
does not read SVG icons asks only for /favicon.ico, got a 404, and showed a
blank tab — the defect the SVG was meant to close, still open for that reader.

The ICO is the same mark, byte-for-byte what hanzo.ai serves, and it hangs off
the brand row like everything else: a host with no row still answers 404, which
is the right answer for an operator with no mark.
2026-08-21 01:06:56 -07:00
zeekay af382fa0b3 the page at / names the host that answered
One operator's page was compiled into the binary, so every host this runs behind
served the same wordmark, the same links and a bridge on someone else's domain.
mpc.hanzo.ai answered with a page carrying no operator name at all — the reader
had no way to tell whose custody service they were looking at.

brand.go resolves the operator from the Host header and landing.go renders the
page for it: wordmark, mark, docs and site link. A host with no row renders
unbranded, which is what anyone running this themselves should get, and is why
adding a row is the whole cost of onboarding an operator.

The mark doubles as /favicon.svg so the tab is not blank, and Geist is served
from the binary at /fonts — a cross-origin stylesheet is refused by the browser
and would leave every reader on system-ui while every rule asked for Geist.

Colours are the operator's, not a gradient: the two greys that sat under 4.5:1
on black are lifted to 4.9 and 6.1.
2026-08-21 00:44:55 -07:00
zooqueen b6ab5f46c1 reveal: the ring can be asked, and every node answers
pkg/reveal could open a secret; nothing could ask it to. This is the
asking: an opcode the KMS client has always sent, a backend that fans the
request out and waits for a quorum, and every node answering from its own
share.

Every node serves the answering half. A ring where only some answer is a
ring with a smaller quorum than its threshold claims, and the shortfall
would show up as a timeout rather than as the misconfiguration it is.

A node that holds no share for the key publishes that fact. Silence and
refusal look identical to a caller counting answers and they are not: one
means wait longer, the other means never — so a failed opening says how
many answered and names the nodes that would not.

The wait ends at a quorum rather than at the deadline. This runs at boot,
where a process that already has what it needs must not sit for twenty
seconds proving it, and one that will never get it must say so rather
than hang.

Answers are counted per party under a lock. A redelivery or a duplicated
subscription must not fill two seats — Open refuses such a set anyway,
but the wait should be honest as well as the result.

The two halves take a bus, a store and a name rather than reaching into
the backend, so the whole path is exercised over a bus a test controls:
the real Ed25519 keygen, each share in a store of its own, a root that
travels the ring and comes back, and a quorum that never forms saying
exactly why.

The interface gained a method, so two test mocks did too. That is the
compiler naming every implementation, which is the reason to put it
there.
2026-08-20 13:44:05 -07:00
zeekay 8273105926 The signing curve is derived from the network, not stated by the caller
Solana addresses were correct and receive-only: ConsensusKeyInfoStore.Save was
the only RegisterKey caller and hardcoded "secp256k1", and TriggerSign read the
curve from that field, so no product entry point could reach Ed25519 signing.

The hardcode is not fixed, it is UNREPRESENTABLE. transport.KeyInfo.KeyType is
deleted — the record carries one public key per curve and a PublicKeyFor(curve)
— and RegisterKey lost its keyType parameter, so there is no longer a place to
state a curve wrongly. A scalar KeyType was the wrong shape anyway: one keygen
ceremony produces BOTH a secp256k1 and an Ed25519 key, so "what curve is this
wallet" has no single answer.

The request carries the network instead, and ONE table maps it to a curve, with
NO default arm — KeyTypeForNetwork returns ErrUnknownNetwork rather than
guessing. That is the point: the old default returned secp256k1 for anything
unrecognized, which is how a Solana request would have been signed with the
wrong key.

/sign's 32-byte clamp becomes the SCHEME's rule rather than the endpoint's:
secp256k1 signs a digest and requires exactly 32 bytes, Ed25519 signs the
message at any length. /sign no longer accepts key_type at all — two ways to say
the curve is the misuse being removed — and Network is bound into the
idempotency fingerprint, so the same bytes for SOL and ETH conflict instead of
sharing one cached signature from a different key.

⚠ The trap this nearly shipped with: every pre-existing KeyInfo record has EMPTY
public keys, because the old Save wrote "", "". A naive "does this wallet have
this curve" gate would have refused every existing wallet on every curve. The
check is gated on RecordsCurves() — a record naming no curve denies none, and
the missing share stays the thing that fails closed.

Verified end to end, not by mirroring: a Solana request through
ConsensusMPCBackend.TriggerSign with a real 2-of-3 FROST ring produces a
signature crypto/ed25519.Verify accepts, over a 63-byte message so any
reintroduced clamp or pre-hash fails it; ETH/EVM/BTC/LUX still route to
secp256k1 and must RECOVER to the wallet's key. Four mutations tried, all
caught.

Behavioral risk worth naming: an unrecognized chain string now fails its
transaction closed rather than signing with a default. That is correct, and the
real fix is validating chain at POST /v1/transactions.
2026-08-06 15:59:30 -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 01db2a1edf api: doc comments said /api/v1 for routes that are /v1
Every route in pkg/api is mounted under r.Route("/v1", ...) at server.go:272,
so /auth/oidc is /v1/auth/oidc and trade/submit is /v1/.../trade/submit. Nine
doc comments claimed an /api/v1 prefix that no route has ever carried:

  handlers_trade_approval.go  submit / approve / reject / pending
  handlers_oidc.go            auth/oidc
  handlers_bridge.go          generate_mpc_sig / complete
  server.go                   the health-check note and the trade-endpoint note

handlers_approval.go already states the rule in-tree — "One canonical surface —
no `/api/` prefix, no v2" — so these were drift, not a second convention.

Comments that misdescribe a route are the same failure as a config that
misdescribes a threshold: they are what an audit reads. This is documentation
only; no route, handler or behaviour changes.

Remaining /api/ in this repo is all foreign and stays: pkg/hsm/zymbit.go (Zymbit
HSM device), pkg/hsm/providers.go (AWS IMDS), pkg/risk/chainalysis.go
(Chainalysis vendor). pkg/hsm/kms.go is a separate matter — it is an Infisical
client (/api/v3/secrets/raw with workspaceId/secretPath/type:shared), so moving
it to our native /v1/kms/orgs/{org}/secrets is a port, not a path swap.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-25 23:23:53 -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
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 ec0fb82eaa webauthn: vendor-neutral pluggable liveness Verifier; drop
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.)
2026-06-10 21:42:27 -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 03b1da16b9 sec(mpc): gate TFHE wallet keygen behind experimental_tfhe build tag
The threshold-FHE wallet keygen path (pkg/mpc/tfhe_session.go:154,165
production caller via pkg/mpc/node.go:583) was wired to
luxfi/threshold/protocols/tfhe.NewKeyGenerator + GenerateKeys. Per #197,
that primitive is a fail-loud placeholder: every party stores the full
master key, PartialDecrypt returns an HMAC tag, and CombineShares ignores
partials and runs single-party decryption. Upstream guards every entry
point with guardUnsafe() that panics unless
LUX_ALLOW_FAKE_TFHE_FOR_TESTING_ONLY=1 is set — by design.

Default production builds must not be able to reach the placeholder
primitive even with the env var set. Real-threshold wiring (luxfi/lattice
Shamir/LWE) is tracked as a multi-week separate task per LP-137 §2.6.

Changes:

- pkg/mpc/tfhe_session.go: //go:build experimental_tfhe — entire
  tfheKeygenSession + tfheComputeSession + TFHESession interface gated
  behind the tag.
- pkg/mpc/tfhe_node.go (new, experimental_tfhe): Node.CreateTFHEKeyGenSession
  + CreateTFHEComputeSession constructing the experimental sessions.
- pkg/mpc/tfhe_node_stub.go (new, !experimental_tfhe): stub Node methods
  that return ErrTFHENotImplemented from every entry point. This is what
  the production binary ships with.
- pkg/mpc/tfhe_errors.go (new, unconditional): ErrTFHENotImplemented
  symbol with a description pointing at LP-137 §2.6.
- pkg/mpc/node.go: removed the unconditional CreateTFHEKeyGenSession +
  CreateTFHEComputeSession definitions and the unused luxfi/fhe import.

Test pass:

  go test ./pkg/mpc/... ./pkg/policy/... -short -count=1 -race -timeout 60s
  ok  github.com/luxfi/mpc/pkg/mpc      2.016s
  ok  github.com/luxfi/mpc/pkg/policy   1.477s
  91 PASS, 0 FAIL (default build, no env var set)

  go build -o /tmp/mpcd_final ./cmd/mpcd/
  /tmp/mpcd_final version  # mpcd version 0.3.3 — no panic, no UNSAFE TFHE reach

  go build -tags experimental_tfhe ./pkg/mpc/...  # opt-in build OK

Note: pkg/policy/main_test.go sets LUX_ALLOW_FAKE_TFHE_FOR_TESTING_ONLY=1
because RealThresholdDecryptor's dispatcher tests exercise the placeholder
PartialDecrypter — same TestMain pattern threshold/protocols/tfhe uses.
These are orchestration-shape tests; they verify no threshold security
property because the underlying scheme has none.

Companion bumps required for the branch to compile:
- luxfi/threshold v1.6.0 -> v1.6.4 (FHECiphertext / FHEThresholdShare /
  ShareAggregator exports landed in v1.6.x)
- google/go-sev-guest -> go.mod (cc/attest/sev.go direct import)
- pkg/api/server.go: missing approval + tieredWallets fields added to
  satisfy in-tree handler references
2026-04-28 08:46:08 -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 cd9bf93919 feat(mpc): treasury 3-of-5 + purge Postgres, SQLite-only
- Postgres purge: delete cas_postgres_test.go, strip PG sidecar from CI
- Treasury 3-of-5 governance: pkg/api/handlers_treasury.go + models
- Routes: POST /v1/mpc/treasury/wallets, POST /v1/mpc/treasury/sign,
  GET /v1/mpc/treasury/wallets/{id}
- Per-org HSM isolation via tenant-scoped cosigner key
- Threshold escalation: small=3-of-5, large=4-of-5
2026-04-18 21:07:26 -07:00
zeekay 0313b7333d R3-8: bind liveness envelope to WebAuthn credential + challenge
A stolen  attestation (within its 2-minute validity window)
could be replayed against a different WebAuthn enrollment because the
envelope only attested (userID, score, timestamp) — it said nothing
about WHICH public key the user was actually enrolling.

Fix: extend LivenessAttestation with two optional binding fields:

  credentialHash = sha256(publicKey) [base64-std]
  challengeId    = the WebAuthn challenge being enrolled [base64url]

handleBiometricEnroll computes both values from the real enrollment
data (consumed challenge bytes + submitted public key) and passes them
as ExpectedCredentialHash / ExpectedChallengeID to VerifyLiveness.
Mismatches are always fatal. A missing binding is fatal in STRICT
mode (the default) and warns-and-accepts in LAX.

BindingMode is server-configured via MPC_LIVENESS_BINDING=strict|lax.
Production mainnet runs STRICT —  must ship the extended
envelope as a deployment blocker. LAX exists solely for the rollout
window; every permissive accept emits an `mpc.biometric.binding_warn`
log line so operators can see rollout progress.

Tests added in pkg/webauthn/liveness_test.go:
  - R38_AcceptsMatchingCredentialHash
  - R38_RejectsMismatchedCredentialHash
  - R38_RejectsMissingBindingStrict
  - R38_LaxWarnsOnMissing
  - R38_AcceptsMatchingChallengeID
  - R38_RejectsMismatchedChallengeID

Full ./pkg/api/ suite green; the existing pre-R3-8 LAX-style liveness
tests still pass (they supply no Expected* values → the binding path
is a no-op for them).
2026-04-18 17:30:14 -07:00
zeekay 6feb4d304b remove: "api" role pass-through from requireRole — one way only
Every /v1 mutating route that previously accepted role=="api" without
permission checks now uses requireRoleOrAPIPermission — JWT human roles OR
API keys that carry the concrete permission. R2-3 regression surface:

- /v1/transactions             → mpc:transactions:create
- /v1/subscriptions/*          → mpc:subscriptions:write
- /v1/payment-requests/*       → mpc:payment-requests:write
- /v1/smart-wallets/*          → mpc:smart-wallets:write
- /v1/trade/*                  → mpc:trade:write
- /v1/intents/* (sign/co-sign) → mpc:sign (same perm as /v1/mpc/sign)
- /v1/mpc/operations/approve,reject → mpc:operations:approve

Bare role=="api" is no longer sufficient anywhere — an API key with empty
permissions hits 403. Human roles (owner/admin/signer) still pass through
on JWT auth. Also adds the canonical requireRoleOrAPIPermission helper to
middleware.go.
2026-04-18 16:46:41 -07:00
zeekay ece16944e9 fix(mpc): F1/F4/F5/F6/F14/F15/F22 — harden /v1/mpc/sign + biometric + default wallet
- F1: /v1/mpc/sign now requires sessionId (403 without). Same for
  settlement/sign. No silent auto-session selection in handlers.
- F15: /v1/mpc/sign now gated by requireRole("owner","admin","signer","api")
  to match /v1/mpc/settlement/sign.
- F6: decodeMessage is strict — returns error when the declared encoding
  does not parse. Removed the raw-string fallback that made the endpoint
  an arbitrary-bytes signing oracle.
- F4: /v1/mpc/balances/{address} now resolves the address against the
  caller's org (ETH/BTC/SOL columns). Unknown or cross-tenant address
  returns 404 — no more "empty balances" leak.
- F5: biometric enroll requires a server-issued challenge, signed
  attestation (P-256 ECDSA verify), and a  liveness score
  above 0.8 (PAD-2). Raw templates are no longer accepted. Added
  consumeBiometricChallenge + verifyBiometricAttestation helpers.
- F14: added Wallet.DefaultForUserID column; handleMpcSetDefaultWallet
  now writes to it instead of stuffing a "default:<userID>" sentinel
  into the user-facing Name field. Previous default (if any) is cleared
  atomically per (orgId, userId).
- F22: findDefaultWalletID reads the new is_default column (via
  defaultForUserId) — no more "most recently created wallet" fallback
  that produced non-deterministic behavior.
2026-04-18 16:07:29 -07:00
zeekay 938b8c30e4 feat(api): /v1/mpc/* spec surface (wallets, sessions, operations, policies, audit)
Implement the canonical  MPC surface frozen in
openapi/mpc.yaml. All endpoints mount under /v1/mpc/* — one and only
one path per operation, no aliases, no backwards compatibility.

New:
- handlers_mpc.go — wallets, balances, default wallet, sign,
  settlement/sign, webauthn/biometric spec shapes
- handlers_sessions.go — time-bounded signing grants with operation
  and value limits, enforced on every /sign via consumeSessionForSign
- handlers_operations.go — unified Operation view over db.Transaction
  with kind ∈ {sign,send,mint,burn,transfer,contract_call}

Changed:
- server.go: mount r.Route("/mpc", …); remove top-level /v1/policies
  and /v1/audit (moved to /v1/mpc/policies, /v1/mpc/audit); retain
  /v1/transactions POST as internal wiring
- handlers_policy.go: accept both {id} (internal) and {policyId} (spec)
- pkg/db/models.go: add Session model (registered as "mpc-session")
2026-04-18 15:18:36 -07:00
zeekay f2fc502845 feat: REST + WebSocket + ZAP transports on every MPC node
- ws.go: EventBus pubsub + /v1/ws upgrade handler (stdlib, no deps)
- zap_server.go: StartZAP(port) with opcodes 0x0060-0x0064
  keygen, sign, status, wallets, intents over ZAP binary protocol
- Events field on Server for cross-transport event broadcasting
- Each consensus node serves full API (not separate mpc-api sidecar)
2026-04-17 19:55:55 -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 4c8948a48d fix: remove all tenant-specific data from MPC infrastructure
MPC is generic infrastructure — no hardcoded tenant URLs.
- Remove pars.network, zoo.network from OIDC issuers, ingress, TLS
- Remove .com,  from CORS, WebAuthn, tests
- CORS/WebAuthn origins now env-driven: MPC_CORS_ORIGINS, MPC_WEBAUTHN_ORIGINS
- Ingress: mpc.lux.network only — tenants route through their own ingress
- Tests updated to match infrastructure-only origin lists
2026-04-07 00:42:17 -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 bb9fc6a76c security: fix remaining Red team findings (LOW + MEDIUM)
MEDIUM: Bridge wallet org isolation — added orgID filter to wallet query
  in handleBridgeSign, prevents cross-tenant signing.
LOW-1: JWT iss/aud claims — tokens now carry issuer "mpc.lux.network"
  and distinct audiences ("mpc-api" vs "mpc-refresh"). Refresh tokens
  can no longer be used as access tokens.
LOW-2: Rate limiter XFF bypass — clientIP uses r.RemoteAddr (set by
  RealIP middleware) instead of parsing X-Forwarded-For directly.
LOW-3: Request body size limit — 1MB maxBodySize middleware prevents OOM
  from oversized payloads.
2026-04-06 23:28:34 -07:00
zeekay a3892c1b8f security: fix 3 critical findings from Red team review
CRITICAL-1: KMS routes registered outside auth middleware — moved inside
  authenticated route group with requireRole("owner", "admin")
CRITICAL-2: X-Org-ID header trusted without validation — replaced with
  getOrgID(r.Context()) from JWT claims
CRITICAL-3: (KMS standalone addressed in lux/kms repo)
2026-04-06 23:23:05 -07:00
zeekay c6a424659c feat: merge KMS validator key management into MPC binary
KMS routes now live inside MPC API server — one binary for both:
- POST /api/v1/keys/generate — create validator BLS + Corona keys
- GET /api/v1/keys — list all validator key sets
- GET /api/v1/keys/{id} — get validator key set
- POST /api/v1/keys/{id}/sign — sign with validator key
- POST /api/v1/keys/{id}/rotate — reshare validator keys

Storage: ORM (same postgres/ZapDB as MPC wallets).
No separate KMS binary needed — mpcd serves both.
2026-04-06 23:12:19 -07:00
zeekay e48748ce49 fix: remove duplicate bridge watchers, expose Handler for Base mount
- Remove pkg/bridge/ (btc_watcher + orchestrator) — bridge/opnet-watcher
  is the single source of truth for chain watching per CTO directive
- Add Handler() to API server for mounting on Hanzo Base
- MPC is signing-only; bridge is the control plane
2026-04-06 23:07:25 -07:00
zeekay bf24fd583d feat: configurable WebAuthn RP ID and origins for exchange
Add MPC_WEBAUTHN_RP_ID and MPC_WEBAUTHN_ORIGINS env vars to make
WebAuthn relying party and origin allowlist configurable. Adds
 exchange origins (exchange.{dev,test,main}..com,
) to WebAuthn origin checks, CORS, and security tests.
2026-04-02 19:09:00 -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 32c79e289f feat: add biometric trade approval flow with push notifications
- Trade approval service: submit, approve (WebAuthn), reject, pending
- Push notifier: APNS (iOS) + FCM (Android) for trade approval requests
- Biometric verification: Face ID/Touch ID with 30s freshness, nonce replay guard
- Custody policy engine: KYC/AML/sanctions/limits checks before co-signing
- Device store: register/revoke mobile devices with Secure Enclave keys
- API routes: POST /api/v1/trade/{submit,approve,reject}, GET pending
- Trade expiry reaper: cleanup expired approvals (>5 min)
- 17 custody tests + 33 API tests passing
2026-03-30 15:25:53 -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 3cfc76b275 fix: goimports formatting + CGO_ENABLED=0 for CI tests
- Run goimports -w on all 17 Go files with formatting issues
- Set CGO_ENABLED=0 on all CI steps (test, lint, build, CodeQL, SBOM)
- Remove -race flag from tests (requires CGO which pulls in blst.h)
- All 17 test packages pass with CGO_ENABLED=0
2026-03-22 12:50:58 -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 2fd738edc1 feat: dashboard pages + security hardening for intents, settlements, tx lifecycle
Backend:
- Reorg detection in tx tracker (re-verify block hash, detect receipt disappearance)
- Post-finalization safety margin (6 extra blocks before stopping tracking)
- Server-side HSM co-signing (HSMProvider interface, server calls HSM directly)
- Background intent expiry reaper (5-min sweep for stuck intents)
- Domain-separated intent hashing (lux-mpc-intent:v1 prefix)
- Backup rate limiting (max 3 active per wallet)
- Default backup changed to 2-of-3 (iCloud + HSM + offline) for fault tolerance
- All status transitions now use RecordTransition with proper actors

Frontend:
- Intents list page with status filter
- Intent detail page with lifecycle progress bar, sign/co-sign actions
- Settlements list page with status filter
- Settlement detail page with lifecycle, HSM signatures, transfer agency
- Enhanced transaction detail with on-chain confirmation, revert reason, status history
- Wallet backup UI with Shamir shard visualization
- Updated StatusBadge with all new status colors
- Nav updated with Intents + Settlements links
- Co-sign UI updated for server-side HSM flow (key_id only, no client signature)
2026-03-02 16:38:54 -08:00
zeekay f98a5649c8 feat: tx lifecycle tracking, Shamir backup sharding, settlement engine
Transaction Lifecycle ("did the tx land?"):
- TxTracker polls eth_getTransactionReceipt for block inclusion
- Receipt fields: blockNumber, blockHash, receiptStatus, gasUsed, revertReason
- Confirmation counter with configurable target (default 12)
- StatusHistory records every state transition with timestamp and actor
- Stuck/dropped tx detection with webhook alerts

Backup Key Sharding (Secure Recovery Flow):
- GF(2^8) Shamir Secret Sharing (ShamirSplit/ShamirCombine)
- WalletBackup: AES-256-GCM encrypt key share, Shamir-split the key
- Labeled shard destinations: icloud, hsm, offline, custody, device
- Default 2-of-2 (iCloud + Platform HSM), institutional 3-of-5

Settlement & Intent Engine:
- Intent lifecycle: pending_sign → signed → co_signed → recorded → matched
- HSM co-signing as second signer in multisig flow
- Settlement with HSM multisig attestation collection
- TransferAgency hash registration and verification
- WalletBackup, Intent, Settlement DB models

API Routes:
- POST/GET /intents, /intents/:id/sign, /intents/:id/co-sign
- GET /settlements, /settlements/:id
- POST/GET /wallets/:id/backup
- New webhooks: tx.included, tx.finalized, tx.reverted, intent.*

40 tests passing across backup, settlement, txtracker, api packages.
2026-03-02 14:22:05 -08:00
zeekay b9f268d661 feat(api): add landing page to dashboard API
Add HTML landing page at / route for mpc.lux.network showing:
- CGGMP21 and FROST protocol info
- Supported chains (BTC, ETH, LUX, SOL, XRPL, TON, etc.)
- Links to health, bridge dashboard, and API docs

Also update go.work to Go 1.26.1.
2026-02-28 15:22:57 -08:00
zeekay f80833787e feat: bridge admin dashboard, WebAuthn device management, LSS protocol selection
- Bridge admin pages: /bridge (overview), /bridge/wallets (signing wallet
  selector), /bridge/fees (fee config), /bridge/contracts (BRIDGE_ROLE mgmt)
- Bridge config API: GET/PATCH /api/v1/bridge/config, GET /bridge/networks
- BridgeConfig model in DB (org-scoped, stores wallet ID, fees, toggles)
- WebAuthn device management: /settings/devices (register/delete devices)
- Biometric approve button on transaction detail page using WebAuthn API
- Dashboard lib/webauthn.ts with startRegistration, startAuthentication
- Protocol selection in wallet creation wizard (CGGMP21, LSS, FROST)
- Protocol field on Wallet model, validation in create handler
- Wallet detail page shows protocol type
- Bridge nav link in sidebar
2026-02-25 01:38:29 -08:00
zeekay add4d7164b feat: WebAuthn biometric signing, enforce approver roles in policies
- Add WebAuthn/FIDO2 registration and verification endpoints
  POST /webauthn/register/begin, /register/complete, /verify
- Biometric approval: Face ID/Touch ID/YubiKey approve transactions
- Challenge bound to transaction ID (SHA256) preventing replay
- Enforce ApproverRoles from policy engine during transaction approval
- Add WebAuthnCredential DB model (pending_registration, active, revoked)
- Policy decisions now carry ApproverRoles for role-gated approvals
2026-02-25 00:46:17 -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 043c272187 fix: OAuth callback handles query string tokens, correct authorize endpoint
- Callback page now checks both hash fragment and query string for
  access_token (Casdoor returns it in query string, not hash)
- Login redirect uses /oauth/authorize instead of /login/oauth/authorize
- Add hanzo.id to default OIDC issuers in MPC API
- Update E2E tests to match corrected authorize URL
2026-02-23 19:57:09 -08:00
zeekay f3f39daff0 feat: OIDC login via Lux ID, fix ingress routing, deploy to lux-k8s
- Add OIDC token exchange endpoint (POST /api/v1/auth/oidc) that
  validates Lux ID access tokens and issues local MPC API JWTs
- Update dashboard auth callback to do 2-step exchange: get Lux ID
  token, then exchange for MPC API JWT via OIDC endpoint
- Fix IngressRoute to use path-based routing (/api/v1 → API, / → dashboard)
- Fix deploy.yml to target lux-k8s cluster (not hanzo-k8s)
- Fix wallet creation page to use api client instead of raw fetch
- Add /api and /healthz to dashboard middleware public paths
2026-02-22 12:55:48 -08:00
zeekay f711f4770f feat: API key permissions in context, clean up stale files
- validateAPIKey returns *db.APIKey so middleware has access to permissions
- API key permissions propagated into request context via ctxPermissions
- requirePermission middleware checks actual key permissions (not just role)
- PasswordHash/MFASecret fields marked api:"-" to prevent serialization
- Remove stale generated files (CHANGELOG, CI_STATUS, DOCKER_BUILD etc.)
- Update LLM.md with production deployment state and architecture decisions
2026-03-01 17:03:06 -08:00
zeekay df84005b46 feat: replace BadgerDB with ZapDB, remove local replace directives, vendor mode CI/CD
- Replace github.com/dgraph-io/badger/v4 with github.com/luxfi/zapdb/v4 v4.9.2
  throughout pkg/kvstore/, pkg/transport/, cmd/mpcd/, e2e/, scripts/
- Rename BadgerPath/BadgerPassword → ZapDBPath/ZapDBPassword in FactoryConfig
- Remove local replace directives from go.mod:
  - hanzoai/orm: now uses published v0.3.2 (tagged on GitHub)
  - hanzoai/kv-go/v9: now uses published v9.17.2-hanzo.1
  - Only remaining replace: agl/ed25519 → luxfi/edwards25519
- Add vendor/ directory (GOWORK=off go mod vendor) for reproducible builds
- Update Dockerfile to golang:1.26-alpine + -mod=vendor build flag
- Update CI/CD to use self-hosted linux/amd64 runners (no cross-compilation)
- Add k8s/valkey.yaml for session KV store
- Update k8s manifests: api-kv support, ZapDB password env, fresh secrets
- Dashboard API and MPC nodes all tested running on cluster (3/3 nodes healthy)
2026-03-01 15:27:32 -08:00
zeekay 864b6c217d fix: pass key type in signing requests, increase API timeout to 120s
- TriggerSign now looks up key type from keyInfoStore (was empty, causing
  "CGGMP21 only supports Secp256k1" errors)
- Wire keyInfoStore into ConsensusMPCBackend
- Increase HTTP middleware timeout from 30s to 120s for MPC signing
2026-03-01 05:10:49 -08:00
zeekay da5518b1b5 feat: add bridge signing endpoints for cross-chain transfers
- POST /api/v1/generate_mpc_sig — MPC signature for bridge transactions
- POST /api/v1/complete — mark bridge swap as confirmed
- Add bridge.lux.network to CORS origins
- Bridge wallet: 0xe591c587889dea548b6f886cea60049e4e358805
2026-03-01 05:01:19 -08:00
zeekay 8c3a27cd1f fix: Dockerfile Go version, public health endpoint, K8s probes
- Update Dockerfile from golang:1.23-alpine to golang:1.25-alpine
  matching go.mod requirement (go 1.25.5)
- Add public /healthz endpoint for K8s liveness/readiness probes
- Update api-deployment.yaml probes to use /healthz instead of
  JWT-protected /api/v1/status
- Run go mod tidy to fix missing go.sum entries
2026-02-28 13:32:02 -08:00
zeekay f110e06800 feat: MPC wallet dashboard - full-stack implementation
Complete MPC wallet management platform rivaling Fireblocks/Utila/Fordefi:

Backend (Go):
- REST API with 60+ endpoints on chi router (pkg/api/)
- PostgreSQL schema with 13 tables (pkg/db/)
- JWT + MFA auth, bcrypt passwords, API key management
- Policy engine (deny/approve/require_approval)
- Transaction signing flow with approval workflow
- Subscription scheduler for recurring payments
- Payment requests with public pay pages
- Safe + ERC-4337 smart wallet integration (pkg/smart/)
- Rate limiting, webhook delivery (HMAC-SHA256), audit logging
- 27 passing tests (auth, policy engine, middleware)

Frontend (Next.js 14):
- 27 routes with monochrome dark theme (Tailwind + Geist fonts)
- Full API client with JWT auto-refresh
- React Query hooks for all resources
- Pages: dashboard, vaults, wallets, transactions, policies,
  whitelist, team, settings, API keys, webhooks, audit,
  subscriptions, payments, smart wallets

Infrastructure:
- Dockerfiles for API (Go) and dashboard (Next.js standalone)
- compose.yml for local development (PostgreSQL + API + dashboard)
- K8s manifests for mpc.lux.network + cloud.lux.network
- GitHub Actions deploy workflow (build → push GHCR → kubectl apply)
- TLS via cert-manager, Traefik IngressRoute for both domains
2026-02-28 13:27:34 -08:00