Files
chains/mpcvm/executor.go
T
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

225 lines
6.8 KiB
Go

// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package mpcvm
// executor.go — the one place that knows the threshold library exists.
//
// M-Chain runs exactly one threshold protocol: CGGMP21 (cmp), the threshold
// ECDSA that produces signatures an external chain's ecrecover accepts. There
// is no protocol registry and no selection: a custody key's Kind names the
// protocol it was generated with, and cggmp21 is the only value that can be
// written. A registry whose other entries return "not yet implemented" is not
// a choice, it is a menu of failures, and it invites a caller to reach one.
//
// Everything above this file speaks in ceremonies; everything below it is
// github.com/luxfi/threshold. The MessageRouter is the seam: the executor
// drives a per-party protocol.Handler and the router moves that handler's
// opaque messages to the other parties (transport.go).
import (
"context"
"fmt"
"sync"
"github.com/luxfi/log"
"github.com/luxfi/threshold/pkg/ecdsa"
"github.com/luxfi/threshold/pkg/math/curve"
"github.com/luxfi/threshold/pkg/party"
"github.com/luxfi/threshold/pkg/pool"
"github.com/luxfi/threshold/pkg/protocol"
"github.com/luxfi/threshold/protocols/cmp"
cmpconfig "github.com/luxfi/threshold/protocols/cmp/config"
)
// MessageRouter carries one ceremony's protocol messages between parties. It
// never interprets them: the cryptography is the threshold library's, the
// delivery is the router's, and neither knows the other's business.
type MessageRouter interface {
// Send sends a message to the party it addresses, or broadcasts it to the
// whole ceremony when To is empty.
Send(msg *protocol.Message) error
// Receive yields messages addressed to this party.
Receive() <-chan *protocol.Message
}
// ProtocolExecutor drives threshold protocols to completion over a router.
type ProtocolExecutor struct {
pool *pool.Pool
logger log.Logger
// handlers are the ceremonies running right now, keyed by session id, so a
// ceremony can be torn down by name when it finishes or its context dies.
mu sync.RWMutex
handlers map[string]*protocol.Handler
}
// NewProtocolExecutor creates a new protocol executor.
func NewProtocolExecutor(workerPool *pool.Pool, logger log.Logger) *ProtocolExecutor {
return &ProtocolExecutor{
pool: workerPool,
logger: logger,
handlers: make(map[string]*protocol.Handler),
}
}
// CreateHandler starts one ceremony and registers it under sessionID.
func (pe *ProtocolExecutor) CreateHandler(
ctx context.Context,
sessionID string,
startFunc protocol.StartFunc,
) (*protocol.Handler, error) {
handler, err := protocol.NewHandler(
ctx,
pe.logger,
nil, // no metrics registry
startFunc,
[]byte(sessionID),
protocol.DefaultConfig(),
)
if err != nil {
return nil, fmt.Errorf("mpcvm: create ceremony handler: %w", err)
}
pe.mu.Lock()
pe.handlers[sessionID] = handler
pe.mu.Unlock()
return handler, nil
}
// RemoveHandler stops a ceremony and forgets it. Idempotent.
func (pe *ProtocolExecutor) RemoveHandler(sessionID string) {
pe.mu.Lock()
defer pe.mu.Unlock()
if handler, ok := pe.handlers[sessionID]; ok {
handler.Stop()
delete(pe.handlers, sessionID)
}
}
// Live reports how many ceremonies this executor is driving. It is what a
// leaked handler shows up in.
func (pe *ProtocolExecutor) Live() int {
pe.mu.RLock()
defer pe.mu.RUnlock()
return len(pe.handlers)
}
// RunCMPKeygen runs one distributed key generation to completion. threshold is
// the polynomial degree t, never the signer count: see quorum.Policy.Degree.
func (pe *ProtocolExecutor) RunCMPKeygen(
ctx context.Context,
sessionID string,
selfID party.ID,
participants []party.ID,
threshold int,
router MessageRouter,
) (*cmpconfig.Config, error) {
start := cmp.Keygen(curve.Secp256k1{}, selfID, participants, threshold, pe.pool)
return runProtocol[*cmpconfig.Config](ctx, pe, sessionID, start, router)
}
// RunCMPSign runs one signing ceremony to completion and returns the canonical
// Ethereum r‖s‖v encoding.
func (pe *ProtocolExecutor) RunCMPSign(
ctx context.Context,
sessionID string,
config *cmpconfig.Config,
signers []party.ID,
messageHash []byte,
router MessageRouter,
) (*ECDSASignature, error) {
start := cmp.Sign(config, signers, messageHash, pe.pool)
// cmp.Sign yields *ecdsa.Signature (R point, S scalar). Asserting the
// protocol result straight to *ECDSASignature never matches — it produced
// "unexpected result type" on every signature. Assert the real type, then
// convert.
sig, err := runProtocol[*ecdsa.Signature](ctx, pe, sessionID, start, router)
if err != nil {
return nil, err
}
return ecdsaSigToWrapper(sig)
}
// runProtocol drives one ceremony to its result, moving messages both ways over
// the router for as long as it runs.
func runProtocol[T any](
ctx context.Context,
pe *ProtocolExecutor,
sessionID string,
startFunc protocol.StartFunc,
router MessageRouter,
) (T, error) {
var zero T
handler, err := pe.CreateHandler(ctx, sessionID, startFunc)
if err != nil {
return zero, err
}
defer pe.RemoveHandler(sessionID)
done := make(chan struct{})
var routerErr error
// Outgoing: everything this party emits goes to the other parties.
go func() {
defer close(done)
for msg := range handler.Listen() {
if err := router.Send(msg); err != nil {
routerErr = err
return
}
}
}()
// Incoming: everything the other parties emit reaches this handler.
go func() {
for msg := range router.Receive() {
handler.Accept(msg)
}
}()
result, err := handler.WaitForResult()
if err != nil {
return zero, fmt.Errorf("mpcvm: ceremony %s: %w", sessionID, err)
}
<-done
if routerErr != nil {
return zero, fmt.Errorf("mpcvm: ceremony %s transport: %w", sessionID, routerErr)
}
typed, ok := result.(T)
if !ok {
return zero, fmt.Errorf("mpcvm: ceremony %s produced %T, want %T", sessionID, result, zero)
}
return typed, nil
}
// ECDSASignature is a threshold ECDSA signature in the one encoding M-Chain
// stores and every verifier reads: r(32) ‖ s(32) ‖ v(1).
type ECDSASignature struct {
R []byte
S []byte
V byte
}
// ecdsaSigToWrapper converts a threshold-library ECDSA signature into the wire
// wrapper as canonical Ethereum r(32)‖s(32)‖v(1): low-S normalised, so it
// verifies under luxfi/crypto secp256k1.VerifySignature and on-chain ecrecover.
func ecdsaSigToWrapper(sig *ecdsa.Signature) (*ECDSASignature, error) {
if sig == nil {
return nil, fmt.Errorf("mpcvm: nil signature")
}
// SigEthereum builds a fixed 65-byte array and checks both component lengths
// on the way in, so a successful return is 65 bytes by construction. A
// length check here would restate that and could never fire.
eth, err := sig.SigEthereum()
if err != nil {
return nil, fmt.Errorf("mpcvm: encode signature: %w", err)
}
return &ECDSASignature{R: eth[0:32], S: eth[32:64], V: eth[64]}, nil
}