Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ require (
github.com/ccoveille/go-safecast/v2 v2.0.1
github.com/cenkalti/backoff/v4 v4.3.0
github.com/cespare/xxhash/v2 v2.3.0
github.com/cloudflare/circl v1.6.3
github.com/cloudflare/circl v1.6.5
github.com/consensys/gnark v0.16.3
github.com/consensys/gnark-crypto v0.21.0
github.com/coocood/freecache v1.2.7
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyY
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
github.com/cloudflare/circl v1.6.5 h1:O64F26HEqNhznd/hrC5KZXVKYuKM2rx4deZDTc4ihQA=
github.com/cloudflare/circl v1.6.5/go.mod h1:h5LNyxAc5nTue9DS5jT+48en2PSDYt3zdGnz5OstK6c=
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
github.com/consensys/gnark v0.16.3 h1:S7BtIQSX2WLHV2857HrLmrQ5xIl0ZRL8kT6rcLn8gow=
github.com/consensys/gnark v0.16.3/go.mod h1:ChMGCGi8KztMtuQXgxprorLVJY29FPnKkjN19RXB/KU=
Expand Down
4 changes: 2 additions & 2 deletions itests/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ const (
goContainerName = "go-node"

ScalaImageRepository = "wavesplatform/wavesnode"
ScalaImageTag = "1.6.4"
goImageRepository = "go-node"
DefaultImageTag = "1.6.4"
)

const (
Expand Down Expand Up @@ -144,7 +144,7 @@ func (c *ScalaConfigurator) DockerRunOptions() *dockertest.RunOptions {
}

if c.imageTag == "" {
c.imageTag = DefaultImageTag
c.imageTag = ScalaImageTag
}

kps := new(strings.Builder)
Expand Down
2 changes: 1 addition & 1 deletion itests/fixtures/base_fixtures.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ type BaseSuite struct {
}

func (suite *BaseSuite) BaseSetup(options ...config.BlockchainOption) {
suite.BaseSetupWithImages(config.ScalaImageRepository, config.DefaultImageTag, options...)
suite.BaseSetupWithImages(config.ScalaImageRepository, config.ScalaImageTag, options...)
}

func (suite *BaseSuite) BaseSetupWithImages(scalaRepository, scalaTag string,
Expand Down
4 changes: 2 additions & 2 deletions itests/init_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,8 @@ func testsSetup() error {
slog.Info("Pulling scala-node image", "platform", platform)
if plErr := pool.Client.PullImage(
dc.PullImageOptions{
Repository: "wavesplatform/wavesnode",
Tag: "latest",
Repository: config.ScalaImageRepository,
Tag: config.ScalaImageTag,
Platform: platform,
},
dc.AuthConfiguration{}); plErr != nil {
Expand Down
56 changes: 42 additions & 14 deletions pkg/crypto/bls/bls.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"fmt"
"strings"

"github.com/cloudflare/circl/ecc/bls12381"
cbls "github.com/cloudflare/circl/sign/bls"
"github.com/mr-tron/base58"

Expand All @@ -23,8 +24,11 @@ const (
)

var (
ErrNoSignatures = errors.New("no signatures")
ErrDuplicateSignature = errors.New("duplicate signature")
ErrNoSignatures = errors.New("no signatures")
ErrDuplicateSignature = errors.New("duplicate signature")
ErrNoPublicKeys = errors.New("no public keys")
ErrDuplicatePublicKey = errors.New("duplicate public key")
ErrIdentityAggregatedPublicKey = errors.New("aggregated public key is the identity")
)

// SecretKey is 32-byte BLS secret key.
Expand Down Expand Up @@ -266,25 +270,49 @@ func AggregateSignatures(signatures []Signature) (Signature, error) {
return NewSignatureFromBytes(aggregate)
}

// VerifyAggregate verifies aggregated signature over the same message.
func VerifyAggregate(pks []PublicKey, msg []byte, sig Signature) bool {
// AggregatePublicKeys combines the given public keys into a single aggregated public key by adding up the
// corresponding points of G1. Every key is validated (correct encoding, on-curve, in the correct subgroup and
// not the identity) while being deserialized. Duplicate keys are rejected.
func AggregatePublicKeys(pks []PublicKey) (PublicKey, error) {
if len(pks) == 0 {
return false
return PublicKey{}, ErrNoPublicKeys
}
if !isUnique(pks) {
return false
return PublicKey{}, ErrDuplicatePublicKey
}
ks := make([]*cbls.PublicKey[cbls.G1], len(pks))
ms := make([][]byte, len(pks))
var agg bls12381.G1
agg.SetIdentity()
for i := range pks {
k := new(cbls.PublicKey[cbls.G1])
if err := k.UnmarshalBinary(pks[i].Bytes()); err != nil {
return false
var p bls12381.G1
if err := p.SetBytes(pks[i].Bytes()); err != nil { // SetBytes rejects points outside of the G1 subgroup.
return PublicKey{}, fmt.Errorf("failed to aggregate public keys: invalid public key #%d: %w", i, err)
}
ks[i] = k
ms[i] = msg
if p.IsIdentity() {
return PublicKey{}, fmt.Errorf("failed to aggregate public keys: public key #%d is the identity", i)
}
agg.Add(&agg, &p)
}
if agg.IsIdentity() { // Aggregation of valid keys can still cancel out to the identity, such a key is unusable.
return PublicKey{}, ErrIdentityAggregatedPublicKey
}
return NewPublicKeyFromBytes(agg.BytesCompressed())
}

// VerifyAggregate verifies aggregated signature over the same message.
// This is the FastAggregateVerify algorithm of the BLS signature scheme with proof of possession: the public keys
// are aggregated into a single one, and the aggregated signature is verified against it.
// Security note: aggregation of public keys is only safe against rogue public key attacks if every key was proven
// to be possessed by its owner, see VerifyPoP. Keys passed here MUST have been registered with a valid PoP.
func VerifyAggregate(pks []PublicKey, msg []byte, sig Signature) bool {
apk, err := AggregatePublicKeys(pks)
if err != nil {
return false
}
ok, err := Verify(apk, msg, sig)
if err != nil {
return false
}
return cbls.VerifyAggregate[cbls.G1](ks, ms, sig.Bytes())
return ok
}

func isUnique[T comparable](in []T) bool {
Expand Down
136 changes: 136 additions & 0 deletions pkg/crypto/bls/bls_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,142 @@ func TestAggregateFromWavesSecrets_SameMessage(t *testing.T) {
require.False(t, ok, "aggregate must fail on different message")
}

// generateKeyPairs creates n BLS key pairs from random Waves secret keys.
func generateKeyPairs(t *testing.T, n int) ([]bls.SecretKey, []bls.PublicKey) {
t.Helper()
sks := make([]bls.SecretKey, n)
pks := make([]bls.PublicKey, n)
for i := range n {
sk, err := secretKeyFromWavesSecretKey(randWavesSK(t))
require.NoError(t, err)
pk, err := sk.PublicKey()
require.NoError(t, err)
sks[i], pks[i] = sk, pk
}
return sks, pks
}

// signAll signs the given message with every given secret key.
func signAll(t *testing.T, sks []bls.SecretKey, msg []byte) []bls.Signature {
t.Helper()
sigs := make([]bls.Signature, len(sks))
for i, sk := range sks {
sig, err := bls.Sign(sk, msg)
require.NoError(t, err)
sigs[i] = sig
}
return sigs
}

// TestAggregatePublicKeys_SameMessage checks that an aggregated public key acts as an ordinary public key of the
// group of signers: every signer signs the same message, each signature is verified separately by the corresponding
// public key, and the aggregated signature is verified by the aggregated public key. Aggregating a public key of a
// signer who did not sign, or leaving out a signer who did, must break the verification.
func TestAggregatePublicKeys_SameMessage(t *testing.T) {
const n = 4
msg := []byte("aggregated public key test")

sks, pks := generateKeyPairs(t, n+1) // The last key pair is an outsider, it doesn't sign the message.
signers, signerPKs := sks[:n], pks[:n]
outsiderSK, outsiderPK := sks[n], pks[n]

// Every signature is valid for its own public key and invalid for the public key of another signer.
sigs := signAll(t, signers, msg)
for i := range signers {
ok, err := bls.Verify(signerPKs[i], msg, sigs[i])
require.NoError(t, err)
assert.True(t, ok, "signature %d must be valid for its own public key", i)

ok, err = bls.Verify(signerPKs[(i+1)%n], msg, sigs[i])
require.NoError(t, err)
assert.False(t, ok, "signature %d must be invalid for the public key of another signer", i)
}

// The aggregated signature is valid for the aggregated public key of all the signers.
aggSig, err := bls.AggregateSignatures(sigs)
require.NoError(t, err)
aggPK, err := bls.AggregatePublicKeys(signerPKs)
require.NoError(t, err)

ok, err := bls.Verify(aggPK, msg, aggSig)
require.NoError(t, err)
assert.True(t, ok, "aggregated signature must be valid for the aggregated public key")

// The same verification through VerifyAggregate must produce the same result.
assert.True(t, bls.VerifyAggregate(signerPKs, msg, aggSig))

// A different message must not verify.
ok, err = bls.Verify(aggPK, []byte("another message"), aggSig)
require.NoError(t, err)
assert.False(t, ok, "aggregated signature must be invalid for a different message")

// A public key of a signer who didn't sign the message added to the aggregate must break the verification.
extendedPKs := append(slices.Clone(signerPKs), outsiderPK)
extendedPK, err := bls.AggregatePublicKeys(extendedPKs)
require.NoError(t, err)
ok, err = bls.Verify(extendedPK, msg, aggSig)
require.NoError(t, err)
assert.False(t, ok, "aggregated public key of a larger set of signers must not verify the signature")
assert.False(t, bls.VerifyAggregate(extendedPKs, msg, aggSig))

// But it verifies again as soon as the signature of that signer joins the aggregate.
outsiderSig, err := bls.Sign(outsiderSK, msg)
require.NoError(t, err)
extendedSig, err := bls.AggregateSignatures(append(slices.Clone(sigs), outsiderSig))
require.NoError(t, err)
ok, err = bls.Verify(extendedPK, msg, extendedSig)
require.NoError(t, err)
assert.True(t, ok, "aggregated signature of all the signers must be valid for their aggregated public key")

// A missing signer breaks the verification in the same way.
reducedPKs := signerPKs[:n-1]
reducedPK, err := bls.AggregatePublicKeys(reducedPKs)
require.NoError(t, err)
ok, err = bls.Verify(reducedPK, msg, aggSig)
require.NoError(t, err)
assert.False(t, ok, "aggregated public key of a smaller set of signers must not verify the signature")
assert.False(t, bls.VerifyAggregate(reducedPKs, msg, aggSig))
}

// TestAggregatePublicKeys_OrderIndependent checks that aggregation of public keys is commutative.
func TestAggregatePublicKeys_OrderIndependent(t *testing.T) {
_, pks := generateKeyPairs(t, 5)

expected, err := bls.AggregatePublicKeys(pks)
require.NoError(t, err)

slices.Reverse(pks)
actual, err := bls.AggregatePublicKeys(pks)
require.NoError(t, err)
assert.Equal(t, expected, actual, "order of public keys must not affect the aggregate")

// A single public key aggregates into itself.
single, err := bls.AggregatePublicKeys(pks[:1])
require.NoError(t, err)
assert.Equal(t, pks[0], single)
}

// TestAggregatePublicKeys_Errors checks that invalid sets of public keys are rejected.
func TestAggregatePublicKeys_Errors(t *testing.T) {
_, pks := generateKeyPairs(t, 2)

_, err := bls.AggregatePublicKeys(nil)
assert.ErrorIs(t, err, bls.ErrNoPublicKeys)

_, err = bls.AggregatePublicKeys([]bls.PublicKey{pks[0], pks[1], pks[0]})
assert.ErrorIs(t, err, bls.ErrDuplicatePublicKey)

// Keys that are not valid points of G1 are rejected.
_, err = bls.AggregatePublicKeys([]bls.PublicKey{pks[0], {}})
assert.Error(t, err)
Comment thread
alexeykiselev marked this conversation as resolved.

// The identity, a valid encoding but an unusable public key, is rejected as well.
var identity bls.PublicKey
identity[0] = 0xc0 // Compressed point at infinity.
_, err = bls.AggregatePublicKeys([]bls.PublicKey{pks[0], identity})
assert.Error(t, err)
}

func TestVerifyAggregate_RejectsDuplicatePublicKeys(t *testing.T) {
sk1, err := secretKeyFromWavesSecretKey(randWavesSK(t))
require.NoError(t, err)
Expand Down
8 changes: 3 additions & 5 deletions pkg/crypto/bls/pop.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package bls
import (
"encoding/binary"
"fmt"

cbls "github.com/cloudflare/circl/sign/bls"
)

const PoPMessageSize = PublicKeySize + 4
Expand Down Expand Up @@ -44,9 +46,5 @@ func VerifyPoP(pk PublicKey, height uint32, sig Signature) (bool, error) {
return false, fmt.Errorf("failed to verify PoP, invalid public key")
}
msg := BuildPoPMessage(pk, height)
ok, err := Verify(pk, msg, sig)
if err != nil {
return false, fmt.Errorf("failed to verify PoP: %w", err)
}
return ok, nil
return cbls.Verify[cbls.G1](cpk, msg, sig.Bytes()), nil
}
Loading