mirror of
https://github.com/luxfi/mpc.git
synced 2026-09-24 00:26:24 +00:00
158 lines
6.0 KiB
Go
158 lines
6.0 KiB
Go
// Package client — canonical client interface for Lux MPC.
|
|
//
|
|
// import mpc "github.com/luxfi/mpc/client"
|
|
// var t mpc.Threshold = luxmpc.NewThreshold(cfg)
|
|
|
|
package client
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
)
|
|
|
|
// Typed errors. Callers branch via errors.Is.
|
|
var (
|
|
// ErrIdempotencyConflict is returned when a Sign call reuses an
|
|
// IdempotencyKey from a prior session whose canonical request hash
|
|
// differs from this one. Implementations MUST never return a
|
|
// cached signature when the request differs — that is a signing
|
|
// oracle.
|
|
ErrIdempotencyConflict = errors.New("mpc: idempotency conflict")
|
|
|
|
// ErrIdempotencyKeyRequired is returned when Sign is called with
|
|
// an empty IdempotencyKey.
|
|
ErrIdempotencyKeyRequired = errors.New("mpc: idempotency key required")
|
|
|
|
// ErrComplianceRejected is returned when the backend's compliance
|
|
// ruleset refuses the signature. The wrapped *ComplianceRejection
|
|
// names the gate.
|
|
ErrComplianceRejected = errors.New("mpc: compliance rejected")
|
|
|
|
// ErrSessionNotFound is returned by GetSession / Cancel for an
|
|
// unknown session id, OR for a session that does not belong to the
|
|
// caller's org (the implementation MUST NOT distinguish the two —
|
|
// disclosing existence is an enumeration oracle).
|
|
ErrSessionNotFound = errors.New("mpc: session not found")
|
|
)
|
|
|
|
// Threshold is the compliance-gated threshold-signature surface.
|
|
// Every custody mutation (mint, burn, transfer, settle) signs through
|
|
// Threshold.Sign — there is no direct path to the MPC backend.
|
|
//
|
|
// Identity: the request's UserID + OrgID MUST be derived from the
|
|
// validated bearer-auth Claims attached to ctx. Implementations MUST
|
|
// refuse a call whose request UserID/OrgID disagrees with ctx Claims.
|
|
type Threshold interface {
|
|
// Kind reports the backend identifier
|
|
// (ta-cggmp21 | lit-protocol | fireblocks-mpc | coinbase-custody).
|
|
Kind() string
|
|
|
|
// Sign requests a threshold signature.
|
|
//
|
|
// Idempotency contract:
|
|
// 1. IdempotencyKey is required. Empty → ErrIdempotencyKeyRequired.
|
|
// 2. The implementation MUST compute CanonicalHash(SignRequest)
|
|
// over every request field except IdempotencyKey itself, and
|
|
// bind the cached session to that hash.
|
|
// 3. A second call with the same IdempotencyKey and the same
|
|
// CanonicalHash returns the cached SignResult.
|
|
// 4. A second call with the same IdempotencyKey but a different
|
|
// CanonicalHash returns ErrIdempotencyConflict. NO signing.
|
|
//
|
|
// Compliance contract:
|
|
// The implementation MUST re-derive the on-chain payload hash
|
|
// from (Intent, WalletID, Asset, Amount, ChainID) and compare to
|
|
// the caller-supplied PayloadHash. Mismatch → reject with
|
|
// ErrPayloadMismatch. This prevents a caller from describing a
|
|
// "$10 settle" while signing a "$100M withdraw" hash.
|
|
Sign(ctx context.Context, req SignRequest) (*SignResult, error)
|
|
|
|
// GetSession returns the current state of a signature session
|
|
// belonging to the caller's org. Unknown ids and cross-org ids
|
|
// return ErrSessionNotFound indistinguishably.
|
|
GetSession(ctx context.Context, sessionID string) (*SignSession, error)
|
|
|
|
// Cancel cancels a pending session belonging to the caller's org.
|
|
//
|
|
// Scoping (MUST be enforced by the implementation):
|
|
// - Derive orgID from ctx Claims; the caller does not supply it.
|
|
// - SELECT-FOR-UPDATE the session row and verify session.org_id ==
|
|
// ctx.OrgID before transitioning. Mismatch returns
|
|
// ErrSessionNotFound — the same opaque response as a truly
|
|
// unknown id, so a cross-org enumeration cannot distinguish
|
|
// "exists, not yours" from "does not exist".
|
|
// - The session-state transition (pending → cancelled) MUST be a
|
|
// row-level CAS with the org predicate inside the WHERE so a
|
|
// concurrent legitimate cancel from the right org cannot lose
|
|
// to a spoofed cross-org call.
|
|
//
|
|
// Idempotent on already-cancelled sessions for the SAME org; cross-
|
|
// org cancellation returns ErrSessionNotFound even if the session
|
|
// is already cancelled (no oracle leak via repeat-cancel semantics).
|
|
Cancel(ctx context.Context, sessionID string) error
|
|
}
|
|
|
|
// SignRequest is the caller-side payload.
|
|
//
|
|
// UserID and OrgID are RESERVED for the implementation to populate
|
|
// from ctx Claims. Callers SHOULD leave them empty; if set, the
|
|
// implementation MUST verify they match ctx Claims and reject on
|
|
// mismatch.
|
|
type SignRequest struct {
|
|
// UserID + OrgID are server-populated from ctx Claims.
|
|
UserID, OrgID string
|
|
|
|
// Intent: settle_trade | swap_dex | mint | burn | withdraw |
|
|
// redeem | corporate_action.
|
|
Intent string
|
|
// WalletID is the provider_wallet_id from custody_wallets.
|
|
WalletID string
|
|
// Asset is the token symbol (BTC, USDL, ETH, ...).
|
|
Asset string
|
|
// Amount is the human-readable string.
|
|
Amount string
|
|
// ChainID is the EIP-155 chain id (or chain-specific identifier
|
|
// for non-EVM keys).
|
|
ChainID int
|
|
// PayloadHash is the 32-byte hex of the tx payload. The
|
|
// implementation MUST re-derive from the other request fields and
|
|
// reject mismatch.
|
|
PayloadHash string
|
|
// KeyType: secp256k1 | ed25519 | sr25519 | mldsa65.
|
|
KeyType string
|
|
// IdempotencyKey is required.
|
|
IdempotencyKey string
|
|
// TradeID is optional context for settle_trade.
|
|
TradeID string
|
|
}
|
|
|
|
// ComplianceRejection details which compliance gate refused a Sign call.
|
|
type ComplianceRejection struct {
|
|
// Reason: sanctions | velocity | blocklist | manual_review |
|
|
// key_disabled | tenant_disabled | payload_mismatch.
|
|
Reason string
|
|
// ApprovalID is the audit row id; appears in operator dashboards.
|
|
ApprovalID string
|
|
}
|
|
|
|
func (r *ComplianceRejection) Error() string { return "mpc: " + r.Reason }
|
|
|
|
// SignResult is the immediate response.
|
|
type SignResult struct {
|
|
SessionID string
|
|
Signature []byte
|
|
// Status: pending | signed.
|
|
Status string
|
|
ApprovalID string
|
|
}
|
|
|
|
// SignSession is the poll response.
|
|
type SignSession struct {
|
|
SessionID string
|
|
Status string // pending | signed | rejected | cancelled
|
|
Signature []byte
|
|
ApprovalID string
|
|
// Reason populated when Status=rejected. See ComplianceRejection.
|
|
Reason string
|
|
}
|