R2-1: biometric enrollment is a forged-enrollment oracle fixed.
- pkg/webauthn/verify.go: full WebAuthn verification — compare against the
random challenge bytes we issued (not the DB row ID), origin allowlist,
rpIDHash equality, UP+UV authenticator-flag enforcement, ceremony type.
- pkg/webauthn/liveness.go: SecureGate-signed liveness attestation. Ed25519
envelope over {providerId, userId, score, timestamp, nonce}. MPC verifies
against MPC_SECUREGATE_PUBKEY_ED25519 and enforces userId match, score
floor, max age. Body-supplied livenessScore is never trusted again.
- If SecureGate pubkey is unconfigured, handleBiometricEnroll returns 503.
- 14 new tests cover each rejection path.
R2-2: CAS-under-Postgres. The previous in-Go check-then-write +
unconditional UPSERT works on SQLite (writeMu) but loses updates under
Postgres READ COMMITTED. Switched to SELECT ... FOR UPDATE inside READ
COMMITTED tx via orm.RunInTransactionWith + orm.GetForUpdate[T]. Requires
hanzoai/[email protected] (tagged today).
- Session consume: operationLimit strictly respected under concurrency;
valueAccum computed under the lock.
- Operation approve: ApprovedBy append under the row lock, quorum
finalization transitions at most once even under racing approvers.
- pkg/db.New now supports postgres:// DSNs.
- New Postgres-backed regression tests in cas_postgres_test.go —
auto-skipped when no TEST_PG_DSN reachable; 10 concurrent consumers
against operationLimit=5 yields exactly 5 successes and 5 denials.
Bumps github.com/hanzoai/orm v0.3.2 → v0.4.0.
Test plan:
go test -tags sqlite_json1 -count=10 \
-run 'CASUnderRace|CASUnderRace_Postgres|RequireRoleOrAPIPermission' \
./pkg/api/ ./pkg/webauthn/
- 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 SecureGate 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.
Implement the canonical Liquidity 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")
db.New() auto-detects backend from DSN:
- "" or "sqlite://path" or "file.db" → SQLite (default)
- "postgres://..." → PostgreSQL (production upgrade)
KV (Valkey/Redis) is optional — pass "" to skip.
No postgres, no redis required to run mpcd out of the box.
Removed pgxpool direct dependency from db package.
Backup shard (shard 3) is encrypted with user's WebAuthn passkey
public key (P-256 ECDH). HSM stores the ciphertext but cannot
decrypt — only user's biometric (via Secure Enclave private key) can.
ATS shard (2) + encrypted_backup (3) = company CANNOT sign.
User shard (1) + passkey_decrypt(3) = user CAN exit independently.
Added UserPasskeyFingerprint to WalletBackup model.
Backup handler looks up user's WebAuthn credential for encryption.
- 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
- 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
- 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())
- 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
- 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)