Skip to content

Commit

Permalink
Merge pull request #5 from threshold-network/frost-2
Browse files Browse the repository at this point in the history
Elliptic curve point serialization and FROST group commitment encoding
  • Loading branch information
eth-r authored Dec 18, 2023
2 parents 40f49d7 + 3ce2b26 commit d3ba588
Show file tree
Hide file tree
Showing 6 changed files with 468 additions and 10 deletions.
41 changes: 39 additions & 2 deletions frost/bip340.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,43 @@ func (bc *Bip340Curve) EcBaseMul(k *big.Int) *Point {
return &Point{gs_x, gs_y}
}

// IsNotIdentity validates if the point lies on the curve and is not an identity
// element.
func (bc *Bip340Curve) IsNotIdentity(p *Point) bool {
return bc.IsOnCurve(p.X, p.Y)
}

// SerializedPointLength returns the byte length of a serialized curve point.
func (b *Bip340Curve) SerializedPointLength() int {
// From the Marshal() function of secp256k1 go-ethereum implementation:
// byteLen := (BitCurve.BitSize + 7) >> 3
// ret := make([]byte, 1+2*byteLen)
return 65
}

// SerializePoint serializes the provided elliptic curve point to bytes.
// The slice length is equal to SerializedPointLength().
func (b *Bip340Curve) SerializePoint(p *Point) []byte {
// Note that secp256k1 implementation uses a fixed length of
// (BitCurve.BitSize + 7) >> 3
return b.Marshal(p.X, p.Y)
}

// DeserializePoint deserializes byte slice to an elliptic curve point. The
// byte slice length must be equal to SerializedPointLength(). Otherwise,
// the function returns nil.
func (b *Bip340Curve) DeserializePoint(bytes []byte) *Point {

// TODO: validate if point is on the curve

x, y := b.Unmarshal(bytes)
if x == nil || y == nil {
return nil
}

return &Point{x, y}
}

// H1 is the implementation of H1(m) function from [FROST].
func (b *Bip340Ciphersuite) H1(m []byte) *big.Int {
// From [FROST], we know the tag should be DST = contextString || "rho".
Expand Down Expand Up @@ -68,15 +105,15 @@ func (b *Bip340Ciphersuite) H3(m []byte, ms ...[]byte) *big.Int {
}

// H4 is the implementation of H4(m) function from [FROST].
func (b *Bip340Ciphersuite) H4(m []byte, ms ...[]byte) []byte {
func (b *Bip340Ciphersuite) H4(m []byte) []byte {
// From [FROST], we know the tag should be DST = contextString || "msg".
dst := concat(b.contextString(), []byte("msg"))
hash := b.hash(dst, m)
return hash[:]
}

// H5 is the implementation of H5(m) function from [FROST].
func (b *Bip340Ciphersuite) H5(m []byte, ms ...[]byte) []byte {
func (b *Bip340Ciphersuite) H5(m []byte) []byte {
// From [FROST], we know the tag should be DST = contextString || "com".
dst := concat(b.contextString(), []byte("com"))
hash := b.hash(dst, m)
Expand Down
76 changes: 69 additions & 7 deletions frost/bip340_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,69 @@ import (
"threshold.network/roast/internal/testutils"
)

func Test_Bip340Ciphersuite_H1(t *testing.T) {
func TestBip340CurveSerializedPointLength(t *testing.T) {
curve := NewBip340Ciphersuite().Curve()

point := curve.EcBaseMul(big.NewInt(1119991111222))

actual := len(curve.SerializePoint(point))
expected1 := curve.SerializedPointLength()
expected2 := 65 // double-checking in case the underlying implementation changes

testutils.AssertIntsEqual(t, "byte length", expected1, actual)
testutils.AssertIntsEqual(t, "byte length", expected2, actual)
}

func TestBip340CurveSerializeDeserializePoint(t *testing.T) {
curve := NewBip340Ciphersuite().Curve()

point := curve.EcBaseMul(big.NewInt(1337))

serialized := curve.SerializePoint(point)
deserialized := curve.DeserializePoint(serialized)

testutils.AssertBigIntsEqual(t, "X coordinate", point.X, deserialized.X)
testutils.AssertBigIntsEqual(t, "Y coordinate", point.Y, deserialized.Y)
}

func TestBip340CurveDeserialize(t *testing.T) {
// The happy path is covered by TestBip340CurveSerializeDeserializePoint.
// Let's cover the negative path.

curve := NewBip340Ciphersuite().Curve()
point := curve.EcBaseMul(big.NewInt(10))

serialized := curve.SerializePoint(point)

var tests = map[string]struct {
input []byte
}{
"nil": {
input: nil,
},
"empty": {
input: []byte{},
},
"one less than expected": {
input: serialized[:len(serialized)-1],
},
"one more than expected": {
input: append(serialized, 0x1),
},
}

for testName, test := range tests {
t.Run(testName, func(t *testing.T) {
result := curve.DeserializePoint(test.input)
if result != nil {
t.Fatalf("nil result expected, got: [%v]", result)
}
})
}

}

func TestBip340CiphersuiteH1(t *testing.T) {
// There are no official test vectors available. Yet, we want to ensure the
// function does not panic for empty or nil. We also want to make sure the
// happy path works producing a non-zero value.
Expand All @@ -35,7 +97,7 @@ func Test_Bip340Ciphersuite_H1(t *testing.T) {
}
}

func Test_Bip340Ciphersuite_H2(t *testing.T) {
func TestBip340CiphersuiteH2(t *testing.T) {
// There are no official test vectors available. Yet, we want to ensure the
// function does not panic for empty or nil. We also want to make sure the
// happy path works producing a non-zero value.
Expand Down Expand Up @@ -75,7 +137,7 @@ func Test_Bip340Ciphersuite_H2(t *testing.T) {
}
}

func Test_Bip340Ciphersuite_H3(t *testing.T) {
func TestBip340CiphersuiteH3(t *testing.T) {
// There are no official test vectors available. Yet, we want to ensure the
// function does not panic for empty or nil. We also want to make sure the
// happy path works producing a non-zero value.
Expand Down Expand Up @@ -114,7 +176,7 @@ func Test_Bip340Ciphersuite_H3(t *testing.T) {
}
}

func Test_Bip340Ciphersuite_H4(t *testing.T) {
func TestBip340CiphersuiteH4(t *testing.T) {
// There are no official test vectors available. Yet, we want to ensure the
// function does not panic for empty or nil. We also want to make sure the
// happy path works producing a non-zero value.
Expand Down Expand Up @@ -144,7 +206,7 @@ func Test_Bip340Ciphersuite_H4(t *testing.T) {
}
}

func Test_Bip340Ciphersuite_H5(t *testing.T) {
func TestBip340CiphersuiteH5(t *testing.T) {
// There are no official test vectors available. Yet, we want to ensure the
// function does not panic for empty or nil. We also want to make sure the
// happy path works producing a non-zero value.
Expand Down Expand Up @@ -174,7 +236,7 @@ func Test_Bip340Ciphersuite_H5(t *testing.T) {
}
}

func Test_Bip340Ciphersuite_hashToScalar(t *testing.T) {
func TestBip340CiphersuiteHashToScalar(t *testing.T) {
var tests = map[string]struct {
tag []byte
msg []byte
Expand Down Expand Up @@ -223,7 +285,7 @@ func Test_Bip340Ciphersuite_hashToScalar(t *testing.T) {
}
}

func Test_Bip340Ciphersuite_hash(t *testing.T) {
func TestBip340CiphersuiteHash(t *testing.T) {
var tests = map[string]struct {
tag []byte
msg []byte
Expand Down
32 changes: 31 additions & 1 deletion frost/ciphersuite.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package frost

import "math/big"
import (
"fmt"
"math/big"
)

// Ciphersuite interface abstracts out the particular ciphersuite implementation
// used for the [FROST] protocol execution. This is a strategy design pattern
Expand Down Expand Up @@ -30,11 +33,38 @@ type Hashing interface {
// Curve interface abstracts out the particular elliptic curve implementation
// specific to the ciphersuite used.
type Curve interface {
// EcBaseMul returns k*G, where G is the base point of the group.
EcBaseMul(*big.Int) *Point

// IsNotIdentity validates if the point lies on the curve and is not an
// identity element.
IsNotIdentity(*Point) bool

// SerializedPointLength returns the byte length of a serialized curve point.
// The value is specific to the implementation. It is expected that the
// SerializePoint function always return a slice of this length and the
// DeserializePoint can only deserialize byte slice of this length.
SerializedPointLength() int

// SerializePoint serializes the provided elliptic curve point to bytes.
// The byte slice returned must always have a length equal to
// SerializedPointLength().
SerializePoint(*Point) []byte

// DeserializePoint deserializes byte slice to an elliptic curve point. The
// byte slice length must be equal to SerializedPointLength(). Otherwise,
// the function returns nil.
DeserializePoint([]byte) *Point
}

// Point represents a valid point on the Curve.
type Point struct {
X *big.Int // the X coordinate of the point
Y *big.Int // the Y coordinate of the point
}

// String transforms Point structure into a string so that it can be used
// in logging.
func (p *Point) String() string {
return fmt.Sprintf("Point[X=0x%v, Y=0x%v]", p.X.Text(16), p.Y.Text(16))
}
139 changes: 139 additions & 0 deletions frost/signer.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package frost

import (
"crypto/rand"
"encoding/binary"
"fmt"
"math/big"
)
Expand All @@ -27,6 +28,8 @@ type NonceCommitment struct {
bindingNonceCommitment *Point
}

// Round1 implements the Round One - Commitment phase from [FROST], section
// 5.1. Round One - Commitment.
func (s *Signer) Round1() (*Nonce, *NonceCommitment, error) {
// From [FROST]:
//
Expand Down Expand Up @@ -84,3 +87,139 @@ func (s *Signer) generateNonce(secret []byte) (*big.Int, error) {
// return H3(random_bytes || secret_enc)
return s.ciphersuite.H3(b, secret), nil
}

// encodeGroupCommitment implements def encode_group_commitment_list(commitment_list)
// function from [FROST], as defined in section 4.3. List Operations.
//
// The function calling encodeGroupCommitment must ensure a valid number of
// commitments have been received.
func (s *Signer) encodeGroupCommitment(commitments []*NonceCommitment) ([]byte, []error) {
// From [FROST]:
//
// 4.3. List Operations
//
// This section describes helper functions that work on lists of values
// produced during the FROST protocol. The following function encodes a
// list of participant commitments into a byte string for use in the
// FROST protocol.
//
// Inputs:
// - commitment_list = [(i, hiding_nonce_commitment_i,
// binding_nonce_commitment_i), ...], a list of commitments issued by
// each participant, where each element in the list indicates a
// NonZeroScalar identifier i and two commitment Element values
// (hiding_nonce_commitment_i, binding_nonce_commitment_i). This list
// MUST be sorted in ascending order by identifier.
//
// Outputs:
// - encoded_group_commitment, the serialized representation of
// commitment_list, a byte string.
//
// def encode_group_commitment_list(commitment_list):

// perform validations early to extract complexity out of the loop
// constructing encoded_group_commitment
validationErrors := s.validateGroupCommitment(commitments)
if len(validationErrors) != 0 {
return nil, validationErrors
}

curve := s.ciphersuite.Curve()
ecPointLength := curve.SerializedPointLength()

// preallocate the necessary space to avoid waste:
// 8 bytes for signerIndex (uint64)
// ecPointLength for hidingNonceCommitment
// ecPointLength for bindingNonceCommitment
b := make([]byte, 0, (8+2*ecPointLength)*len(commitments))

// encoded_group_commitment = nil
// for (identifier, hiding_nonce_commitment,
// binding_nonce_commitment) in commitment_list:
for _, c := range commitments {
// encoded_commitment = (
// G.SerializeScalar(identifier) ||
// G.SerializeElement(hiding_nonce_commitment) ||
// G.SerializeElement(binding_nonce_commitment))
// encoded_group_commitment = (
// encoded_group_commitment ||
// encoded_commitment)
b = binary.BigEndian.AppendUint64(b, c.signerIndex)
b = append(b, curve.SerializePoint(c.hidingNonceCommitment)...)
b = append(b, curve.SerializePoint(c.bindingNonceCommitment)...)
}

// return encoded_group_commitment
return b, nil
}

// validateGroupCommitment is a helper function used internally by
// encodeGroupCommitment to validate the group commitments. Two validations are
// done:
// - None of the commitments is the identity element of the curve.
// - The list of commitments is sorted in ascending order by signer identifier.
func (s *Signer) validateGroupCommitment(commitments []*NonceCommitment) []error {
// From [FROST]:
//
// 3.1 Prime-Order Group
//
// (...)
//
// SerializeElement(A): Maps an Element A to a canonical byte array
// buf of fixed length Ne. This function raises an error if A is the
// identity element of the group.
//
// 4.3. List Operations
//
// (...)
//
// commitment_list = [(i, hiding_nonce_commitment_i,
// binding_nonce_commitment_i), ...], a list of commitments issued by
// each participant, where each element in the list indicates a
// NonZeroScalar identifier i and two commitment Element values
// (hiding_nonce_commitment_i, binding_nonce_commitment_i). This list
// MUST be sorted in ascending order by identifier.
var errors []error

curve := s.ciphersuite.Curve()

// we index from 1 so this number will always be lower
lastSignerIndex := uint64(0)

for i, c := range commitments {
if c.signerIndex <= lastSignerIndex {
errors = append(
errors, fmt.Errorf(
"commitments not sorted in ascending order: "+
"commitments[%v].signerIndex=%v, commitments[%v].signerIndex=%v",
i-1,
lastSignerIndex,
i,
c.signerIndex,
),
)
}

lastSignerIndex = c.signerIndex

if !curve.IsNotIdentity(c.bindingNonceCommitment) {
errors = append(errors, fmt.Errorf(
"binding nonce commitment from signer [%v] is not a valid "+
"non-identity point on the curve: [%s]",
c.signerIndex,
c.bindingNonceCommitment,
))
}

if !curve.IsNotIdentity(c.hidingNonceCommitment) {
errors = append(errors, fmt.Errorf(
"hiding nonce commitment from signer [%v] is not a valid "+
"non-identity point on the curve: [%s]",
c.signerIndex,
c.hidingNonceCommitment,
))
}
}

return errors
}
Loading

0 comments on commit d3ba588

Please sign in to comment.