Files
tss/ecdsa/keygen/local_party_test.go
2025-08-02 15:20:14 -05:00

366 lines
11 KiB
Go

// Copyright © 2019 Binance
//
// This file is part of Binance. The full Binance copyright notice, including
// terms governing use, modification, and redistribution, is contained in the
// file LICENSE at the root of the source code distribution tree.
package keygen
import (
"crypto/ecdsa"
"crypto/rand"
"encoding/json"
"fmt"
"math/big"
"os"
"runtime"
"sync/atomic"
"testing"
"github.com/ipfs/go-log"
"github.com/stretchr/testify/assert"
"github.com/luxfi/tss/v2/common"
"github.com/luxfi/tss/v2/crypto"
"github.com/luxfi/tss/v2/crypto/dlnproof"
"github.com/luxfi/tss/v2/crypto/paillier"
"github.com/luxfi/tss/v2/crypto/vss"
"github.com/luxfi/tss/v2/test"
"github.com/luxfi/tss/v2/tss"
)
const (
testParticipants = TestParticipants
testThreshold = TestThreshold
)
func setUp(level string) {
if err := log.SetLogLevel("tss-lib", level); err != nil {
panic(err)
}
}
func TestStartRound1Paillier(t *testing.T) {
setUp("debug")
pIDs := tss.GenerateTestPartyIDs(1)
p2pCtx := tss.NewPeerContext(pIDs)
threshold := 1
params := tss.NewParameters(tss.EC(), p2pCtx, pIDs[0], len(pIDs), threshold)
fixtures, pIDs, err := LoadKeygenTestFixtures(testParticipants)
if err != nil {
common.Logger.Info("No test fixtures were found, so the safe primes will be generated from scratch. This may take a while...")
pIDs = tss.GenerateTestPartyIDs(testParticipants)
}
var lp *LocalParty
out := make(chan tss.Message, len(pIDs))
if 0 < len(fixtures) {
lp = NewLocalParty(params, out, nil, fixtures[0].LocalPreParams).(*LocalParty)
} else {
lp = NewLocalParty(params, out, nil).(*LocalParty)
}
if err := lp.Start(); err != nil {
assert.FailNow(t, err.Error())
}
<-out
// Paillier modulus 2048 (two 1024-bit primes)
// round up to 256, it was used to be flaky, sometimes comes back with 1 byte less
len1 := len(lp.data.PaillierSK.LambdaN.Bytes())
len2 := len(lp.data.PaillierSK.PublicKey.N.Bytes())
if len1%2 != 0 {
len1 = len1 + (256 - (len1 % 256))
}
if len2%2 != 0 {
len2 = len2 + (256 - (len2 % 256))
}
assert.Equal(t, 2048/8, len1)
assert.Equal(t, 2048/8, len2)
}
func TestFinishAndSaveH1H2(t *testing.T) {
setUp("debug")
pIDs := tss.GenerateTestPartyIDs(1)
p2pCtx := tss.NewPeerContext(pIDs)
threshold := 1
params := tss.NewParameters(tss.EC(), p2pCtx, pIDs[0], len(pIDs), threshold)
fixtures, pIDs, err := LoadKeygenTestFixtures(testParticipants)
if err != nil {
common.Logger.Info("No test fixtures were found, so the safe primes will be generated from scratch. This may take a while...")
pIDs = tss.GenerateTestPartyIDs(testParticipants)
}
var lp *LocalParty
out := make(chan tss.Message, len(pIDs))
if 0 < len(fixtures) {
lp = NewLocalParty(params, out, nil, fixtures[0].LocalPreParams).(*LocalParty)
} else {
lp = NewLocalParty(params, out, nil).(*LocalParty)
}
if err := lp.Start(); err != nil {
assert.FailNow(t, err.Error())
}
// RSA modulus 2048 (two 1024-bit primes)
// round up to 256
len1 := len(lp.data.H1j[0].Bytes())
len2 := len(lp.data.H2j[0].Bytes())
len3 := len(lp.data.NTildej[0].Bytes())
if len1%2 != 0 {
len1 = len1 + (256 - (len1 % 256))
}
if len2%2 != 0 {
len2 = len2 + (256 - (len2 % 256))
}
if len3%2 != 0 {
len3 = len3 + (256 - (len3 % 256))
}
// 256 bytes = 2048 bits
assert.Equal(t, 256, len1, "h1 should be correct len")
assert.Equal(t, 256, len2, "h2 should be correct len")
assert.Equal(t, 256, len3, "n-tilde should be correct len")
assert.NotZero(t, lp.data.H1i, "h1 should be non-zero")
assert.NotZero(t, lp.data.H2i, "h2 should be non-zero")
assert.NotZero(t, lp.data.NTildei, "n-tilde should be non-zero")
}
func TestBadMessageCulprits(t *testing.T) {
setUp("debug")
pIDs := tss.GenerateTestPartyIDs(2)
p2pCtx := tss.NewPeerContext(pIDs)
params := tss.NewParameters(tss.S256(), p2pCtx, pIDs[0], len(pIDs), 1)
fixtures, pIDs, err := LoadKeygenTestFixtures(testParticipants)
if err != nil {
common.Logger.Info("No test fixtures were found, so the safe primes will be generated from scratch. This may take a while...")
pIDs = tss.GenerateTestPartyIDs(testParticipants)
}
var lp *LocalParty
out := make(chan tss.Message, len(pIDs))
if 0 < len(fixtures) {
lp = NewLocalParty(params, out, nil, fixtures[0].LocalPreParams).(*LocalParty)
} else {
lp = NewLocalParty(params, out, nil).(*LocalParty)
}
if err := lp.Start(); err != nil {
assert.FailNow(t, err.Error())
}
badMsg, _ := NewKGRound1Message(pIDs[1], zero, &paillier.PublicKey{N: zero}, zero, zero, zero, new(dlnproof.Proof), new(dlnproof.Proof))
ok, err2 := lp.Update(badMsg)
t.Log(err2)
assert.False(t, ok)
if !assert.Error(t, err2) {
return
}
assert.Equal(t, 1, len(err2.Culprits()))
assert.Equal(t, pIDs[1], err2.Culprits()[0])
assert.Equal(t,
"task ecdsa-keygen, party {0,P[1]}, round 1, culprits [{1,2}]: message failed ValidateBasic: Type: binance.tsslib.ecdsa.keygen.KGRound1Message, From: {1,2}, To: all",
err2.Error())
}
func TestE2EConcurrentAndSaveFixtures(t *testing.T) {
setUp("info")
// tss.SetCurve(elliptic.P256())
threshold := testThreshold
fixtures, pIDs, err := LoadKeygenTestFixtures(testParticipants)
if err != nil {
common.Logger.Info("No test fixtures were found, so the safe primes will be generated from scratch. This may take a while...")
pIDs = tss.GenerateTestPartyIDs(testParticipants)
}
p2pCtx := tss.NewPeerContext(pIDs)
parties := make([]*LocalParty, 0, len(pIDs))
errCh := make(chan *tss.Error, len(pIDs))
outCh := make(chan tss.Message, len(pIDs))
endCh := make(chan *LocalPartySaveData, len(pIDs))
updater := test.SharedPartyUpdater
startGR := runtime.NumGoroutine()
// init the parties
for i := 0; i < len(pIDs); i++ {
var P *LocalParty
params := tss.NewParameters(tss.S256(), p2pCtx, pIDs[i], len(pIDs), threshold)
// do not use in untrusted setting
params.SetNoProofMod()
// do not use in untrusted setting
params.SetNoProofFac()
if i < len(fixtures) {
P = NewLocalParty(params, outCh, endCh, fixtures[i].LocalPreParams).(*LocalParty)
} else {
P = NewLocalParty(params, outCh, endCh).(*LocalParty)
}
parties = append(parties, P)
go func(P *LocalParty) {
if err := P.Start(); err != nil {
errCh <- err
}
}(P)
}
// PHASE: keygen
var ended int32
keygen:
for {
fmt.Printf("ACTIVE GOROUTINES: %d\n", runtime.NumGoroutine())
select {
case err := <-errCh:
common.Logger.Errorf("Error: %s", err)
assert.FailNow(t, err.Error())
break keygen
case msg := <-outCh:
dest := msg.GetTo()
if dest == nil { // broadcast!
for _, P := range parties {
if P.PartyID().Index == msg.GetFrom().Index {
continue
}
go updater(P, msg, errCh)
}
} else { // point-to-point!
if dest[0].Index == msg.GetFrom().Index {
t.Fatalf("party %d tried to send a message to itself (%d)", dest[0].Index, msg.GetFrom().Index)
return
}
go updater(parties[dest[0].Index], msg, errCh)
}
case save := <-endCh:
// SAVE a test fixture file for this P (if it doesn't already exist)
// .. here comes a workaround to recover this party's index (it was removed from save data)
index, err := save.OriginalIndex()
assert.NoErrorf(t, err, "should not be an error getting a party's index from save data")
tryWriteTestFixtureFile(t, index, *save)
atomic.AddInt32(&ended, 1)
if atomic.LoadInt32(&ended) == int32(len(pIDs)) {
t.Logf("Done. Received save data from %d participants", ended)
// combine shares for each Pj to get u
u := new(big.Int)
for j, Pj := range parties {
pShares := make(vss.Shares, 0)
for _, P := range parties {
vssMsgs := P.temp.kgRound2Message1s
share := vssMsgs[j].Content().(*KGRound2Message1).Share
shareStruct := &vss.Share{
Threshold: threshold,
ID: P.PartyID().KeyInt(),
Share: new(big.Int).SetBytes(share),
}
pShares = append(pShares, shareStruct)
}
uj, err := pShares[:threshold+1].ReConstruct(tss.S256())
assert.NoError(t, err, "vss.ReConstruct should not throw error")
// uG test: u*G[j] == V[0]
assert.Equal(t, uj, Pj.temp.ui)
uG := crypto.ScalarBaseMult(tss.EC(), uj)
assert.True(t, uG.Equals(Pj.temp.vs[0]), "ensure u*G[j] == V_0")
// xj tests: BigXj == xj*G
xj := Pj.data.Xi
gXj := crypto.ScalarBaseMult(tss.EC(), xj)
BigXj := Pj.data.BigXj[j]
assert.True(t, BigXj.Equals(gXj), "ensure BigX_j == g^x_j")
// fails if threshold cannot be satisfied (bad share)
{
badShares := pShares[:threshold]
badShares[len(badShares)-1].Share.Set(big.NewInt(0))
uj, err := pShares[:threshold].ReConstruct(tss.S256())
assert.NoError(t, err)
assert.NotEqual(t, parties[j].temp.ui, uj)
BigXjX, BigXjY := tss.EC().ScalarBaseMult(uj.Bytes())
assert.NotEqual(t, BigXjX, Pj.temp.vs[0].X())
assert.NotEqual(t, BigXjY, Pj.temp.vs[0].Y())
}
u = new(big.Int).Add(u, uj)
}
// build ecdsa key pair
pkX, pkY := save.ECDSAPub.X(), save.ECDSAPub.Y()
pk := ecdsa.PublicKey{
Curve: tss.EC(),
X: pkX,
Y: pkY,
}
sk := ecdsa.PrivateKey{
PublicKey: pk,
D: u,
}
// test pub key, should be on curve and match pkX, pkY
assert.True(t, sk.IsOnCurve(pkX, pkY), "public key must be on curve")
// public key tests
assert.NotZero(t, u, "u should not be zero")
ourPkX, ourPkY := tss.EC().ScalarBaseMult(u.Bytes())
assert.Equal(t, pkX, ourPkX, "pkX should match expected pk derived from u")
assert.Equal(t, pkY, ourPkY, "pkY should match expected pk derived from u")
t.Log("Public key tests done.")
// make sure everyone has the same ECDSA public key
for _, Pj := range parties {
assert.Equal(t, pkX, Pj.data.ECDSAPub.X())
assert.Equal(t, pkY, Pj.data.ECDSAPub.Y())
}
t.Log("Public key distribution test done.")
// test sign/verify
data := make([]byte, 32)
for i := range data {
data[i] = byte(i)
}
r, s, err := ecdsa.Sign(rand.Reader, &sk, data)
assert.NoError(t, err, "sign should not throw an error")
ok := ecdsa.Verify(&pk, data, r, s)
assert.True(t, ok, "signature should be ok")
t.Log("ECDSA signing test done.")
t.Logf("Start goroutines: %d, End goroutines: %d", startGR, runtime.NumGoroutine())
break keygen
}
}
}
}
func tryWriteTestFixtureFile(t *testing.T, index int, data LocalPartySaveData) {
fixtureFileName := makeTestFixtureFilePath(index)
// fixture file does not already exist?
// if it does, we won't re-create it here
fi, err := os.Stat(fixtureFileName)
if !(err == nil && fi != nil && !fi.IsDir()) {
fd, err := os.OpenFile(fixtureFileName, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
assert.NoErrorf(t, err, "unable to open fixture file %s for writing", fixtureFileName)
}
bz, err := json.Marshal(&data)
if err != nil {
t.Fatalf("unable to marshal save data for fixture file %s", fixtureFileName)
}
_, err = fd.Write(bz)
if err != nil {
t.Fatalf("unable to write to fixture file %s", fixtureFileName)
}
t.Logf("Saved a test fixture file for party %d: %s", index, fixtureFileName)
} else {
t.Logf("Fixture file already exists for party %d; not re-creating: %s", index, fixtureFileName)
}
//
}