Files
chains/mpcvm/executor_test.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

227 lines
7.9 KiB
Go

// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package mpcvm
// executor_test.go — the one place that knows the threshold library exists.
import (
"context"
"errors"
"sync"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/luxfi/crypto/secp256k1"
"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"
)
func newExecutor(t *testing.T) *ProtocolExecutor {
t.Helper()
pl := pool.NewPool(2)
t.Cleanup(pl.TearDown)
return NewProtocolExecutor(pl, log.NewTestLogger(log.ErrorLevel))
}
// A ceremony is registered while it runs and gone when it ends, so a leaked
// handler — one whose goroutines are still holding a worker pool and a party's
// round state — is visible rather than silent.
func TestACeremonyIsTrackedWhileItRunsAndNotAfter(t *testing.T) {
pe := newExecutor(t)
require.Zero(t, pe.Live())
start := cmp.Keygen(curve.Secp256k1{}, "pa", []party.ID{"pa", "pb"}, 1, pe.pool)
handler, err := pe.CreateHandler(ctx(), "mpc/one", start)
require.NoError(t, err)
require.NotNil(t, handler)
require.Equal(t, 1, pe.Live())
pe.RemoveHandler("mpc/one")
require.Zero(t, pe.Live())
pe.RemoveHandler("mpc/one")
require.Zero(t, pe.Live(), "tearing down a ceremony twice is not an error")
pe.RemoveHandler("mpc/never-existed")
}
// A ceremony that cannot be started is reported as such rather than registered
// as a ceremony that is running.
func TestACeremonyThatCannotStartIsNotTracked(t *testing.T) {
pe := newExecutor(t)
// A start function for a party that is not in its own participant set: the
// library refuses before the first round.
start := cmp.Keygen(curve.Secp256k1{}, "stranger", []party.ID{"pa", "pb"}, 1, pe.pool)
_, err := pe.CreateHandler(ctx(), "mpc/doomed", start)
require.ErrorContains(t, err, "create ceremony handler")
require.Zero(t, pe.Live())
}
// A ceremony whose transport fails is reported as a transport failure, named
// with the ceremony, rather than as a protocol failure or a timeout.
func TestATransportFailureIsAttributedToTheTransport(t *testing.T) {
pe := newExecutor(t)
ctxT, cancel := context.WithTimeout(ctx(), 30*time.Second)
defer cancel()
start := cmp.Keygen(curve.Secp256k1{}, "pa", []party.ID{"pa", "pb"}, 1, pe.pool)
_, err := runProtocol[*ecdsa.Signature](ctxT, pe, "mpc/broken", start, &brokenRouter{})
require.Error(t, err)
require.Contains(t, err.Error(), "mpc/broken")
}
// A ceremony that produces something other than what the caller asked for is an
// error naming both types, not a zero value the caller reads as success.
func TestAnUnexpectedCeremonyResultIsNamed(t *testing.T) {
pe := newExecutor(t)
ctxT, cancel := context.WithTimeout(ctx(), 60*time.Second)
defer cancel()
ids := []party.ID{"pa", "pb"}
net, routers := newMeshRouters(ids)
defer net.close()
done := make(chan struct{})
go func() {
defer close(done)
other := NewProtocolExecutor(pe.pool, log.NewTestLogger(log.ErrorLevel))
start := cmp.Keygen(curve.Secp256k1{}, "pb", ids, 1, other.pool)
_, _ = runProtocol[*ecdsa.Signature](ctxT, other, "mpc/typed", start, routers["pb"])
}()
start := cmp.Keygen(curve.Secp256k1{}, "pa", ids, 1, pe.pool)
_, err := runProtocol[*ecdsa.Signature](ctxT, pe, "mpc/typed", start, routers["pa"])
require.ErrorContains(t, err, "produced *config.Config, want *ecdsa.Signature")
<-done
}
// A ceremony whose context is already dead does not run.
func TestACeremonyWithNoTimeLeftDoesNotRun(t *testing.T) {
pe := newExecutor(t)
dead, cancel := context.WithCancel(ctx())
cancel()
net, routers := newMeshRouters([]party.ID{"pa", "pb"})
defer net.close()
_, err := pe.RunCMPKeygen(dead, "mpc/dead", "pa", []party.ID{"pa", "pb"}, 1, routers["pa"])
require.Error(t, err)
}
// -----------------------------------------------------------------------------
// The signature encoding
// -----------------------------------------------------------------------------
// The one encoding M-Chain stores and every verifier reads: r(32) ‖ s(32) ‖
// v(1), low-S normalised, so it verifies under luxfi/crypto and under an
// on-chain ecrecover without either side re-deriving anything.
func TestASignatureIsStoredInTheEncodingEveryVerifierReads(t *testing.T) {
_, groupPub, degree := realShare(t)
require.Equal(t, 1, degree)
require.Len(t, groupPub, 33)
pe := newExecutor(t)
ctxT, cancel := context.WithTimeout(ctx(), 120*time.Second)
defer cancel()
sig := signWithRealShare(t, ctxT, pe, digestOf(1))
require.Len(t, sig.R, 32)
require.Len(t, sig.S, 32)
raw := append(append([]byte(nil), sig.R...), sig.S...)
require.True(t, secp256k1.VerifySignature(groupPub, digestOf(1), raw),
"a signature the executor produced must verify under the key the DKG produced")
require.NoError(t, verifyGroupSignature(groupPub,
digestOf(1), append(raw, sig.V)),
"and under the same check a block applies")
}
// A ceremony that produced nothing is reported as nothing, not dereferenced.
// The type assertion in runProtocol admits a typed nil, and SigEthereum has a
// value receiver, so encoding one would fault the VM rather than fail a
// ceremony.
func TestACeremonyThatProducedNothingIsNamedNotDereferenced(t *testing.T) {
_, err := ecdsaSigToWrapper(nil)
require.ErrorContains(t, err, "nil signature")
}
// -----------------------------------------------------------------------------
// helpers
// -----------------------------------------------------------------------------
// brokenRouter refuses to carry anything, which is how a ceremony fails when
// the network under it does.
type brokenRouter struct{}
func (brokenRouter) Send(*protocol.Message) error { return errors.New("carrier pigeon lost") }
func (brokenRouter) Receive() <-chan *protocol.Message { return make(chan *protocol.Message) }
// signWithRealShare runs a signing ceremony across the committee that generated
// the package's one real key, and returns the signature every signer derived.
func signWithRealShare(t *testing.T, c context.Context, pe *ProtocolExecutor, digest []byte) *ECDSASignature {
t.Helper()
cfgs := realConfigs(t)
signers := make([]party.ID, 0, len(cfgs))
for id := range cfgs {
signers = append(signers, id)
}
signers = canonicalParties(signers)
net, routers := newMeshRouters(signers)
defer net.close()
sigs := make([]*ECDSASignature, len(signers))
errs := make([]error, len(signers))
var wg sync.WaitGroup
for i, id := range signers {
wg.Add(1)
go func(i int, id party.ID) {
defer wg.Done()
exec := NewProtocolExecutor(pe.pool, log.NewTestLogger(log.ErrorLevel))
sigs[i], errs[i] = exec.RunCMPSign(c, "mpc/sign", cfgs[id], signers, digest, routers[id])
}(i, id)
}
wg.Wait()
for i, err := range errs {
require.NoErrorf(t, err, "signer %s", signers[i])
}
for i := 1; i < len(sigs); i++ {
require.Equal(t, sigs[0].R, sigs[i].R, "every honest signer derives the identical signature")
require.Equal(t, sigs[0].S, sigs[i].S)
}
return sigs[0]
}
// A signing ceremony that cannot start is reported as a failed ceremony, not as
// a signature that failed to encode.
func TestASigningCeremonyThatCannotStartIsNamed(t *testing.T) {
pe := newExecutor(t)
dead, cancel := context.WithCancel(ctx())
cancel()
cfgs := realConfigs(t)
signers := make([]party.ID, 0, len(cfgs))
for id := range cfgs {
signers = append(signers, id)
}
signers = canonicalParties(signers)
net, routers := newMeshRouters(signers)
defer net.close()
_, err := pe.RunCMPSign(dead, "mpc/dead-sign", cfgs[signers[0]], signers, digestOf(1), routers[signers[0]])
require.ErrorContains(t, err, "mpc/dead-sign")
// And a ceremony whose start function refuses is reported before any round.
_, err = pe.RunCMPKeygen(ctx(), "mpc/no-self", "stranger", signers, 1, routers[signers[0]])
require.ErrorContains(t, err, "create ceremony handler")
}