Files
zeekay 6981a3cd6c mpcvm: M-Chain to 98.6%
The lane that did this was killed before it could commit; the tree it left is
green and is preserved here rather than lost. Root package 51.5% at the start
of the day, 93.3% at its last checkpoint, 98.6% now, with cert, fhe, types and
cmd/plugin all passing.

Includes the fault-injection suite it was building when it stopped: a node that
cannot read its state does not start, and one that cannot enumerate its
registry does not either.

Not reviewed by me beyond build, vet, gofmt and the full suite under -count=1.
The lane's own report is owed and did not arrive, so its mutation count and its
verdict on the mpcvm/fhe duplication are unrecorded.
2026-08-28 13:25:30 -07:00

509 lines
15 KiB
Go

// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package mpcvm
import (
"context"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"net/http"
)
// RPCRequest represents a JSON-RPC request
type RPCRequest struct {
JSONRPC string `json:"jsonrpc"`
ID interface{} `json:"id"`
Method string `json:"method"`
Params json.RawMessage `json:"params"`
}
// RPCResponse represents a JSON-RPC response
type RPCResponse struct {
JSONRPC string `json:"jsonrpc"`
ID interface{} `json:"id"`
Result interface{} `json:"result,omitempty"`
Error *RPCError `json:"error,omitempty"`
}
// RPCError represents a JSON-RPC error
type RPCError struct {
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data,omitempty"`
}
// Error implements the error interface
func (e *RPCError) Error() string {
return fmt.Sprintf("RPC error %d: %s", e.Code, e.Message)
}
// Error codes. Only codes this server can actually return are declared: a
// published code that nothing emits reads as a contract to callers who then
// write dead branches against it.
const (
RPCErrorInvalidRequest = -32600
RPCErrorMethodNotFound = -32601
RPCErrorInvalidParams = -32602
RPCErrorInternal = -32603
RPCErrorUnauthorized = -32002
RPCErrorQuotaExceeded = -32003
RPCErrorCeremonyNotFound = -32004
RPCErrorKeyNotFound = -32005
)
// createRPCHandler creates the JSON-RPC handler
func (vm *VM) createRPCHandler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
if r.Method != http.MethodPost {
writeRPCError(w, nil, RPCErrorInvalidRequest, "Method not allowed", nil)
return
}
var req RPCRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeRPCError(w, nil, RPCErrorInvalidRequest, "Invalid JSON", nil)
return
}
result, err := vm.handleRPCMethod(r.Context(), req.Method, req.Params)
if err != nil {
writeRPCResponse(w, req.ID, nil, asRPCError(err))
return
}
writeRPCResponse(w, req.ID, result, nil)
})
}
func writeRPCResponse(w http.ResponseWriter, id interface{}, result interface{}, err *RPCError) {
resp := RPCResponse{
JSONRPC: "2.0",
ID: id,
Result: result,
Error: err,
}
json.NewEncoder(w).Encode(resp)
}
func writeRPCError(w http.ResponseWriter, id interface{}, code int, message string, data interface{}) {
writeRPCResponse(w, id, nil, &RPCError{Code: code, Message: message, Data: data})
}
// handleRPCMethod dispatches RPC method calls.
//
// Two families, one per noun: threshold_* runs ceremonies and reports on the
// node, mpc_* reads the replicated custody state (keys, ceremonies, root).
// Nothing is reachable under two names — a capability with two spellings is a
// capability whose two spellings will eventually disagree.
func (vm *VM) handleRPCMethod(ctx context.Context, method string, params json.RawMessage) (interface{}, error) {
switch method {
// Ceremonies. Both run to completion before returning: keygen yields a
// registered key, sign yields the signature itself.
case "threshold_keygen", "threshold_sign":
// Ceremonies are requested BY a chain, and this transport authenticates
// no chain: an HTTP body can say it is the bridge as easily as it can
// say anything else. Custody rights are granted over the cross-chain
// path, where the sender's chain id is authenticated (CrossChainRequest),
// so there is exactly one way in and it is not this one.
return nil, &RPCError{
Code: RPCErrorUnauthorized,
Message: fmt.Sprintf("%s is requested by a chain, over the cross-chain transport that authenticates it; this endpoint authenticates no caller", method),
}
// Custody registry (replicated state)
case "mpc_getKey":
return vm.rpcGetKey(params)
case "mpc_listKeys":
return vm.rpcListKeys()
case "threshold_getPublicKey":
return vm.rpcGetPublicKey(params)
case "threshold_getAddress":
return vm.rpcGetAddress(params)
// Ceremony log (replicated state) — where a produced signature is read back
// from durably, and the root that says two validators agree.
case "mpc_getCeremony":
return vm.rpcGetCeremony(params)
case "mpc_listCeremonies":
return vm.rpcListCeremonies()
case "mpc_getStateRoot":
return vm.rpcGetStateRoot()
// Network Information
case "threshold_getInfo":
return vm.rpcGetInfo()
case "threshold_getStats":
return vm.rpcGetStats()
case "threshold_getParties":
return vm.rpcGetParties(ctx)
case "threshold_getQuota":
return vm.rpcGetQuota(params)
// Authorization
case "threshold_getAuthorizedChains":
return vm.rpcGetAuthorizedChains()
case "threshold_getChainPermissions":
return vm.rpcGetChainPermissions(params)
// Health
case "threshold_health":
return vm.rpcHealthCheck(ctx)
default:
return nil, &RPCError{Code: RPCErrorMethodNotFound, Message: fmt.Sprintf("method not found: %s", method)}
}
}
// asRPCError maps an error to its JSON-RPC code. It is the ONE classifier: the
// handler runs it over whatever a method returned, so a method that already
// classified its own failure keeps that code and one that did not gets the code
// this table gives it. Two classifiers would eventually disagree about the same
// failure.
//
// Matching is by errors.Is/As because the VM wraps its errors with context.
func asRPCError(err error) *RPCError {
var already *RPCError
if errors.As(err, &already) {
return already
}
switch {
case errors.Is(err, ErrUnauthorizedChain):
return &RPCError{Code: RPCErrorUnauthorized, Message: err.Error()}
case errors.Is(err, ErrQuotaExceeded):
return &RPCError{Code: RPCErrorQuotaExceeded, Message: err.Error()}
case errors.Is(err, ErrUnknownKey), errors.Is(err, ErrShareNotHeld):
return &RPCError{Code: RPCErrorKeyNotFound, Message: err.Error()}
default:
return &RPCError{Code: RPCErrorInternal, Message: err.Error()}
}
}
// =============================================================================
// Ceremony RPCs
// =============================================================================
// =============================================================================
// Custody registry RPCs (replicated state)
// =============================================================================
// GetKeyParams contains parameters for getting a key.
type GetKeyParams struct {
KeyID string `json:"keyId"`
}
func (vm *VM) rpcListKeys() ([]KeyInfo, error) {
recs, err := vm.Keys()
if err != nil {
return nil, asRPCError(err)
}
keys := make([]KeyInfo, 0, len(recs))
for _, rec := range recs {
keys = append(keys, keyInfoOf(rec))
}
return keys, nil
}
func (vm *VM) rpcGetKey(params json.RawMessage) (*KeyInfo, error) {
var p GetKeyParams
if err := json.Unmarshal(params, &p); err != nil {
return nil, &RPCError{Code: RPCErrorInvalidParams, Message: err.Error()}
}
rec, err := vm.Key(p.KeyID)
if err != nil {
return nil, asRPCError(err)
}
info := keyInfoOf(rec)
return &info, nil
}
func (vm *VM) rpcGetPublicKey(params json.RawMessage) (map[string]string, error) {
var p GetKeyParams
if err := json.Unmarshal(params, &p); err != nil {
return nil, &RPCError{Code: RPCErrorInvalidParams, Message: err.Error()}
}
pubKey, err := vm.PublicKey(p.KeyID)
if err != nil {
return nil, asRPCError(err)
}
return map[string]string{"publicKey": "0x" + hex.EncodeToString(pubKey)}, nil
}
func (vm *VM) rpcGetAddress(params json.RawMessage) (map[string]string, error) {
var p GetKeyParams
if err := json.Unmarshal(params, &p); err != nil {
return nil, &RPCError{Code: RPCErrorInvalidParams, Message: err.Error()}
}
address, err := vm.Address(p.KeyID)
if err != nil {
return nil, asRPCError(err)
}
return map[string]string{"address": "0x" + hex.EncodeToString(address)}, nil
}
// =============================================================================
// Ceremony log RPCs (replicated state)
// =============================================================================
// GetCeremonyParams contains parameters for reading one ceremony.
type GetCeremonyParams struct {
CeremonyID string `json:"ceremonyId"`
}
func (vm *VM) rpcGetCeremony(params json.RawMessage) (*CeremonyInfo, error) {
var p GetCeremonyParams
if err := json.Unmarshal(params, &p); err != nil {
return nil, &RPCError{Code: RPCErrorInvalidParams, Message: err.Error()}
}
rec, err := vm.Ceremony(p.CeremonyID)
if err != nil {
return nil, &RPCError{Code: RPCErrorCeremonyNotFound, Message: err.Error()}
}
info := ceremonyInfoOf(rec)
return &info, nil
}
func (vm *VM) rpcListCeremonies() ([]CeremonyInfo, error) {
recs, err := vm.Ceremonies()
if err != nil {
return nil, asRPCError(err)
}
out := make([]CeremonyInfo, 0, len(recs))
for _, rec := range recs {
out = append(out, ceremonyInfoOf(rec))
}
return out, nil
}
func (vm *VM) rpcGetStateRoot() (map[string]string, error) {
root := vm.StateRoot()
return map[string]string{"stateRoot": "0x" + hex.EncodeToString(root[:])}, nil
}
// keyInfoOf projects a custody key's replicated record onto the wire. One
// conversion in one place: every RPC that returns a key returns the same shape,
// so a field cannot mean one thing under mpc_getKey and another under
// threshold_keygen.
func keyInfoOf(rec *KeyRecord) KeyInfo {
participants := make([]string, len(rec.Participants))
for i, p := range rec.Participants {
participants[i] = string(p)
}
return KeyInfo{
KeyID: rec.KeyID,
Kind: rec.Kind,
Policy: rec.Policy.String(),
Degree: rec.Degree(),
GroupPublicKey: "0x" + hex.EncodeToString(rec.GroupPublicKey),
Address: "0x" + hex.EncodeToString(rec.Address),
Participants: participants,
Generation: rec.Generation,
CreatedHeight: rec.CreatedHeight,
}
}
// ceremonyInfoOf projects a ceremony record onto the wire, splitting the
// 65-byte artifact into r‖s‖v for callers whose verifier wants the parts.
func ceremonyInfoOf(c *CeremonyRecord) CeremonyInfo {
signers := make([]string, len(c.Signers))
for i, s := range c.Signers {
signers[i] = string(s)
}
info := CeremonyInfo{
CeremonyID: c.ID,
Kind: c.Kind,
KeyID: c.KeyID,
Digest: "0x" + hex.EncodeToString(c.Digest),
Signature: "0x" + hex.EncodeToString(c.Artifact),
Signers: signers,
RequestingChain: c.RequestingChain,
Height: c.Height,
}
if len(c.Artifact) == 65 {
info.R = "0x" + hex.EncodeToString(c.Artifact[0:32])
info.S = "0x" + hex.EncodeToString(c.Artifact[32:64])
info.V = int(c.Artifact[64])
}
return info
}
// =============================================================================
// Network Information RPCs
// =============================================================================
// Note: ThresholdInfo type is defined in client.go
func (vm *VM) rpcGetInfo() (*ThresholdInfo, error) {
keys, err := vm.Keys()
if err != nil {
return nil, asRPCError(err)
}
root := vm.StateRoot()
vm.mu.RLock()
defer vm.mu.RUnlock()
chains := make([]string, 0, len(vm.config.AuthorizedChains))
for chainID := range vm.config.AuthorizedChains {
chains = append(chains, chainID)
}
return &ThresholdInfo{
Version: Version.String(),
NodeID: vm.rt.NodeID.String(),
ChainID: vm.rt.ChainID.String(),
PartyID: string(vm.partyID),
Policy: vm.config.Policy.String(),
AuthorizedChains: chains,
TotalKeys: len(keys),
SharesHeld: len(vm.shares),
StagedCeremonies: vm.staged.Len(),
StateRoot: "0x" + hex.EncodeToString(root[:]),
}, nil
}
func (vm *VM) rpcGetStats() (*NetworkStats, error) {
vm.mu.RLock()
staged := vm.staged.Len()
vm.mu.RUnlock()
vm.stats.mu.RLock()
defer vm.stats.mu.RUnlock()
stats := &NetworkStats{
TotalSignatures: vm.stats.TotalSignatures,
TotalKeygens: vm.stats.TotalKeygens,
StagedCeremonies: staged,
SignaturesByChain: make(map[string]uint64, len(vm.stats.SignaturesByChain)),
}
for k, v := range vm.stats.SignaturesByChain {
stats.SignaturesByChain[k] = v
}
return stats, nil
}
// PartyInfo contains party information.
type PartyInfo struct {
// PartyID and NodeID are the same value in two spellings — party.ID IS the
// NodeID string — and both are reported so a caller reading either column
// needs no side table to join them.
PartyID string `json:"partyId"`
NodeID string `json:"nodeId"`
IsLocal bool `json:"isLocal"`
}
// rpcGetParties reports the ceremony committee: this chain's validator set.
// There is no separate MPC party roster to drift from it — joining the signing
// ring IS joining the validator set.
func (vm *VM) rpcGetParties(ctx context.Context) ([]PartyInfo, error) {
vm.mu.RLock()
height := vm.rt.PChainHeight
self := vm.partyID
vm.mu.RUnlock()
committee, err := vm.Committee(ctx, height)
if err != nil {
return nil, asRPCError(err)
}
parties := make([]PartyInfo, len(committee))
for i, pid := range committee {
parties[i] = PartyInfo{
PartyID: string(pid),
NodeID: string(pid),
IsLocal: pid == self,
}
}
return parties, nil
}
// GetQuotaParams contains parameters for getting quota
type GetQuotaParams struct {
ChainID string `json:"chainId"`
}
// Note: QuotaInfo type is defined in client.go
func (vm *VM) rpcGetQuota(params json.RawMessage) (*QuotaInfo, error) {
var p GetQuotaParams
if err := json.Unmarshal(params, &p); err != nil {
return nil, &RPCError{Code: RPCErrorInvalidParams, Message: err.Error()}
}
vm.mu.RLock()
defer vm.mu.RUnlock()
perms, ok := vm.config.AuthorizedChains[p.ChainID]
if !ok {
return nil, &RPCError{Code: RPCErrorUnauthorized, Message: "chain not authorized"}
}
limit := perms.DailySigningLimit
if vm.config.DailySigningQuota[p.ChainID] > 0 {
limit = vm.config.DailySigningQuota[p.ChainID]
}
used := vm.dailySigningCount[p.ChainID]
remaining := uint64(0)
if limit > used {
remaining = limit - used
}
return &QuotaInfo{
ChainID: p.ChainID,
DailyLimit: limit,
UsedToday: used,
Remaining: remaining,
ResetTime: vm.quotaResetTime.Unix(),
}, nil
}
// =============================================================================
// Authorization RPCs
// =============================================================================
func (vm *VM) rpcGetAuthorizedChains() ([]string, error) {
chains := make([]string, 0, len(vm.config.AuthorizedChains))
for chainID := range vm.config.AuthorizedChains {
chains = append(chains, chainID)
}
return chains, nil
}
// GetChainPermissionsParams contains parameters for getting chain permissions
type GetChainPermissionsParams struct {
ChainID string `json:"chainId"`
}
func (vm *VM) rpcGetChainPermissions(params json.RawMessage) (*ChainPermissions, error) {
var p GetChainPermissionsParams
if err := json.Unmarshal(params, &p); err != nil {
return nil, &RPCError{Code: RPCErrorInvalidParams, Message: err.Error()}
}
perms, ok := vm.config.AuthorizedChains[p.ChainID]
if !ok {
return nil, &RPCError{Code: RPCErrorUnauthorized, Message: "chain not authorized"}
}
return perms, nil
}
// =============================================================================
// Health RPCs
// =============================================================================
func (vm *VM) rpcHealthCheck(ctx context.Context) (map[string]interface{}, error) {
health, err := vm.HealthCheck(ctx)
if err != nil {
return nil, asRPCError(err)
}
result := make(map[string]interface{}, len(health.Details)+1)
result["healthy"] = health.Healthy
for k, v := range health.Details {
result[k] = v
}
return result, nil
}