Skip to content

Commit

Permalink
Merge pull request #11 from threshold-network/roundtrip-frost
Browse files Browse the repository at this point in the history
Roundtrip integration test for FROST
  • Loading branch information
eth-r authored Jan 10, 2024
2 parents 952dabd + 8a37f80 commit f173471
Show file tree
Hide file tree
Showing 5 changed files with 243 additions and 41 deletions.
13 changes: 13 additions & 0 deletions frost/coordinator.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,19 @@ type Coordinator struct {
Participant
}

// NewCoordinator creates a new [FROST] Coordinator instance.
func NewCoordinator(
ciphersuite Ciphersuite,
publicKey *Point,
) *Coordinator {
return &Coordinator{
Participant: Participant{
ciphersuite: ciphersuite,
publicKey: publicKey,
},
}
}

// Aggregate implements Signature Share Aggregation from [FROST], section
// 5.3. Signature Share Aggregation.
//
Expand Down
161 changes: 161 additions & 0 deletions frost/frost_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
package frost

import (
"crypto/rand"
"fmt"
"math/big"
"testing"

"threshold.network/roast/internal/testutils"
)

var ciphersuite = NewBip340Ciphersuite()
var threshold = 51
var groupSize = 100

func TestFrostRoundtrip(t *testing.T) {
message := []byte("For even the very wise cannot see all ends")

tests := map[string]struct {
numberOfSigners int
}{
"the entire group": {
numberOfSigners: groupSize,
},
"threshold of the group": {
numberOfSigners: threshold,
},
}

for testName, test := range tests {
t.Run(testName, func(t *testing.T) {
signers := createSigners(t)[:test.numberOfSigners]
publicKey := signers[0].publicKey

isSignatureValid := false
maxAttempts := 5

// From [BIP-340]:
// Let k' = int(rand) mod n[13].
// Fail if k' = 0.
// Let R = k'⋅G.
// Let k = k' if has_even_y(R), otherwise let k = n - k' .
//
// Although it is easy to address similar requirement for `d` by
// inverting the secret key earlier in the test, for `k` the
// situation is more complicated because it is generated by [FROST].
// For now, we just retry.
//
// See https://github.com/threshold-network/roast-go/issues/12
for i := 0; !isSignatureValid && i < maxAttempts; i++ {
nonces, commitments := executeRound1(t, signers)
signatureShares := executeRound2(t, signers, message, nonces, commitments)

coordinator := NewCoordinator(ciphersuite, publicKey)
signature, err := coordinator.Aggregate(message, commitments, signatureShares)
if err != nil {
t.Fatal(err)
}

isSignatureValid, err = ciphersuite.VerifySignature(
signature,
publicKey,
message,
)
if err != nil {
fmt.Printf(
"signature verification error on attempt [%v]: [%v]\n",
i,
err,
)
}
}

testutils.AssertBoolsEqual(
t,
"signature verification result",
true,
isSignatureValid,
)
})
}
}

func createSigners(t *testing.T) []*Signer {
curve := ciphersuite.Curve()
order := curve.Order()

secretKey, err := rand.Int(rand.Reader, order)
if err != nil {
t.Fatal(err)
}

publicKey := curve.EcBaseMul(secretKey)

// From [BIP-340]:
// Let d' = int(sk)
// Fail if d' = 0 or d' ≥ n
// Let P = d'⋅G
// Let d = d' if has_even_y(P), otherwise let d = n - d' .
if publicKey.Y.Bit(0) != 0 { // is Y even?
secretKey.Sub(order, secretKey)
publicKey = curve.EcBaseMul(secretKey)
}

keyShares := testutils.GenerateKeyShares(
secretKey,
groupSize,
threshold,
order,
)

signers := make([]*Signer, groupSize)

for i := 0; i < groupSize; i++ {
j := i + 1
signers[i] = NewSigner(ciphersuite, uint64(j), publicKey, keyShares[i])
}

return signers
}

func executeRound1(
t *testing.T,
signers []*Signer,
) ([]*Nonce, []*NonceCommitment) {
nonces := make([]*Nonce, len(signers))
commitments := make([]*NonceCommitment, len(signers))

for i, signer := range signers {
n, c, err := signer.Round1()
if err != nil {
t.Fatal(t)
}

nonces[i] = n
commitments[i] = c
}

return nonces, commitments
}

func executeRound2(
t *testing.T,
signers []*Signer,
message []byte,
nonces []*Nonce,
nonceCommitments []*NonceCommitment,
) []*big.Int {
signatureShares := make([]*big.Int, len(signers))

for i, signer := range signers {
signatureShare, err := signer.Round2(message, nonces[i], nonceCommitments)
if err != nil {
t.Fatal(t)
}

signatureShares[i] = signatureShare
}

return signatureShares
}
41 changes: 0 additions & 41 deletions frost/signer_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package frost

import (
"crypto/rand"
"fmt"
"math/big"
"slices"
Expand All @@ -10,9 +9,6 @@ import (
"threshold.network/roast/internal/testutils"
)

var ciphersuite = NewBip340Ciphersuite()
var groupSize = 100

func TestRound2_ValidationError(t *testing.T) {
// just a basic test checking if Round2 calls validateGroupCommitments
signers := createSigners(t)
Expand Down Expand Up @@ -147,40 +143,3 @@ func TestValidateGroupCommitments_Errors(t *testing.T) {
})
}
}

func createSigners(t *testing.T) []*Signer {
var signers []*Signer

// TODO: replace dummy secret key share with something real
buf := make([]byte, 32)
_, err := rand.Read(buf)
if err != nil {
t.Fatal(err)
}
secretKeyShare := new(big.Int).SetBytes(buf)

for i := 1; i <= groupSize; i++ {
// TODO: pass real public key instead of nil
signer := NewSigner(ciphersuite, uint64(i), nil, secretKeyShare)
signers = append(signers, signer)
}

return signers
}

func executeRound1(t *testing.T, signers []*Signer) ([]*Nonce, []*NonceCommitment) {
var nonces []*Nonce
var commitments []*NonceCommitment

for _, signer := range signers {
n, c, err := signer.Round1()
if err != nil {
t.Fatal(t)
}

nonces = append(nonces, n)
commitments = append(commitments, c)
}

return nonces, commitments
}
File renamed without changes.
69 changes: 69 additions & 0 deletions internal/testutils/shamir.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package testutils

import (
"crypto/rand"
"math/big"
)

// GenerateKeyShares generates a secret key and secret key shares for the group
// of the given size with the required signing threshold.
func GenerateKeyShares(
secretKey *big.Int,
groupSize int,
threshold int,
order *big.Int,
) []*big.Int {
coefficients := generatePolynomial(secretKey, threshold, order)

secretKeyShares := make([]*big.Int, groupSize)
for i := 0; i < groupSize; i++ {
j := i + 1
secretKeyShares[i] = calculatePolynomial(
coefficients,
j,
order,
)
}

return secretKeyShares
}

// generatePolynomial generates a polynomial of degree equal to `threshold` with
// random coefficients, not higher than the group `order`.
func generatePolynomial(
secretKey *big.Int,
threshold int,
order *big.Int,
) []*big.Int {
arr := make([]*big.Int, threshold)
arr[0] = secretKey
for i := 1; i < threshold; i++ {
random, err := rand.Int(rand.Reader, order)
if err != nil {
panic(err)
}
arr[i] = random
}

return arr
}

// calculatePolynomial calculates the polynomial value for the given `x` modulo
// group `order`. Polynomial `coefficients` need to be passed as parameters.
func calculatePolynomial(
coefficients []*big.Int,
x int,
order *big.Int,
) *big.Int {
result := new(big.Int)

bigX := big.NewInt(int64(x))

for i, c := range coefficients {
tmp := new(big.Int).Exp(bigX, big.NewInt(int64(i)), order)
tmp.Mul(tmp, c)
result.Add(result, tmp)
}

return new(big.Int).Mod(result, order)
}

0 comments on commit f173471

Please sign in to comment.