Files
chains/mpcvm/executor.go
T
zeekay 1393f91994 mpcvm: delete the docs for a design that was never built
docs/ and DESIGN.md described protocol/{cggmp21,frost,corona_general,
tfhe_keygen} — four interface-only packages with no implementation and no
importer, deleted this pass — and pointed at a chains/mchain/ tree that does
not exist, which LLM.md already says never did. Prose describing an unbuilt
system reads as a contract; the reader who believes it looks for code that
isn't there.

What is true moves into LLM.md: the constants that must stay constants because
Verify enforces them, the two checks that must read consensus state rather than
the document being checked, and the boot fault that made a share-holding
validator unable to restart.
2026-08-28 13:47:20 -07:00

226 lines
6.9 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 offering four protocols and implementing one is not a
// choice, and a caller that reaches the other three finds an error where a
// signature should be.
//
// 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
}