diff --git a/blssig/aggregation.go b/blssig/aggregation.go index ea468305..7d160d4e 100644 --- a/blssig/aggregation.go +++ b/blssig/aggregation.go @@ -6,18 +6,23 @@ import ( "fmt" "runtime/debug" - "github.com/filecoin-project/go-f3/gpbft" - "github.com/filecoin-project/go-f3/internal/measurements" + "go.dedis.ch/kyber/v4" "go.opentelemetry.io/otel/metric" - "github.com/drand/kyber" - "github.com/drand/kyber/sign" + "github.com/filecoin-project/go-f3/gpbft" + "github.com/filecoin-project/go-f3/internal/bls/bdn" + "github.com/filecoin-project/go-f3/internal/measurements" ) // Max size of the point cache. const maxPointCacheSize = 10_000 -func (v *Verifier) Aggregate(pubkeys []gpbft.PubKey, signatures [][]byte) (_agg []byte, _err error) { +type aggregation struct { + mask *bdn.Mask + scheme *bdn.Scheme +} + +func (a *aggregation) Aggregate(mask []int, signatures [][]byte) (_agg []byte, _err error) { defer func() { status := measurements.AttrStatusSuccess if _err != nil { @@ -25,29 +30,31 @@ func (v *Verifier) Aggregate(pubkeys []gpbft.PubKey, signatures [][]byte) (_agg } if perr := recover(); perr != nil { - _err = fmt.Errorf("panicked aggregating public keys: %v\n%s", + _err = fmt.Errorf("panicked aggregating signatures: %v\n%s", perr, string(debug.Stack())) log.Error(_err) status = measurements.AttrStatusPanic } metrics.aggregate.Record( - context.TODO(), int64(len(pubkeys)), + context.TODO(), int64(len(mask)), metric.WithAttributes(status), ) }() - if len(pubkeys) != len(signatures) { + if len(mask) != len(signatures) { return nil, fmt.Errorf("lengths of pubkeys and sigs does not match %d != %d", - len(pubkeys), len(signatures)) + len(mask), len(signatures)) } - mask, err := v.pubkeysToMask(pubkeys) - if err != nil { - return nil, fmt.Errorf("converting public keys to mask: %w", err) + bdnMask := a.mask.Clone() + for _, bit := range mask { + if err := bdnMask.SetBit(bit, true); err != nil { + return nil, err + } } - aggSigPoint, err := v.scheme.AggregateSignatures(signatures, mask) + aggSigPoint, err := a.scheme.AggregateSignatures(signatures, bdnMask) if err != nil { return nil, fmt.Errorf("computing aggregate signature: %w", err) } @@ -59,7 +66,7 @@ func (v *Verifier) Aggregate(pubkeys []gpbft.PubKey, signatures [][]byte) (_agg return aggSig, nil } -func (v *Verifier) VerifyAggregate(msg []byte, signature []byte, pubkeys []gpbft.PubKey) (_err error) { +func (a *aggregation) VerifyAggregate(mask []int, msg []byte, signature []byte) (_err error) { defer func() { status := measurements.AttrStatusSuccess if _err != nil { @@ -75,25 +82,35 @@ func (v *Verifier) VerifyAggregate(msg []byte, signature []byte, pubkeys []gpbft } metrics.verifyAggregate.Record( - context.TODO(), int64(len(pubkeys)), + context.TODO(), int64(len(mask)), metric.WithAttributes(status), ) }() - mask, err := v.pubkeysToMask(pubkeys) - if err != nil { - return fmt.Errorf("converting public keys to mask: %w", err) + bdnMask := a.mask.Clone() + for _, bit := range mask { + if err := bdnMask.SetBit(bit, true); err != nil { + return err + } } - aggPubKey, err := v.scheme.AggregatePublicKeys(mask) + aggPubKey, err := a.scheme.AggregatePublicKeys(bdnMask) if err != nil { return fmt.Errorf("aggregating public keys: %w", err) } - return v.scheme.Verify(aggPubKey, msg, signature) + return a.scheme.Verify(aggPubKey, msg, signature) } -func (v *Verifier) pubkeysToMask(pubkeys []gpbft.PubKey) (*sign.Mask, error) { +func (v *Verifier) Aggregate(pubkeys []gpbft.PubKey) (_agg gpbft.Aggregate, _err error) { + defer func() { + if perr := recover(); perr != nil { + _err = fmt.Errorf("panicked aggregating public keys: %v\n%s", + perr, string(debug.Stack())) + log.Error(_err) + } + }() + kPubkeys := make([]kyber.Point, 0, len(pubkeys)) for i, p := range pubkeys { point, err := v.pubkeyToPoint(p) @@ -103,15 +120,12 @@ func (v *Verifier) pubkeysToMask(pubkeys []gpbft.PubKey) (*sign.Mask, error) { kPubkeys = append(kPubkeys, point.Clone()) } - mask, err := sign.NewMask(v.suite, kPubkeys, nil) + mask, err := bdn.NewMask(v.keyGroup, kPubkeys, nil) if err != nil { return nil, fmt.Errorf("creating key mask: %w", err) } - for i := range kPubkeys { - err := mask.SetBit(i, true) - if err != nil { - return nil, fmt.Errorf("setting mask bit %d: %w", i, err) - } - } - return mask, nil + return &aggregation{ + mask: mask, + scheme: v.scheme, + }, nil } diff --git a/blssig/cache_test.go b/blssig/cache_test.go index 91d564f7..d9bfd7da 100644 --- a/blssig/cache_test.go +++ b/blssig/cache_test.go @@ -4,17 +4,17 @@ import ( "runtime" "testing" - bls12381 "github.com/drand/kyber-bls12381" - "github.com/drand/kyber/sign/bdn" "github.com/stretchr/testify/require" "github.com/filecoin-project/go-f3/gpbft" + "github.com/filecoin-project/go-f3/internal/bls/bdn" + bls12381 "github.com/filecoin-project/go-f3/internal/bls/gnark" ) const maxCacheMemory uint64 = 10 << 20 // 10MiB func TestCacheMemory(t *testing.T) { - suite := bls12381.NewBLS12381Suite() + suite := bls12381.NewSuiteBLS12381() scheme := bdn.NewSchemeOnG2(suite) rand := suite.RandomStream() diff --git a/blssig/signer.go b/blssig/signer.go index 9c3cf6f8..f90027ef 100644 --- a/blssig/signer.go +++ b/blssig/signer.go @@ -5,10 +5,10 @@ import ( "context" "errors" - "github.com/drand/kyber" - bls12381 "github.com/drand/kyber-bls12381" - "github.com/drand/kyber/sign/bdn" "github.com/filecoin-project/go-f3/gpbft" + "github.com/filecoin-project/go-f3/internal/bls/bdn" + bls12381 "github.com/filecoin-project/go-f3/internal/bls/gnark" + "go.dedis.ch/kyber/v4" ) var _ gpbft.Signer = (*Signer)(nil) @@ -21,7 +21,7 @@ type Signer struct { func SignerWithKeyOnG1(pub gpbft.PubKey, privKey kyber.Scalar) *Signer { return &Signer{ - scheme: bdn.NewSchemeOnG2(bls12381.NewBLS12381Suite()), + scheme: bdn.NewSchemeOnG2(bls12381.NewSuiteBLS12381()), pubKey: pub, privKey: privKey, } diff --git a/blssig/verifier.go b/blssig/verifier.go index ad6c746d..282d700d 100644 --- a/blssig/verifier.go +++ b/blssig/verifier.go @@ -7,18 +7,16 @@ import ( "runtime/debug" "sync" - "github.com/drand/kyber" - bls12381 "github.com/drand/kyber-bls12381" - "github.com/drand/kyber/pairing" - "github.com/drand/kyber/sign/bdn" + "go.dedis.ch/kyber/v4" "go.opentelemetry.io/otel/metric" "github.com/filecoin-project/go-f3/gpbft" + "github.com/filecoin-project/go-f3/internal/bls/bdn" + bls12381 "github.com/filecoin-project/go-f3/internal/bls/gnark" "github.com/filecoin-project/go-f3/internal/measurements" ) type Verifier struct { - suite pairing.Suite scheme *bdn.Scheme keyGroup kyber.Group @@ -27,9 +25,8 @@ type Verifier struct { } func VerifierWithKeyOnG1() *Verifier { - suite := bls12381.NewBLS12381Suite() + suite := bls12381.NewSuiteBLS12381() return &Verifier{ - suite: suite, scheme: bdn.NewSchemeOnG2(suite), keyGroup: suite.G1(), } diff --git a/blssig/verifier_test.go b/blssig/verifier_test.go index bced0983..6ad70b3b 100644 --- a/blssig/verifier_test.go +++ b/blssig/verifier_test.go @@ -4,14 +4,14 @@ import ( "context" "testing" - bls12381 "github.com/drand/kyber-bls12381" - "github.com/drand/kyber/sign/bdn" + "github.com/filecoin-project/go-f3/internal/bls/bdn" + bls12381 "github.com/filecoin-project/go-f3/internal/bls/gnark" "github.com/stretchr/testify/require" ) func BenchmarkBLSSigning(b *testing.B) { var ( - blsSuit = bls12381.NewBLS12381Suite() + blsSuit = bls12381.NewSuiteBLS12381() blsSchema = bdn.NewSchemeOnG2(blsSuit) ) privKey, pubKey := blsSchema.NewKeyPair(blsSuit.RandomStream()) diff --git a/certs/certs.go b/certs/certs.go index b22ca346..40c3a7c2 100644 --- a/certs/certs.go +++ b/certs/certs.go @@ -150,7 +150,8 @@ func verifyFinalityCertificateSignature(verifier gpbft.Verifier, powerTable gpbf return fmt.Errorf("failed to scale power table: %w", err) } - signers := make([]gpbft.PubKey, 0, len(powerTable)) + keys := powerTable.PublicKeys() + mask := make([]int, 0, len(powerTable)) var signerPowers int64 if err := cert.Signers.ForEach(func(i uint64) error { if i >= uint64(len(powerTable)) { @@ -165,7 +166,7 @@ func verifyFinalityCertificateSignature(verifier gpbft.Verifier, powerTable gpbf cert.GPBFTInstance, powerTable[i].ID) } signerPowers += power - signers = append(signers, powerTable[i].PubKey) + mask = append(mask, int(i)) return nil }); err != nil { return err @@ -192,7 +193,12 @@ func verifyFinalityCertificateSignature(verifier gpbft.Verifier, powerTable gpbf signedBytes = payload.MarshalForSigning(nn) } - if err := verifier.VerifyAggregate(signedBytes, cert.Signature, signers); err != nil { + aggregate, err := verifier.Aggregate(keys) + if err != nil { + return err + } + + if err := aggregate.VerifyAggregate(mask, signedBytes, cert.Signature); err != nil { return fmt.Errorf("invalid signature on finality certificate for instance %d: %w", cert.GPBFTInstance, err) } return nil diff --git a/emulator/host.go b/emulator/host.go index 2c3e9579..0ce89d36 100644 --- a/emulator/host.go +++ b/emulator/host.go @@ -82,8 +82,9 @@ func (h *driverHost) GetCommittee(id uint64) (*gpbft.Committee, error) { return nil, fmt.Errorf("instance ID %d not found", id) } return &gpbft.Committee{ - PowerTable: instance.powerTable, - Beacon: instance.beacon, + PowerTable: instance.powerTable, + Beacon: instance.beacon, + AggregateVerifier: instance.aggregateVerifier, }, nil } diff --git a/emulator/instance.go b/emulator/instance.go index d5882259..4d711159 100644 --- a/emulator/instance.go +++ b/emulator/instance.go @@ -14,14 +14,15 @@ import ( // Instance represents a GPBFT instance capturing all the information necessary // for GPBFT to function, along with the final decision reached if any. type Instance struct { - t *testing.T - id uint64 - supplementalData gpbft.SupplementalData - proposal gpbft.ECChain - powerTable *gpbft.PowerTable - beacon []byte - decision *gpbft.Justification - signing Signing + t *testing.T + id uint64 + supplementalData gpbft.SupplementalData + proposal gpbft.ECChain + powerTable *gpbft.PowerTable + beacon []byte + decision *gpbft.Justification + signing Signing + aggregateVerifier gpbft.Aggregate } // NewInstance instantiates a new Instance for emulation. If absent, the @@ -58,7 +59,8 @@ func NewInstance(t *testing.T, id uint64, powerEntries gpbft.PowerEntries, propo } proposalChain, err := gpbft.NewChain(proposal[0], proposal[1:]...) require.NoError(t, err) - return &Instance{ + + i := &Instance{ t: t, id: id, powerTable: pt, @@ -68,11 +70,18 @@ func NewInstance(t *testing.T, id uint64, powerEntries gpbft.PowerEntries, propo Commitments: [32]byte{}, PowerTable: ptCid, }, - signing: AdhocSigning(), } + + i.SetSigning(AdhocSigning()) + return i } -func (i *Instance) SetSigning(signing Signing) { i.signing = signing } +func (i *Instance) SetSigning(signing Signing) { + var err error + i.signing = signing + i.aggregateVerifier, err = signing.Aggregate(i.powerTable.Entries.PublicKeys()) + require.NoError(i.t, err) +} func (i *Instance) Proposal() gpbft.ECChain { return i.proposal } func (i *Instance) GetDecision() *gpbft.Justification { return i.decision } func (i *Instance) ID() uint64 { return i.id } @@ -140,7 +149,6 @@ func (i *Instance) NewJustificationWithPayload(payload gpbft.Payload, from ...gp msg := i.signing.MarshalPayloadForSigning(networkName, &payload) qr := gpbft.QuorumResult{ Signers: make([]int, len(from)), - PubKeys: make([]gpbft.PubKey, len(from)), Signatures: make([][]byte, len(from)), } for j, actor := range from { @@ -150,10 +158,9 @@ func (i *Instance) NewJustificationWithPayload(payload gpbft.Payload, from ...gp signature, err := i.signing.Sign(context.Background(), entry.PubKey, msg) require.NoError(i.t, err) qr.Signatures[j] = signature - qr.PubKeys[j] = entry.PubKey qr.Signers[j] = index } - aggregate, err := i.signing.Aggregate(qr.PubKeys, qr.Signatures) + aggregate, err := i.aggregateVerifier.Aggregate(qr.Signers, qr.Signatures) require.NoError(i.t, err) return &gpbft.Justification{ Vote: payload, diff --git a/emulator/signing.go b/emulator/signing.go index 6979f618..c715506e 100644 --- a/emulator/signing.go +++ b/emulator/signing.go @@ -3,6 +3,7 @@ package emulator import ( "bytes" "context" + "encoding/binary" "errors" "hash/crc32" @@ -58,13 +59,22 @@ func (s adhocSigning) Verify(sender gpbft.PubKey, msg, got []byte) error { } } -func (s adhocSigning) Aggregate(signers []gpbft.PubKey, sigs [][]byte) ([]byte, error) { - if len(signers) != len(sigs) { +type aggregate struct { + keys []gpbft.PubKey + signing adhocSigning +} + +// Aggregate implements gpbft.Aggregate. +func (a *aggregate) Aggregate(signerMask []int, sigs [][]byte) ([]byte, error) { + if len(signerMask) != len(sigs) { return nil, errors.New("public keys and signatures length mismatch") } hasher := crc32.NewIEEE() - for i, signer := range signers { - if _, err := hasher.Write(signer); err != nil { + for i, bit := range signerMask { + if err := binary.Write(hasher, binary.BigEndian, uint64(bit)); err != nil { + return nil, err + } + if _, err := hasher.Write(a.keys[bit]); err != nil { return nil, err } if _, err := hasher.Write(sigs[i]); err != nil { @@ -74,16 +84,17 @@ func (s adhocSigning) Aggregate(signers []gpbft.PubKey, sigs [][]byte) ([]byte, return hasher.Sum(nil), nil } -func (s adhocSigning) VerifyAggregate(payload, got []byte, signers []gpbft.PubKey) error { - signatures := make([][]byte, len(signers)) +// VerifyAggregate implements gpbft.Aggregate. +func (a *aggregate) VerifyAggregate(signerMask []int, payload []byte, got []byte) error { + signatures := make([][]byte, len(signerMask)) var err error - for i, signer := range signers { - signatures[i], err = s.Sign(context.Background(), signer, payload) + for i, bit := range signerMask { + signatures[i], err = a.signing.Sign(context.Background(), a.keys[bit], payload) if err != nil { return err } } - want, err := s.Aggregate(signers, signatures) + want, err := a.Aggregate(signerMask, signatures) if err != nil { return err } @@ -93,23 +104,34 @@ func (s adhocSigning) VerifyAggregate(payload, got []byte, signers []gpbft.PubKe return nil } +func (s adhocSigning) Aggregate(keys []gpbft.PubKey) (gpbft.Aggregate, error) { + return &aggregate{keys: keys, + signing: s, + }, nil +} + func (s adhocSigning) MarshalPayloadForSigning(name gpbft.NetworkName, payload *gpbft.Payload) []byte { return payload.MarshalForSigning(name) } type erroneousSigning struct{} +type erroneousAggregate struct{} func (p erroneousSigning) Verify(gpbft.PubKey, []byte, []byte) error { return errors.New("err Verify") } -func (p erroneousSigning) VerifyAggregate([]byte, []byte, []gpbft.PubKey) error { +func (p erroneousAggregate) VerifyAggregate([]int, []byte, []byte) error { return errors.New("err VerifyAggregate") } -func (p erroneousSigning) Aggregate([]gpbft.PubKey, [][]byte) ([]byte, error) { +func (p erroneousAggregate) Aggregate([]int, [][]byte) ([]byte, error) { return nil, errors.New("err Aggregate") } + +func (p erroneousSigning) Aggregate([]gpbft.PubKey) (gpbft.Aggregate, error) { + return erroneousAggregate{}, nil +} func (p erroneousSigning) Sign(context.Context, gpbft.PubKey, []byte) ([]byte, error) { return nil, errors.New("err Sign") } @@ -119,9 +141,16 @@ func (p erroneousSigning) MarshalPayloadForSigning(gpbft.NetworkName, *gpbft.Pay } type panicSigning struct{} +type panicAggregate struct{} func (p panicSigning) Verify(gpbft.PubKey, []byte, []byte) error { panic("π") } func (p panicSigning) VerifyAggregate([]byte, []byte, []gpbft.PubKey) error { panic("π") } -func (p panicSigning) Aggregate([]gpbft.PubKey, [][]byte) ([]byte, error) { panic("π") } func (p panicSigning) Sign(context.Context, gpbft.PubKey, []byte) ([]byte, error) { panic("π") } func (p panicSigning) MarshalPayloadForSigning(gpbft.NetworkName, *gpbft.Payload) []byte { panic("π") } + +func (p panicSigning) Aggregate([]gpbft.PubKey) (gpbft.Aggregate, error) { + return panicAggregate{}, nil +} + +func (p panicAggregate) VerifyAggregate([]int, []byte, []byte) error { panic("π") } +func (p panicAggregate) Aggregate([]int, [][]byte) ([]byte, error) { panic("π") } diff --git a/go.mod b/go.mod index 2dfe4150..207261c8 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,7 @@ module github.com/filecoin-project/go-f3 go 1.22 require ( - github.com/drand/kyber v1.3.1 - github.com/drand/kyber-bls12381 v0.3.1 + github.com/consensys/gnark-crypto v0.12.1 github.com/filecoin-project/go-bitfield v0.2.4 github.com/filecoin-project/go-clock v0.1.0 github.com/filecoin-project/go-state-types v0.14.0 @@ -22,6 +21,7 @@ require ( github.com/stretchr/testify v1.9.0 github.com/urfave/cli/v2 v2.25.5 github.com/whyrusleeping/cbor-gen v0.1.1 + go.dedis.ch/kyber/v4 v4.0.0-pre2.0.20240916105431-b283c0cdd30a go.opentelemetry.io/otel v1.28.0 go.opentelemetry.io/otel/metric v1.28.0 go.uber.org/multierr v1.11.0 @@ -36,7 +36,9 @@ require ( github.com/andybalholm/brotli v1.1.0 // indirect github.com/benbjohnson/clock v1.3.5 // indirect github.com/beorn7/perks v1.0.1 // indirect + github.com/bits-and-blooms/bitset v1.13.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/consensys/bavard v0.1.13 // indirect github.com/containerd/cgroups v1.1.0 // indirect github.com/coreos/go-systemd/v22 v22.5.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect @@ -63,7 +65,6 @@ require ( github.com/jackpal/go-nat-pmp v1.0.2 // indirect github.com/jbenet/go-temp-err-catcher v0.1.0 // indirect github.com/jbenet/goprocess v0.1.4 // indirect - github.com/kilic/bls12-381 v0.1.0 // indirect github.com/klauspost/compress v1.17.9 // indirect github.com/klauspost/cpuid/v2 v2.2.7 // indirect github.com/koron/go-ssdp v0.0.4 // indirect @@ -83,6 +84,7 @@ require ( github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b // indirect github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc // indirect github.com/minio/sha256-simd v1.0.1 // indirect + github.com/mmcloughlin/addchain v0.4.0 // indirect github.com/mr-tron/base58 v1.2.0 // indirect github.com/multiformats/go-base32 v0.1.0 // indirect github.com/multiformats/go-base36 v0.2.0 // indirect @@ -129,6 +131,7 @@ require ( github.com/stretchr/objx v0.5.2 // indirect github.com/syndtr/goleveldb v1.0.0 // indirect github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 // indirect + go.dedis.ch/fixbuf v1.0.3 // indirect go.opentelemetry.io/otel/trace v1.28.0 // indirect go.uber.org/dig v1.17.1 // indirect go.uber.org/fx v1.21.1 // indirect @@ -141,4 +144,5 @@ require ( google.golang.org/protobuf v1.34.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect lukechampine.com/blake3 v1.2.1 // indirect + rsc.io/tmplfunc v0.0.3 // indirect ) diff --git a/go.sum b/go.sum index 00790557..11a0ee6f 100644 --- a/go.sum +++ b/go.sum @@ -18,12 +18,18 @@ github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZx github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bits-and-blooms/bitset v1.13.0 h1:bAQ9OPNFYbGHV6Nez0tmNI0RiEu7/hxlYJRUA0wFAVE= +github.com/bits-and-blooms/bitset v1.13.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= 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/cilium/ebpf v0.2.0/go.mod h1:To2CFviqOWL/M0gIMsvSMlqe7em/l1ALkX1PyjrX2Qs= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/consensys/bavard v0.1.13 h1:oLhMLOFGTLdlda/kma4VOJazblc7IM5y5QPd2A/YjhQ= +github.com/consensys/bavard v0.1.13/go.mod h1:9ItSMtA/dXMAiL7BG6bqW2m3NdSEObYWoH223nGHukI= +github.com/consensys/gnark-crypto v0.12.1 h1:lHH39WuuFgVHONRl3J0LRBtuYdQTumFSDtJF7HpyG8M= +github.com/consensys/gnark-crypto v0.12.1/go.mod h1:v2Gy7L/4ZRosZ7Ivs+9SfUDr0f5UlG+EM5t7MPHiLuY= github.com/containerd/cgroups v0.0.0-20201119153540-4cbc285b3327/go.mod h1:ZJeTFisyysqgcCdecO57Dj79RfL0LNeGiFUqLYQRYLE= github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM= github.com/containerd/cgroups v1.1.0/go.mod h1:6ppBcbh/NOOUU+dMKrykgaBnK9lCIBxHqJDGwsa1mIw= @@ -47,10 +53,6 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3 github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/drand/kyber v1.3.1 h1:E0p6M3II+loMVwTlAp5zu4+GGZFNiRfq02qZxzw2T+Y= -github.com/drand/kyber v1.3.1/go.mod h1:f+mNHjiGT++CuueBrpeMhFNdKZAsy0tu03bKq9D5LPA= -github.com/drand/kyber-bls12381 v0.3.1 h1:KWb8l/zYTP5yrvKTgvhOrk2eNPscbMiUOIeWBnmUxGo= -github.com/drand/kyber-bls12381 v0.3.1/go.mod h1:H4y9bLPu7KZA/1efDg+jtJ7emKx+ro3PU7/jWUVt140= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/elastic/gosigar v0.12.0/go.mod h1:iXRIGg2tLnu7LBdpqzyQfGDEidKCfWcCMS0WKyPWoMs= github.com/elastic/gosigar v0.14.2 h1:Dg80n8cr90OZ7x+bAax/QjoW/XqTI11RmA79ZwIm9/4= @@ -112,6 +114,7 @@ github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OI github.com/google/pprof v0.0.0-20240207164012-fb44976bdcd5 h1:E/LAvt58di64hlYjx7AsNS6C/ysHWYo+2qPCZKTQhRo= github.com/google/pprof v0.0.0-20240207164012-fb44976bdcd5/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -156,8 +159,6 @@ github.com/jbenet/goprocess v0.1.4/go.mod h1:5yspPrukOVuOLORacaBi858NqyClJPQxYZl github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/kilic/bls12-381 v0.1.0 h1:encrdjqKMEvabVQ7qYOKu1OvhqpK4s47wDYtNiPtlp4= -github.com/kilic/bls12-381 v0.1.0/go.mod h1:vDTTHJONJ6G+P2R74EhnyotQDTliQDnFEwhdmfzw1ig= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= @@ -177,6 +178,8 @@ github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leanovate/gopter v0.2.9 h1:fQjYxZaynp97ozCzfOyOuAGOU4aU/z37zf/tOujFk7c= +github.com/leanovate/gopter v0.2.9/go.mod h1:U2L/78B+KVFIx2VmW6onHJQzXtFb+p5y3y2Sh+Jxxv8= github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8= github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg= github.com/libp2p/go-flow-metrics v0.1.0 h1:0iPhMI8PskQwzh57jB9WxIuIOQ0r+15PChFGkx3Q3WM= @@ -228,6 +231,9 @@ github.com/minio/sha256-simd v0.0.0-20190131020904-2d45a736cd16/go.mod h1:2FMWW+ github.com/minio/sha256-simd v0.1.1-0.20190913151208-6de447530771/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM= github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= +github.com/mmcloughlin/addchain v0.4.0 h1:SobOdjm2xLj1KkXN5/n0xTIWyZA2+s99UCY1iPfkHRY= +github.com/mmcloughlin/addchain v0.4.0/go.mod h1:A86O+tHqZLMNO4w6ZZ4FlVQEadcoqkyU72HC5wJ4RlU= +github.com/mmcloughlin/profile v0.1.1/go.mod h1:IhHD7q1ooxgwTgjxQYkACGA77oFTDdFVejUS1/tS/qU= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/mr-tron/base58 v1.1.0/go.mod h1:xcD2VGqlgYjBdcBLw+TuYLr8afG+Hj8g2eTVqeSzSU8= @@ -436,6 +442,10 @@ github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.dedis.ch/fixbuf v1.0.3 h1:hGcV9Cd/znUxlusJ64eAlExS+5cJDIyTyEG+otu5wQs= go.dedis.ch/fixbuf v1.0.3/go.mod h1:yzJMt34Wa5xD37V5RTdmp38cz3QhMagdGoem9anUalw= +go.dedis.ch/kyber/v4 v4.0.0-pre2.0.20240916105431-b283c0cdd30a h1:FF9ZER1DSNuJWcpC/P9AJ9LpswxPxyyQ65YTOqdzDd0= +go.dedis.ch/kyber/v4 v4.0.0-pre2.0.20240916105431-b283c0cdd30a/go.mod h1:tg6jwKTYEjm94VxkFwiQy+ec9hoQvccIU989wNjXWVI= +go.dedis.ch/protobuf v1.0.11 h1:FTYVIEzY/bfl37lu3pR4lIj+F9Vp1jE8oh91VmxKgLo= +go.dedis.ch/protobuf v1.0.11/go.mod h1:97QR256dnkimeNdfmURz0wAMNVbd1VmLXhG1CrTYrJ4= go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo= go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4= @@ -557,7 +567,6 @@ golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201101102859-da207088b7d1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -663,6 +672,8 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWD gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= @@ -674,5 +685,7 @@ honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= lukechampine.com/blake3 v1.2.1 h1:YuqqRuaqsGV71BV/nm9xlI0MKUv4QC54jQnBChWbGnI= lukechampine.com/blake3 v1.2.1/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k= +rsc.io/tmplfunc v0.0.3 h1:53XFQh69AfOa8Tw0Jm7t+GV7KZhOi6jzsCzTtKbMvzU= +rsc.io/tmplfunc v0.0.3/go.mod h1:AG3sTPzElb1Io3Yg4voV9AGZJuleGAwaVRxL9M49PhA= sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck= sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0= diff --git a/gpbft/api.go b/gpbft/api.go index 5a3d0da3..c3458fe0 100644 --- a/gpbft/api.go +++ b/gpbft/api.go @@ -83,6 +83,9 @@ type Committee struct { PowerTable *PowerTable // Beacon is the unique beacon value associated with the committee. Beacon []byte + // AggregateVerifier is used to aggregate and verify aggregate signatures made by the + // committee. + AggregateVerifier Aggregate } // Endpoint to which participants can send messages. @@ -116,15 +119,27 @@ type SigningMarshaler interface { MarshalPayloadForSigning(NetworkName, *Payload) []byte } +type Aggregate interface { + // Aggregates signatures from a participants. + // + // Implementations must be safe for concurrent use. + Aggregate(signerMask []int, sigs [][]byte) ([]byte, error) + // VerifyAggregate verifies an aggregate signature. + // + // Implementations must be safe for concurrent use. + VerifyAggregate(signerMask []int, payload, aggSig []byte) error +} + type Verifier interface { // Verifies a signature for the given public key. + // // Implementations must be safe for concurrent use. Verify(pubKey PubKey, msg, sig []byte) error - // Aggregates signatures from a participants. - Aggregate(pubKeys []PubKey, sigs [][]byte) ([]byte, error) - // VerifyAggregate verifies an aggregate signature. + // Return an Aggregate that can aggregate and verify aggregate signatures made by the given + // public keys. + // // Implementations must be safe for concurrent use. - VerifyAggregate(payload, aggSig []byte, signers []PubKey) error + Aggregate(pubKeys []PubKey) (Aggregate, error) } type Signatures interface { diff --git a/gpbft/gpbft.go b/gpbft/gpbft.go index 9a317e6a..c9a091ce 100644 --- a/gpbft/gpbft.go +++ b/gpbft/gpbft.go @@ -156,6 +156,8 @@ type instance struct { input ECChain // The power table for the base chain, used for power in this instance. powerTable *PowerTable + // The aggregate signature verifier/aggregator. + aggregateVerifier Aggregate // The beacon value from the base chain, used for tickets in this instance. beacon []byte // Current round number. @@ -218,6 +220,7 @@ func newInstance( input ECChain, data *SupplementalData, powerTable *PowerTable, + aggregateVerifier Aggregate, beacon []byte) (*instance, error) { if input.IsZero() { return nil, fmt.Errorf("input is empty") @@ -228,16 +231,17 @@ func newInstance( metrics.currentRound.Record(context.TODO(), 0) return &instance{ - participant: participant, - instanceID: instanceID, - input: input, - powerTable: powerTable, - beacon: beacon, - round: 0, - phase: INITIAL_PHASE, - supplementalData: data, - proposal: input, - value: ECChain{}, + participant: participant, + instanceID: instanceID, + input: input, + powerTable: powerTable, + aggregateVerifier: aggregateVerifier, + beacon: beacon, + round: 0, + phase: INITIAL_PHASE, + supplementalData: data, + proposal: input, + value: ECChain{}, candidates: map[ChainKey]struct{}{ input.BaseChain().Key(): {}, }, @@ -986,7 +990,7 @@ func (i *instance) alarmAfterSynchrony() time.Time { // Builds a justification for a value from a quorum result. func (i *instance) buildJustification(quorum QuorumResult, round uint64, phase Phase, value ECChain) *Justification { - aggSignature, err := quorum.Aggregate(i.participant.host) + aggSignature, err := quorum.Aggregate(i.aggregateVerifier) if err != nil { panic(fmt.Errorf("aggregating for phase %v: %v", phase, err)) } @@ -1174,12 +1178,11 @@ func (q *quorumState) CouldReachStrongQuorumFor(key ChainKey, withAdversary bool type QuorumResult struct { // Signers is an array of indexes into the powertable, sorted in increasing order Signers []int - PubKeys []PubKey Signatures [][]byte } -func (q QuorumResult) Aggregate(v Verifier) ([]byte, error) { - return v.Aggregate(q.PubKeys, q.Signatures) +func (q QuorumResult) Aggregate(v Aggregate) ([]byte, error) { + return v.Aggregate(q.Signers, q.Signatures) } func (q QuorumResult) SignersBitfield() bitfield.BitField { @@ -1216,7 +1219,6 @@ func (q *quorumState) FindStrongQuorumFor(key ChainKey) (QuorumResult, bool) { // Accumulate signers and signatures until they reach a strong quorum. signatures := make([][]byte, 0, len(chainSupport.signatures)) - pubkeys := make([]PubKey, 0, len(signatures)) var justificationPower int64 for i, idx := range signers { if idx >= len(q.powerTable.Entries) { @@ -1226,11 +1228,9 @@ func (q *quorumState) FindStrongQuorumFor(key ChainKey) (QuorumResult, bool) { entry := q.powerTable.Entries[idx] justificationPower += power signatures = append(signatures, chainSupport.signatures[entry.ID]) - pubkeys = append(pubkeys, entry.PubKey) if IsStrongQuorum(justificationPower, q.powerTable.ScaledTotal) { return QuorumResult{ Signers: signers[:i+1], - PubKeys: pubkeys, Signatures: signatures, }, true } diff --git a/gpbft/mock_host_test.go b/gpbft/mock_host_test.go index 715c23da..3ad16a84 100644 --- a/gpbft/mock_host_test.go +++ b/gpbft/mock_host_test.go @@ -21,29 +21,29 @@ func (_m *MockHost) EXPECT() *MockHost_Expecter { return &MockHost_Expecter{mock: &_m.Mock} } -// Aggregate provides a mock function with given fields: pubKeys, sigs -func (_m *MockHost) Aggregate(pubKeys []PubKey, sigs [][]byte) ([]byte, error) { - ret := _m.Called(pubKeys, sigs) +// Aggregate provides a mock function with given fields: pubKeys +func (_m *MockHost) Aggregate(pubKeys []PubKey) (Aggregate, error) { + ret := _m.Called(pubKeys) if len(ret) == 0 { panic("no return value specified for Aggregate") } - var r0 []byte + var r0 Aggregate var r1 error - if rf, ok := ret.Get(0).(func([]PubKey, [][]byte) ([]byte, error)); ok { - return rf(pubKeys, sigs) + if rf, ok := ret.Get(0).(func([]PubKey) (Aggregate, error)); ok { + return rf(pubKeys) } - if rf, ok := ret.Get(0).(func([]PubKey, [][]byte) []byte); ok { - r0 = rf(pubKeys, sigs) + if rf, ok := ret.Get(0).(func([]PubKey) Aggregate); ok { + r0 = rf(pubKeys) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).([]byte) + r0 = ret.Get(0).(Aggregate) } } - if rf, ok := ret.Get(1).(func([]PubKey, [][]byte) error); ok { - r1 = rf(pubKeys, sigs) + if rf, ok := ret.Get(1).(func([]PubKey) error); ok { + r1 = rf(pubKeys) } else { r1 = ret.Error(1) } @@ -58,24 +58,23 @@ type MockHost_Aggregate_Call struct { // Aggregate is a helper method to define mock.On call // - pubKeys []PubKey -// - sigs [][]byte -func (_e *MockHost_Expecter) Aggregate(pubKeys interface{}, sigs interface{}) *MockHost_Aggregate_Call { - return &MockHost_Aggregate_Call{Call: _e.mock.On("Aggregate", pubKeys, sigs)} +func (_e *MockHost_Expecter) Aggregate(pubKeys interface{}) *MockHost_Aggregate_Call { + return &MockHost_Aggregate_Call{Call: _e.mock.On("Aggregate", pubKeys)} } -func (_c *MockHost_Aggregate_Call) Run(run func(pubKeys []PubKey, sigs [][]byte)) *MockHost_Aggregate_Call { +func (_c *MockHost_Aggregate_Call) Run(run func(pubKeys []PubKey)) *MockHost_Aggregate_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].([]PubKey), args[1].([][]byte)) + run(args[0].([]PubKey)) }) return _c } -func (_c *MockHost_Aggregate_Call) Return(_a0 []byte, _a1 error) *MockHost_Aggregate_Call { +func (_c *MockHost_Aggregate_Call) Return(_a0 Aggregate, _a1 error) *MockHost_Aggregate_Call { _c.Call.Return(_a0, _a1) return _c } -func (_c *MockHost_Aggregate_Call) RunAndReturn(run func([]PubKey, [][]byte) ([]byte, error)) *MockHost_Aggregate_Call { +func (_c *MockHost_Aggregate_Call) RunAndReturn(run func([]PubKey) (Aggregate, error)) *MockHost_Aggregate_Call { _c.Call.Return(run) return _c } @@ -527,54 +526,6 @@ func (_c *MockHost_Verify_Call) RunAndReturn(run func(PubKey, []byte, []byte) er return _c } -// VerifyAggregate provides a mock function with given fields: payload, aggSig, signers -func (_m *MockHost) VerifyAggregate(payload []byte, aggSig []byte, signers []PubKey) error { - ret := _m.Called(payload, aggSig, signers) - - if len(ret) == 0 { - panic("no return value specified for VerifyAggregate") - } - - var r0 error - if rf, ok := ret.Get(0).(func([]byte, []byte, []PubKey) error); ok { - r0 = rf(payload, aggSig, signers) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// MockHost_VerifyAggregate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'VerifyAggregate' -type MockHost_VerifyAggregate_Call struct { - *mock.Call -} - -// VerifyAggregate is a helper method to define mock.On call -// - payload []byte -// - aggSig []byte -// - signers []PubKey -func (_e *MockHost_Expecter) VerifyAggregate(payload interface{}, aggSig interface{}, signers interface{}) *MockHost_VerifyAggregate_Call { - return &MockHost_VerifyAggregate_Call{Call: _e.mock.On("VerifyAggregate", payload, aggSig, signers)} -} - -func (_c *MockHost_VerifyAggregate_Call) Run(run func(payload []byte, aggSig []byte, signers []PubKey)) *MockHost_VerifyAggregate_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].([]byte), args[1].([]byte), args[2].([]PubKey)) - }) - return _c -} - -func (_c *MockHost_VerifyAggregate_Call) Return(_a0 error) *MockHost_VerifyAggregate_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *MockHost_VerifyAggregate_Call) RunAndReturn(run func([]byte, []byte, []PubKey) error) *MockHost_VerifyAggregate_Call { - _c.Call.Return(run) - return _c -} - // NewMockHost creates a new instance of MockHost. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewMockHost(t interface { diff --git a/gpbft/participant.go b/gpbft/participant.go index 4cd48b2e..147e4b5b 100644 --- a/gpbft/participant.go +++ b/gpbft/participant.go @@ -333,7 +333,7 @@ func (p *Participant) validateJustification(msg *GMessage, comt *Committee) erro // Check justification power and signature. var justificationPower int64 - signers := make([]PubKey, 0) + signers := make([]int, 0) if err := msg.Justification.Signers.ForEach(func(bit uint64) error { if int(bit) >= len(comt.PowerTable.Entries) { return fmt.Errorf("invalid signer index: %d", bit) @@ -343,7 +343,7 @@ func (p *Participant) validateJustification(msg *GMessage, comt *Committee) erro return fmt.Errorf("signer with ID %d has no power", comt.PowerTable.Entries[bit].ID) } justificationPower += power - signers = append(signers, comt.PowerTable.Entries[bit].PubKey) + signers = append(signers, int(bit)) return nil }); err != nil { return fmt.Errorf("failed to iterate over signers: %w", err) @@ -354,7 +354,7 @@ func (p *Participant) validateJustification(msg *GMessage, comt *Committee) erro } payload := p.host.MarshalPayloadForSigning(p.host.NetworkName(), &msg.Justification.Vote) - if err := p.host.VerifyAggregate(payload, msg.Justification.Signature, signers); err != nil { + if err := comt.AggregateVerifier.VerifyAggregate(signers, payload, msg.Justification.Signature); err != nil { return fmt.Errorf("verification of the aggregate failed: %+v: %w", msg.Justification, err) } @@ -445,7 +445,7 @@ func (p *Participant) beginInstance() error { if err != nil { return err } - if p.gpbft, err = newInstance(p, p.currentInstance, chain, data, comt.PowerTable, comt.Beacon); err != nil { + if p.gpbft, err = newInstance(p, p.currentInstance, chain, data, comt.PowerTable, comt.AggregateVerifier, comt.Beacon); err != nil { return fmt.Errorf("failed creating new gpbft instance: %w", err) } if err := p.gpbft.Start(); err != nil { diff --git a/gpbft/powertable.go b/gpbft/powertable.go index c8a34f71..c052dfac 100644 --- a/gpbft/powertable.go +++ b/gpbft/powertable.go @@ -33,6 +33,14 @@ type PowerTable struct { ScaledTotal int64 } +func (e PowerEntries) PublicKeys() []PubKey { + keys := make([]PubKey, len(e)) + for i, e := range e { + keys[i] = e.PubKey + } + return keys +} + func (p *PowerEntry) Equal(o *PowerEntry) bool { return p.ID == o.ID && p.Power == o.Power && bytes.Equal(p.PubKey, o.PubKey) } diff --git a/host.go b/host.go index 747b56eb..e24c29d3 100644 --- a/host.go +++ b/host.go @@ -620,9 +620,22 @@ func (h *gpbftHost) GetCommittee(instance uint64) (_ *gpbft.Committee, _err erro if err := table.Validate(); err != nil { return nil, fmt.Errorf("invalid power table for instance %d: %w", instance, err) } + + // NOTE: we're intentionally keeping participants here even if they have no + // effective power (after rounding power) to simplify things. The runtime cost is + // minimal and it means that the keys can be aggregated before any rounding is done. + // TODO: this is slow and under a lock, but we only want to do it once per + // instance... ideally we'd have a per-instance lock/once, but that probably isn't + // worth it. + agg, err := h.Aggregate(table.Entries.PublicKeys()) + if err != nil { + return nil, fmt.Errorf("failed to pre-compute aggregate mask for instance %d: %w", instance, err) + } + return &gpbft.Committee{ - PowerTable: table, - Beacon: ts.Beacon(), + PowerTable: table, + Beacon: ts.Beacon(), + AggregateVerifier: agg, }, nil } @@ -732,13 +745,6 @@ func (h *gpbftHost) Verify(pubKey gpbft.PubKey, msg []byte, sig []byte) error { return h.verifier.Verify(pubKey, msg, sig) } -// Aggregates signatures from a participants. -func (h *gpbftHost) Aggregate(pubKeys []gpbft.PubKey, sigs [][]byte) ([]byte, error) { - return h.verifier.Aggregate(pubKeys, sigs) -} - -// VerifyAggregate verifies an aggregate signature. -// Implementations must be safe for concurrent use. -func (h *gpbftHost) VerifyAggregate(payload []byte, aggSig []byte, signers []gpbft.PubKey) error { - return h.verifier.VerifyAggregate(payload, aggSig, signers) +func (h *gpbftHost) Aggregate(pubKeys []gpbft.PubKey) (gpbft.Aggregate, error) { + return h.verifier.Aggregate(pubKeys) } diff --git a/internal/bls/LICENSE b/internal/bls/LICENSE new file mode 100644 index 00000000..411d65a2 --- /dev/null +++ b/internal/bls/LICENSE @@ -0,0 +1,375 @@ +This code is (c) by DEDIS/EPFL 2017 under the MPL v2 or later version. + +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. \ No newline at end of file diff --git a/internal/bls/README.md b/internal/bls/README.md new file mode 100644 index 00000000..b5eddb6c --- /dev/null +++ b/internal/bls/README.md @@ -0,0 +1 @@ +# A temporary fork of part of dedis/kyber diff --git a/internal/bls/bdn/bdn.go b/internal/bls/bdn/bdn.go new file mode 100644 index 00000000..8f1fce38 --- /dev/null +++ b/internal/bls/bdn/bdn.go @@ -0,0 +1,221 @@ +// Package bdn implements the Boneh-Drijvers-Neven signature scheme which is +// an implementation of the bls package which is robust against rogue public-key attacks. Those +// attacks could allow an attacker to forge a public-key and then make a verifiable +// signature for an aggregation of signatures. It fixes the situation by +// adding coefficients to the aggregate. +// +// See the papers: +// https://eprint.iacr.org/2018/483.pdf +// https://crypto.stanford.edu/~dabo/pubs/papers/BLSmultisig.html +package bdn + +import ( + "crypto/cipher" + "errors" + "fmt" + "slices" + + "go.dedis.ch/kyber/v4" + "go.dedis.ch/kyber/v4/pairing" + "go.dedis.ch/kyber/v4/sign" + "go.dedis.ch/kyber/v4/sign/bls" //lint:ignore SA1019 internal + "golang.org/x/crypto/blake2s" +) + +// For the choice of H, we're mostly worried about the second preimage attack. In +// other words, find m' where H(m) == H(m') +// We also use the entire roster so that the coefficient will vary for the same +// public key used in different roster +func hashPointToR(group kyber.Group, pubs []kyber.Point) ([]kyber.Scalar, error) { + h, err := blake2s.NewXOF(blake2s.OutputLengthUnknown, nil) + if err != nil { + return nil, err + } + for _, pub := range pubs { + peer, err := pub.MarshalBinary() + if err != nil { + return nil, err + } + _, err = h.Write(peer) + if err != nil { + return nil, err + } + } + + out := make([]byte, 16*len(pubs)) + _, err = h.Read(out) + if err != nil { + return nil, err + } + + coefs := make([]kyber.Scalar, len(pubs)) + for i := range coefs { + scalar := group.Scalar() + bytes := out[i*16 : (i+1)*16] + if scalar.ByteOrder() != kyber.LittleEndian { + slices.Reverse(bytes) + } + scalar.SetBytes(bytes) + coefs[i] = scalar + } + + return coefs, nil +} + +type Scheme struct { + blsScheme sign.AggregatableScheme + sigGroup kyber.Group + keyGroup kyber.Group + pairing func(signature, public, hashedPoint kyber.Point) bool +} + +// NewSchemeOnG1 returns a sign.Scheme that uses G1 for its signature space and G2 +// for its public keys +func NewSchemeOnG1(suite pairing.Suite) *Scheme { + sigGroup := suite.G1() + keyGroup := suite.G2() + pairing := func(public, hashedMsg, sigPoint kyber.Point) bool { + return suite.ValidatePairing(hashedMsg, public, sigPoint, keyGroup.Point().Base()) + } + return &Scheme{ + blsScheme: bls.NewSchemeOnG1(suite), + sigGroup: sigGroup, + keyGroup: keyGroup, + pairing: pairing, + } +} + +// NewSchemeOnG2 returns a sign.Scheme that uses G2 for its signature space and +// G1 for its public key +func NewSchemeOnG2(suite pairing.Suite) *Scheme { + sigGroup := suite.G2() + keyGroup := suite.G1() + pairing := func(public, hashedMsg, sigPoint kyber.Point) bool { + return suite.ValidatePairing(public, hashedMsg, keyGroup.Point().Base(), sigPoint) + } + return &Scheme{ + blsScheme: bls.NewSchemeOnG2(suite), + sigGroup: sigGroup, + keyGroup: keyGroup, + pairing: pairing, + } +} + +// NewKeyPair creates a new BLS signing key pair. The private key x is a scalar +// and the public key X is a point on the scheme's key group. +func (scheme *Scheme) NewKeyPair(random cipher.Stream) (kyber.Scalar, kyber.Point) { + return scheme.blsScheme.NewKeyPair(random) +} + +// Sign creates a BLS signature S = x * H(m) on a message m using the private +// key x. The signature S is a point on the scheme's signature group. +func (scheme *Scheme) Sign(x kyber.Scalar, msg []byte) ([]byte, error) { + return scheme.blsScheme.Sign(x, msg) +} + +// Verify checks the given BLS signature S on the message m using the public +// key X by verifying that the equality e(H(m), X) == e(H(m), x*B2) == +// e(x*H(m), B2) == e(S, B2) holds where e is the pairing operation and B2 is +// the base point from the scheme's key group. +func (scheme *Scheme) Verify(x kyber.Point, msg, sig []byte) error { + return scheme.blsScheme.Verify(x, msg, sig) +} + +// AggregateSignatures aggregates the signatures using a coefficient for each +// one of them where c = H(pk) and H: keyGroup -> R with R = {1, ..., 2^128} +func (scheme *Scheme) AggregateSignatures(sigs [][]byte, mask *Mask) (kyber.Point, error) { + agg := scheme.sigGroup.Point() + for i := range mask.publics { + if enabled, err := mask.GetBit(i); err != nil { + // this should never happen because of the loop boundary + // an error here is probably a bug in the mask implementation + return nil, fmt.Errorf("couldn't find the index %d: %w", i, err) + } else if !enabled { + continue + } + + if len(sigs) == 0 { + return nil, errors.New("length of signatures and public keys must match") + } + + buf := sigs[0] + sigs = sigs[1:] + + sig := scheme.sigGroup.Point() + err := sig.UnmarshalBinary(buf) + if err != nil { + return nil, err + } + + sigC := sig.Clone().Mul(mask.publicCoefs[i], sig) + // c+1 because R is in the range [1, 2^128] and not [0, 2^128-1] + sigC = sigC.Add(sigC, sig) + agg = agg.Add(agg, sigC) + } + + if len(sigs) > 0 { + return nil, errors.New("length of signatures and public keys must match") + } + + return agg, nil +} + +// AggregatePublicKeys aggregates a set of public keys (similarly to +// AggregateSignatures for signatures) using the hash function +// H: keyGroup -> R with R = {1, ..., 2^128}. +func (scheme *Scheme) AggregatePublicKeys(mask *Mask) (kyber.Point, error) { + agg := scheme.keyGroup.Point() + for i := range mask.publics { + if enabled, err := mask.GetBit(i); err != nil { + // this should never happen because of the loop boundary + // an error here is probably a bug in the mask implementation + return nil, fmt.Errorf("couldn't find the index %d: %w", i, err) + } else if !enabled { + continue + } + + agg = agg.Add(agg, mask.publicTerms[i]) + } + + return agg, nil +} + +// v1 API Deprecated ---------------------------------- + +// NewKeyPair creates a new BLS signing key pair. The private key x is a scalar +// and the public key X is a point on curve G2. +// Deprecated: use the new scheme methods instead. +func NewKeyPair(suite pairing.Suite, random cipher.Stream) (kyber.Scalar, kyber.Point) { + return NewSchemeOnG1(suite).NewKeyPair(random) +} + +// Sign creates a BLS signature S = x * H(m) on a message m using the private +// key x. The signature S is a point on curve G1. +// Deprecated: use the new scheme methods instead. +func Sign(suite pairing.Suite, x kyber.Scalar, msg []byte) ([]byte, error) { + return NewSchemeOnG1(suite).Sign(x, msg) +} + +// Verify checks the given BLS signature S on the message m using the public +// key X by verifying that the equality e(H(m), X) == e(H(m), x*B2) == +// e(x*H(m), B2) == e(S, B2) holds where e is the pairing operation and B2 is +// the base point from curve G2. +// Deprecated: use the new scheme methods instead. +func Verify(suite pairing.Suite, x kyber.Point, msg, sig []byte) error { + return NewSchemeOnG1(suite).Verify(x, msg, sig) +} + +// AggregateSignatures aggregates the signatures using a coefficient for each +// one of them where c = H(pk) and H: G2 -> R with R = {1, ..., 2^128} +// Deprecated: use the new scheme methods instead. +func AggregateSignatures(suite pairing.Suite, sigs [][]byte, mask *Mask) (kyber.Point, error) { + return NewSchemeOnG1(suite).AggregateSignatures(sigs, mask) +} + +// AggregatePublicKeys aggregates a set of public keys (similarly to +// AggregateSignatures for signatures) using the hash function +// H: G2 -> R with R = {1, ..., 2^128}. +// Deprecated: use the new scheme methods instead. +func AggregatePublicKeys(suite pairing.Suite, mask *Mask) (kyber.Point, error) { + return NewSchemeOnG1(suite).AggregatePublicKeys(mask) +} diff --git a/internal/bls/bdn/bdn_test.go b/internal/bls/bdn/bdn_test.go new file mode 100644 index 00000000..db49d8f7 --- /dev/null +++ b/internal/bls/bdn/bdn_test.go @@ -0,0 +1,254 @@ +package bdn + +import ( + "encoding" + "encoding/hex" + "fmt" + "testing" + + "github.com/stretchr/testify/require" + "go.dedis.ch/kyber/v4" + "go.dedis.ch/kyber/v4/pairing/bn256" + "go.dedis.ch/kyber/v4/sign/bls" //lint:ignore SA1019 internal + "go.dedis.ch/kyber/v4/util/random" + + "github.com/filecoin-project/go-f3/internal/bls/gnark" +) + +var suite = bn256.NewSuiteBn256() +var two = suite.Scalar().Add(suite.Scalar().One(), suite.Scalar().One()) +var three = suite.Scalar().Add(two, suite.Scalar().One()) + +// Reference test for other languages +func TestBDN_HashPointToR_BN256(t *testing.T) { + p1 := suite.Point().Base() + p2 := suite.Point().Mul(two, suite.Point().Base()) + p3 := suite.Point().Mul(three, suite.Point().Base()) + + coefs, err := hashPointToR(suite, []kyber.Point{p1, p2, p3}) + + require.NoError(t, err) + require.Equal(t, "35b5b395f58aba3b192fb7e1e5f2abd3", coefs[0].String()) + require.Equal(t, "14dcc79d46b09b93075266e47cd4b19e", coefs[1].String()) + require.Equal(t, "933f6013eb3f654f9489d6d45ad04eaf", coefs[2].String()) + //require.Equal(t, 16, coefs[0].MarshalSize()) + + mask, _ := NewMask(suite, []kyber.Point{p1, p2, p3}, nil) + mask.SetBit(0, true) + mask.SetBit(1, true) + mask.SetBit(2, true) + + agg, err := AggregatePublicKeys(suite, mask) + require.NoError(t, err) + + buf, err := agg.MarshalBinary() + require.NoError(t, err) + ref := "1432ef60379c6549f7e0dbaf289cb45487c9d7da91fc20648f319a9fbebb23164abea76cdf7b1a3d20d539d9fe096b1d6fb3ee31bf1d426cd4a0d09d603b09f55f473fde972aa27aa991c249e890c1e4a678d470592dd09782d0fb3774834f0b2e20074a49870f039848a6b1aff95e1a1f8170163c77098e1f3530744d1826ce" + require.Equal(t, ref, fmt.Sprintf("%x", buf)) +} + +func TestBDN_AggregateSignatures(t *testing.T) { + msg := []byte("Hello Boneh-Lynn-Shacham") + private1, public1 := NewKeyPair(suite, random.New()) + private2, public2 := NewKeyPair(suite, random.New()) + sig1, err := Sign(suite, private1, msg) + require.NoError(t, err) + sig2, err := Sign(suite, private2, msg) + require.NoError(t, err) + + mask, _ := NewMask(suite, []kyber.Point{public1, public2}, nil) + mask.SetBit(0, true) + mask.SetBit(1, true) + + _, err = AggregateSignatures(suite, [][]byte{sig1}, mask) + require.Error(t, err) + + aggregatedSig, err := AggregateSignatures(suite, [][]byte{sig1, sig2}, mask) + require.NoError(t, err) + + aggregatedKey, err := AggregatePublicKeys(suite, mask) + require.NoError(t, err) + + sig, err := aggregatedSig.MarshalBinary() + require.NoError(t, err) + + err = Verify(suite, aggregatedKey, msg, sig) + require.NoError(t, err) + + mask.SetBit(1, false) + aggregatedKey, err = AggregatePublicKeys(suite, mask) + require.NoError(t, err) + + err = Verify(suite, aggregatedKey, msg, sig) + require.Error(t, err) +} + +func TestBDN_SubsetSignature(t *testing.T) { + msg := []byte("Hello Boneh-Lynn-Shacham") + private1, public1 := NewKeyPair(suite, random.New()) + private2, public2 := NewKeyPair(suite, random.New()) + _, public3 := NewKeyPair(suite, random.New()) + sig1, err := Sign(suite, private1, msg) + require.NoError(t, err) + sig2, err := Sign(suite, private2, msg) + require.NoError(t, err) + + mask, _ := NewMask(suite, []kyber.Point{public1, public3, public2}, nil) + mask.SetBit(0, true) + mask.SetBit(2, true) + + aggregatedSig, err := AggregateSignatures(suite, [][]byte{sig1, sig2}, mask) + require.NoError(t, err) + + aggregatedKey, err := AggregatePublicKeys(suite, mask) + require.NoError(t, err) + + sig, err := aggregatedSig.MarshalBinary() + require.NoError(t, err) + + err = Verify(suite, aggregatedKey, msg, sig) + require.NoError(t, err) +} + +func TestBDN_RogueAttack(t *testing.T) { + msg := []byte("Hello Boneh-Lynn-Shacham") + scheme := bls.NewSchemeOnG1(suite) + // honest + _, public1 := scheme.NewKeyPair(random.New()) + // attacker + private2, public2 := scheme.NewKeyPair(random.New()) + + // create a forged public-key for public1 + rogue := public1.Clone().Sub(public2, public1) + + pubs := []kyber.Point{public1, rogue} + + sig, err := Sign(suite, private2, msg) + require.NoError(t, err) + + // Old scheme not resistant to the attack + agg := scheme.AggregatePublicKeys(pubs...) + require.NoError(t, scheme.Verify(agg, msg, sig)) + + // New scheme that should detect + mask, _ := NewMask(suite, pubs, nil) + mask.SetBit(0, true) + mask.SetBit(1, true) + agg, err = AggregatePublicKeys(suite, mask) + require.NoError(t, err) + require.Error(t, Verify(suite, agg, msg, sig)) +} + +func Benchmark_BDN_AggregateSigs(b *testing.B) { + private1, public1 := NewKeyPair(suite, random.New()) + private2, public2 := NewKeyPair(suite, random.New()) + msg := []byte("Hello many times Boneh-Lynn-Shacham") + sig1, err := Sign(suite, private1, msg) + require.Nil(b, err) + sig2, err := Sign(suite, private2, msg) + require.Nil(b, err) + + mask, _ := NewMask(suite, []kyber.Point{public1, public2}, nil) + mask.SetBit(0, true) + mask.SetBit(1, false) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + AggregateSignatures(suite, [][]byte{sig1, sig2}, mask) + } +} + +func Benchmark_BDN_BLS12381_AggregateVerify(b *testing.B) { + suite := gnark.NewSuiteBLS12381() + schemeOnG2 := NewSchemeOnG2(suite) + + rng := random.New() + pubKeys := make([]kyber.Point, 3000) + privKeys := make([]kyber.Scalar, 3000) + for i := range pubKeys { + privKeys[i], pubKeys[i] = schemeOnG2.NewKeyPair(rng) + } + + mask, err := NewMask(suite.G1(), pubKeys, nil) + require.NoError(b, err) + for i := range pubKeys { + require.NoError(b, mask.SetBit(i, true)) + } + + msg := []byte("Hello many times Boneh-Lynn-Shacham") + sigs := make([][]byte, len(privKeys)) + for i, k := range privKeys { + s, err := schemeOnG2.Sign(k, msg) + require.NoError(b, err) + sigs[i] = s + } + + sig, err := schemeOnG2.AggregateSignatures(sigs, mask) + require.NoError(b, err) + sigb, err := sig.MarshalBinary() + require.NoError(b, err) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + pk, err := schemeOnG2.AggregatePublicKeys(mask) + require.NoError(b, err) + require.NoError(b, schemeOnG2.Verify(pk, msg, sigb)) + } +} + +func unmarshalHex[T encoding.BinaryUnmarshaler](t *testing.T, into T, s string) T { + t.Helper() + b, err := hex.DecodeString(s) + require.NoError(t, err) + require.NoError(t, into.UnmarshalBinary(b)) + return into +} + +// This tests exists to make sure we don't accidentally make breaking changes to signature +// aggregation by using checking against known aggregated signatures and keys. +func TestBDNFixtures(t *testing.T) { + schemeOnG1 := NewSchemeOnG1(suite) + + public1 := unmarshalHex(t, suite.G2().Point(), "1a30714035c7a161e286e54c191b8c68345bd8239c74925a26290e8e1ae97ed6657958a17dca12c943fadceb11b824402389ff427179e0f10194da3c1b771c6083797d2b5915ea78123cbdb99ea6389d6d6b67dcb512a2b552c373094ee5693524e3ebb4a176f7efa7285c25c80081d8cb598745978f1a63b886c09a316b1493") + private1 := unmarshalHex(t, suite.G2().Scalar(), "49cfe5e9f4532670137184d43c0299f8b635bcacf6b0af7cab262494602d9f38") + public2 := unmarshalHex(t, suite.G2().Point(), "603bc61466ec8762ec6de2ba9a80b9d302d08f580d1685ac45a8e404a6ed549719dc0faf94d896a9983ff23423772720e3de5d800bc200de6f7d7e146162d3183b8880c5c0d8b71ca4b3b40f30c12d8cc0679c81a47c239c6aa7e9cc2edab4a927fe865cd413c1c17e3df8f74108e784cd77dd3e161bdaf30019a55826a32a1f") + private2 := unmarshalHex(t, suite.G2().Scalar(), "493abea4bb35b74c78ad9245f9d37883aeb6ee91f7fb0d8a8e11abf7aa2be581") + public3 := unmarshalHex(t, suite.G2().Point(), "56118769a1f0b6286abacaa32109c1497ab0819c5d21f27317e184b6681c283007aa981cb4760de044946febdd6503ab77a4586bc29c04159e53a6fa5dcb9c0261ccd1cb2e28db5204ca829ac9f6be95f957a626544adc34ba3bc542533b6e2f5cbd0567e343641a61a42b63f26c3625f74b66f6f46d17b3bf1688fae4d455ec") + private3 := unmarshalHex(t, suite.G2().Scalar(), "7fb0ebc317e161502208c3c16a4af890dedc3c7b275e8a04e99c0528aa6a19aa") + + sig1Exp, err := hex.DecodeString("0913b76987be19f943be23b636cab9a2484507717326bd8bbdcdbbb6b8d5eb9253cfb3597c3fa550ee4972a398813650825a871f8e0b242ae5ddbce1b7c0e2a8") + require.NoError(t, err) + sig2Exp, err := hex.DecodeString("21195d29b1863bca1559e24375211d1411d8a28a8f4c772870b07f4ccda2fd5e337c1315c210475c683e3aa8b87d3aed3f7255b3087daa30d1e1432dd61d7484") + require.NoError(t, err) + sig3Exp, err := hex.DecodeString("3c1ac80345c1733630dbdc8106925c867544b521c259f9fa9678d477e6e5d3d212b09bc0d95137c3dbc0af2241415156c56e757d5577a609293584d045593195") + require.NoError(t, err) + + aggSigExp := unmarshalHex(t, suite.G1().Point(), "43c1d2ad5a7d71a08f3cd7495db6b3c81a4547af1b76438b2f215e85ec178fea048f93f6ffed65a69ea757b47761e7178103bb347fd79689652e55b6e0054af2") + aggKeyExp := unmarshalHex(t, suite.G2().Point(), "43b5161ede207b9a69fc93114b0c5022b76cc22e813ba739c7e622d826b132333cd637505399963b94e393ec7f5d4875f82391620b34be1fde1f232204fa4f723935d4dbfb725f059456bcf2557f846c03190969f7b800e904d25b0b5bcbdd421c9877d443f0313c3425dfc1e7e646b665d27b9e649faadef1129f95670d70e1") + + msg := []byte("Hello many times Boneh-Lynn-Shacham") + sig1, err := schemeOnG1.Sign(private1, msg) + require.Nil(t, err) + require.Equal(t, sig1Exp, sig1) + + sig2, err := schemeOnG1.Sign(private2, msg) + require.Nil(t, err) + require.Equal(t, sig2Exp, sig2) + + sig3, err := schemeOnG1.Sign(private3, msg) + require.Nil(t, err) + require.Equal(t, sig3Exp, sig3) + + mask, _ := NewMask(suite, []kyber.Point{public1, public2, public3}, nil) + mask.SetBit(0, true) + mask.SetBit(1, false) + mask.SetBit(2, true) + + aggSig, err := schemeOnG1.AggregateSignatures([][]byte{sig1, sig3}, mask) + require.NoError(t, err) + require.True(t, aggSigExp.Equal(aggSig)) + + aggKey, err := schemeOnG1.AggregatePublicKeys(mask) + require.NoError(t, err) + require.True(t, aggKeyExp.Equal(aggKey)) +} diff --git a/internal/bls/bdn/mask.go b/internal/bls/bdn/mask.go new file mode 100644 index 00000000..8e695b87 --- /dev/null +++ b/internal/bls/bdn/mask.go @@ -0,0 +1,221 @@ +package bdn + +import ( + "errors" + "fmt" + "slices" + + "go.dedis.ch/kyber/v4" +) + +// Mask is a bitmask of the participation to a collective signature. +type Mask struct { + // The bitmask indicating which public keys are enabled/disabled for aggregation. This is + // the only mutable field. + mask []byte + + // The following fields are immutable and should not be changed after the mask is created. + // They may be shared between multiple masks. + + // Public keys for aggregation & signature verification. + publics []kyber.Point + // Coefficients used when aggregating signatures. + publicCoefs []kyber.Scalar + // Terms used to aggregate public keys + publicTerms []kyber.Point +} + +// NewMask creates a new mask from a list of public keys. If a key is provided, it +// will set the bit of the key to 1 or return an error if it is not found. +// +// The returned Mask will contain pre-computed terms and coefficients for all provided public +// keys, so it should be re-used for optimal performance (e.g., by creating a "base" mask and +// cloning it whenever aggregating signatures and/or public keys). +func NewMask(group kyber.Group, publics []kyber.Point, myKey kyber.Point) (*Mask, error) { + m := &Mask{ + publics: publics, + } + m.mask = make([]byte, m.Len()) + + if myKey != nil { + for i, key := range publics { + if key.Equal(myKey) { + err := m.SetBit(i, true) + return m, err + } + } + + return nil, errors.New("key not found") + } + + var err error + m.publicCoefs, err = hashPointToR(group, publics) + if err != nil { + return nil, fmt.Errorf("failed to hash public keys: %w", err) + } + + m.publicTerms = make([]kyber.Point, len(publics)) + for i, pub := range publics { + pubC := pub.Clone().Mul(m.publicCoefs[i], pub) + m.publicTerms[i] = pubC.Add(pubC, pub) + } + + return m, nil +} + +// Mask returns the bitmask as a byte array. +func (m *Mask) Mask() []byte { + clone := make([]byte, len(m.mask)) + copy(clone, m.mask) + return clone +} + +// Len returns the length of the byte array necessary to store the bitmask. +func (m *Mask) Len() int { + return (len(m.publics) + 7) / 8 +} + +// SetMask replaces the current mask by the new one if the length matches. +func (m *Mask) SetMask(mask []byte) error { + if m.Len() != len(mask) { + return fmt.Errorf("mismatching mask lengths") + } + + m.mask = mask + return nil +} + +// GetBit returns true if the given bit is set. +func (m *Mask) GetBit(i int) (bool, error) { + if i >= len(m.publics) || i < 0 { + return false, errors.New("index out of range") + } + + byteIndex := i / 8 + mask := byte(1) << uint(i&7) + return m.mask[byteIndex]&mask != 0, nil +} + +// SetBit turns on or off the bit at the given index. +func (m *Mask) SetBit(i int, enable bool) error { + if i >= len(m.publics) || i < 0 { + return errors.New("index out of range") + } + + byteIndex := i / 8 + mask := byte(1) << uint(i&7) + if enable { + m.mask[byteIndex] |= mask + } else { + m.mask[byteIndex] &^= mask + } + return nil +} + +// forEachBitEnabled is a helper to iterate over the bits set to 1 in the mask +// and to return the result of the callback only if it is positive. +func (m *Mask) forEachBitEnabled(f func(i, j, n int) int) int { + n := 0 + for i, b := range m.mask { + for j := uint(0); j < 8; j++ { + mm := byte(1) << (j & 7) + + if b&mm != 0 { + if res := f(i, int(j), n); res >= 0 { + return res + } + + n++ + } + } + } + + return -1 +} + +// IndexOfNthEnabled returns the index of the nth enabled bit or -1 if out of bounds. +func (m *Mask) IndexOfNthEnabled(nth int) int { + return m.forEachBitEnabled(func(i, j, n int) int { + if n == nth { + return i*8 + int(j) + } + + return -1 + }) +} + +// NthEnabledAtIndex returns the sum of bits set to 1 until the given index. In other +// words, it returns how many bits are enabled before the given index. +func (m *Mask) NthEnabledAtIndex(idx int) int { + return m.forEachBitEnabled(func(i, j, n int) int { + if i*8+int(j) == idx { + return n + } + + return -1 + }) +} + +// Publics returns a copy of the list of public keys. +func (m *Mask) Publics() []kyber.Point { + pubs := make([]kyber.Point, len(m.publics)) + copy(pubs, m.publics) + return pubs +} + +// Participants returns the list of public keys participating. +func (m *Mask) Participants() []kyber.Point { + pp := []kyber.Point{} + for i, p := range m.publics { + byteIndex := i / 8 + mask := byte(1) << uint(i&7) + if (m.mask[byteIndex] & mask) != 0 { + pp = append(pp, p) + } + } + + return pp +} + +// CountEnabled returns the number of bit set to 1 +func (m *Mask) CountEnabled() int { + count := 0 + for i := range m.publics { + byteIndex := i / 8 + mask := byte(1) << uint(i&7) + if (m.mask[byteIndex] & mask) != 0 { + count++ + } + } + return count +} + +// CountTotal returns the number of potential participants +func (m *Mask) CountTotal() int { + return len(m.publics) +} + +// Merge merges the given mask to the current one only if +// the length matches +func (m *Mask) Merge(mask []byte) error { + if len(m.mask) != len(mask) { + return errors.New("mismatching mask length") + } + + for i := range m.mask { + m.mask[i] |= mask[i] + } + + return nil +} + +// Clone copies the mask while keeping the precomputed coefficients, etc. This method is thread safe +// and does not modify the original mask. Modifications to the new Mask will not affect the original. +func (m *Mask) Clone() *Mask { + return &Mask{ + mask: slices.Clone(m.mask), + publics: m.publics, + publicCoefs: m.publicCoefs, + publicTerms: m.publicTerms, + } +} diff --git a/internal/bls/bdn/mask_test.go b/internal/bls/bdn/mask_test.go new file mode 100644 index 00000000..9cccecab --- /dev/null +++ b/internal/bls/bdn/mask_test.go @@ -0,0 +1,153 @@ +package bdn + +import ( + "crypto/rand" + "testing" + + "github.com/stretchr/testify/require" + "go.dedis.ch/kyber/v4" + "go.dedis.ch/kyber/v4/util/key" +) + +const n = 17 + +var publics []kyber.Point + +func init() { + publics = make([]kyber.Point, n) + + for i := 0; i < n; i++ { + kp := key.NewKeyPair(suite) + publics[i] = kp.Public + } +} + +func TestMask_CreateMask(t *testing.T) { + mask, err := NewMask(suite, publics, nil) + require.NoError(t, err) + + require.Equal(t, len(publics), len(mask.Publics())) + require.Equal(t, 0, mask.CountEnabled()) + require.Equal(t, n, mask.CountTotal()) + require.Equal(t, n/8+1, mask.Len()) + require.Equal(t, uint8(0), mask.Mask()[0]) + + mask, err = NewMask(suite, publics, publics[2]) + require.NoError(t, err) + + require.Equal(t, len(publics), len(mask.Publics())) + require.Equal(t, 1, mask.CountEnabled()) + require.Equal(t, uint8(0x4), mask.Mask()[0]) + + _, err = NewMask(suite, publics, suite.G1().Point()) + require.Error(t, err) +} + +func TestMask_SetBit(t *testing.T) { + mask, err := NewMask(suite, publics, publics[2]) + require.NoError(t, err) + + // Make sure the mask is initially as we'd expect. + + bit, err := mask.GetBit(1) + require.NoError(t, err) + require.False(t, bit) + + bit, err = mask.GetBit(2) + require.NoError(t, err) + require.True(t, bit) + + // Set bit 1 + + err = mask.SetBit(1, true) + require.NoError(t, err) + require.Equal(t, uint8(0x6), mask.Mask()[0]) + require.Equal(t, 2, len(mask.Participants())) + + bit, err = mask.GetBit(1) + require.NoError(t, err) + require.True(t, bit) + + // Set bit 1 again, nothing should change + + err = mask.SetBit(1, true) + require.NoError(t, err) + require.Equal(t, uint8(0x6), mask.Mask()[0]) + require.Equal(t, 2, len(mask.Participants())) + + bit, err = mask.GetBit(1) + require.NoError(t, err) + require.True(t, bit) + + // Unset bit 2 + + err = mask.SetBit(2, false) + require.NoError(t, err) + require.Equal(t, uint8(0x2), mask.Mask()[0]) + require.Equal(t, 1, len(mask.Participants())) + + bit, err = mask.GetBit(2) + require.NoError(t, err) + require.False(t, bit) + + // Set bit 10 (using byte 2 now) + + err = mask.SetBit(10, true) + require.NoError(t, err) + require.Equal(t, uint8(0x2), mask.Mask()[0]) + require.Equal(t, uint8(0x4), mask.Mask()[1]) + require.Equal(t, 2, len(mask.Participants())) + + bit, err = mask.GetBit(10) + require.NoError(t, err) + require.True(t, bit) + + // And make sure the range limit works. + + err = mask.SetBit(-1, true) + require.Error(t, err) + err = mask.SetBit(len(publics), true) + require.Error(t, err) +} + +func TestMask_SetAndMerge(t *testing.T) { + mask, err := NewMask(suite, publics, publics[2]) + require.NoError(t, err) + + err = mask.SetMask([]byte{}) + require.Error(t, err) + + err = mask.SetMask([]byte{0, 0, 0}) + require.NoError(t, err) + + err = mask.Merge([]byte{}) + require.Error(t, err) + + err = mask.Merge([]byte{0x6, 0, 0}) + require.NoError(t, err) + require.Equal(t, uint8(0x6), mask.Mask()[0]) +} + +func TestMask_PositionalQueries(t *testing.T) { + mask, err := NewMask(suite, publics, publics[2]) + require.NoError(t, err) + + for i := 0; i < 10000; i++ { + bb := make([]byte, 3) + _, err := rand.Read(bb) + require.NoError(t, err) + bb[2] &= byte(1) << 7 + + err = mask.SetMask(bb) + require.NoError(t, err) + + for j := 0; j < mask.CountEnabled(); j++ { + idx := mask.IndexOfNthEnabled(j) + n := mask.NthEnabledAtIndex(idx) + require.Equal(t, j, n) + } + + require.Equal(t, -1, mask.IndexOfNthEnabled(mask.CountEnabled()+1)) + require.Equal(t, -1, mask.NthEnabledAtIndex(-1)) + } +} diff --git a/internal/bls/gnark/adapter.go b/internal/bls/gnark/adapter.go new file mode 100644 index 00000000..cca42e0b --- /dev/null +++ b/internal/bls/gnark/adapter.go @@ -0,0 +1,48 @@ +package gnark + +import ( + "go.dedis.ch/kyber/v4" +) + +// SuiteBLS12381 is an adapter that implements the suites.Suite interface so that +// bls12381 can be used as a common suite to generate key pairs for instance but +// still preserves the properties of the pairing (e.g. the Pair function). +// +// It's important to note that the Point function will generate a point +// compatible with public keys only (group G2) where the signature must be +// used as a point from the group G1. +type SuiteBLS12381 struct { + Suite + kyber.Group +} + +// NewSuiteBLS12381 makes a new BN256 suite +func NewSuiteBLS12381() *SuiteBLS12381 { + return &SuiteBLS12381{} +} + +// Point generates a point from the G2 group that can only be used +// for public keys +func (s *SuiteBLS12381) Point() kyber.Point { + return s.G2().Point() +} + +// PointLen returns the length of a G2 point +func (s *SuiteBLS12381) PointLen() int { + return s.G2().PointLen() +} + +// Scalar generates a scalar +func (s *SuiteBLS12381) Scalar() kyber.Scalar { + return s.G1().Scalar() +} + +// ScalarLen returns the length of a scalar +func (s *SuiteBLS12381) ScalarLen() int { + return s.G1().ScalarLen() +} + +// String returns the name of the suite +func (s *SuiteBLS12381) String() string { + return "gnark.adapter" +} diff --git a/internal/bls/gnark/adapter_test.go b/internal/bls/gnark/adapter_test.go new file mode 100644 index 00000000..1e457aeb --- /dev/null +++ b/internal/bls/gnark/adapter_test.go @@ -0,0 +1,28 @@ +package gnark + +import ( + "testing" + + "github.com/stretchr/testify/require" + "go.dedis.ch/kyber/v4/util/key" +) + +func TestAdapter_SuiteBLS12381(t *testing.T) { + suite := NewSuiteBLS12381() + + pair := key.NewKeyPair(suite) + pubkey, err := pair.Public.MarshalBinary() + require.Nil(t, err) + privkey, err := pair.Private.MarshalBinary() + require.Nil(t, err) + + pubhex := suite.Point() + err = pubhex.UnmarshalBinary(pubkey) + require.Nil(t, err) + + privhex := suite.Scalar() + err = privhex.UnmarshalBinary(privkey) + require.Nil(t, err) + + require.Equal(t, "gnark.adapter", suite.String()) +} diff --git a/internal/bls/gnark/g1.go b/internal/bls/gnark/g1.go new file mode 100644 index 00000000..a4041462 --- /dev/null +++ b/internal/bls/gnark/g1.go @@ -0,0 +1,142 @@ +package gnark + +import ( + "crypto/cipher" + "fmt" + "io" + "math/big" + + bls12381 "github.com/consensys/gnark-crypto/ecc/bls12-381" + "go.dedis.ch/kyber/v4" +) + +var _ kyber.SubGroupElement = &G1Elt{} + +// G1Elt is a wrapper around a G1 point on the BLS12-381 Gnark curve. +type G1Elt struct{ inner bls12381.G1Jac } + +// MarshalBinary returns a compressed point, without any domain separation tag information +func (p *G1Elt) MarshalBinary() (data []byte, err error) { + var g1aff bls12381.G1Affine + g1aff.FromJacobian(&p.inner) + res := g1aff.Bytes() + return res[:], nil +} + +// UnmarshalBinary populates the point from a compressed point representation. +func (p *G1Elt) UnmarshalBinary(data []byte) error { + var g1aff bls12381.G1Affine + _, err := g1aff.SetBytes(data) + if err != nil { + return fmt.Errorf("setting affine representation: %w", err) + } + + p.inner.FromAffine(&g1aff) + return nil +} + +func (p *G1Elt) String() string { return p.inner.String() } + +func (p *G1Elt) MarshalSize() int { return bls12381.SizeOfG1AffineCompressed } + +// MarshalTo writes a compressed point to the Writer, without any domain separation tag information +func (p *G1Elt) MarshalTo(w io.Writer) (int, error) { + buf, err := p.MarshalBinary() + if err != nil { + return 0, err + } + return w.Write(buf) +} + +// UnmarshalFrom populates the point from a compressed point representation read from the Reader. +func (p *G1Elt) UnmarshalFrom(r io.Reader) (int, error) { + buf := make([]byte, p.MarshalSize()) + n, err := io.ReadFull(r, buf) + if err != nil { + return n, err + } + return n, p.UnmarshalBinary(buf) +} + +func (p *G1Elt) Equal(p2 kyber.Point) bool { x := p2.(*G1Elt); return p.inner.Equal(&x.inner) } + +func (p *G1Elt) Null() kyber.Point { + p.inner.X.SetZero() + p.inner.Y.SetOne() + p.inner.Z.SetZero() + return p +} + +func (p *G1Elt) Base() kyber.Point { + p.inner, _, _, _ = bls12381.Generators() + return p +} + +func (p *G1Elt) Pick(rand cipher.Stream) kyber.Point { + var buf [32]byte + rand.XORKeyStream(buf[:], buf[:]) + return p.Hash(buf[:]) +} + +func (p *G1Elt) Set(p2 kyber.Point) kyber.Point { p.inner = p2.(*G1Elt).inner; return p } + +func (p *G1Elt) Clone() kyber.Point { return new(G1Elt).Set(p) } + +func (p *G1Elt) EmbedLen() int { + panic("bls12-381: unsupported operation") +} + +func (p *G1Elt) Embed(_ []byte, _ cipher.Stream) kyber.Point { + panic("bls12-381: unsupported operation") +} + +func (p *G1Elt) Data() ([]byte, error) { + panic("bls12-381: unsupported operation") +} + +func (p *G1Elt) Add(a, b kyber.Point) kyber.Point { + aa, bb := a.(*G1Elt), b.(*G1Elt) + p.inner.Set(&aa.inner) + p.inner.AddAssign(&bb.inner) + return p +} + +func (p *G1Elt) Sub(a, b kyber.Point) kyber.Point { + aa, bb := a.(*G1Elt), b.(*G1Elt) + p.inner.Set(&aa.inner) + p.inner.SubAssign(&bb.inner) + return p +} + +func (p *G1Elt) Neg(a kyber.Point) kyber.Point { + p.inner.Neg(&a.(*G1Elt).inner) + return p +} + +func (p *G1Elt) Mul(s kyber.Scalar, q kyber.Point) kyber.Point { + if q == nil { + q = new(G1Elt).Base() + } + ss, qq := s.(*Scalar), q.(*G1Elt) + var scalar big.Int + ss.inner.BigInt(&scalar) + p.inner.ScalarMultiplication(&qq.inner, &scalar) + return p +} + +func (p *G1Elt) IsInCorrectGroup() bool { + return !(p.inner.X.IsZero() && p.inner.Y.IsZero() && p.inner.X.IsZero()) && + p.inner.IsOnCurve() && p.inner.IsInSubGroup() +} + +var domainG1 = []byte("BLS_SIG_BLS12381G1_XMD:SHA-256_SSWU_RO_NUL_") + +func (p *G1Elt) Hash(msg []byte) kyber.Point { return p.Hash2(msg, domainG1) } +func (p *G1Elt) Hash2(msg, dst []byte) kyber.Point { + g1aff, err := bls12381.HashToG1(msg, dst) + if err != nil { + panic(fmt.Errorf("error while hashing: %w", err)) + } + p.inner.FromAffine(&g1aff) + return p +} diff --git a/internal/bls/gnark/g2.go b/internal/bls/gnark/g2.go new file mode 100644 index 00000000..48f6a291 --- /dev/null +++ b/internal/bls/gnark/g2.go @@ -0,0 +1,142 @@ +package gnark + +import ( + "crypto/cipher" + "fmt" + "io" + "math/big" + + bls12381 "github.com/consensys/gnark-crypto/ecc/bls12-381" + "go.dedis.ch/kyber/v4" +) + +var _ kyber.SubGroupElement = &G2Elt{} + +// G2Elt is a wrapper around the Gnark G2 point type. +type G2Elt struct{ inner bls12381.G2Jac } + +// MarshalBinary returns a compressed point, without any domain separation tag information +func (p *G2Elt) MarshalBinary() (data []byte, err error) { + var g2aff bls12381.G2Affine + g2aff.FromJacobian(&p.inner) + res := g2aff.Bytes() + return res[:], nil +} + +// UnmarshalBinary populates the point from a compressed point representation. +func (p *G2Elt) UnmarshalBinary(data []byte) error { + var g2aff bls12381.G2Affine + _, err := g2aff.SetBytes(data) + if err != nil { + return fmt.Errorf("setting affine representation: %w", err) + } + + p.inner.FromAffine(&g2aff) + return nil +} + +func (p *G2Elt) String() string { return p.inner.String() } + +func (p *G2Elt) MarshalSize() int { return bls12381.SizeOfG2AffineCompressed } + +// MarshalTo writes a compressed point to the Writer, without any domain separation tag information +func (p *G2Elt) MarshalTo(w io.Writer) (int, error) { + buf, err := p.MarshalBinary() + if err != nil { + return 0, err + } + return w.Write(buf) +} + +// UnmarshalFrom populates the point from a compressed point representation read from the Reader. +func (p *G2Elt) UnmarshalFrom(r io.Reader) (int, error) { + buf := make([]byte, p.MarshalSize()) + n, err := io.ReadFull(r, buf) + if err != nil { + return n, err + } + return n, p.UnmarshalBinary(buf) +} + +func (p *G2Elt) Equal(p2 kyber.Point) bool { x := p2.(*G2Elt); return p.inner.Equal(&x.inner) } + +func (p *G2Elt) Null() kyber.Point { + p.inner.X.SetZero() + p.inner.Y.SetOne() + p.inner.Z.SetZero() + return p +} + +func (p *G2Elt) Base() kyber.Point { + _, p.inner, _, _ = bls12381.Generators() + return p +} + +func (p *G2Elt) Pick(rand cipher.Stream) kyber.Point { + var buf [32]byte + rand.XORKeyStream(buf[:], buf[:]) + return p.Hash(buf[:]) +} + +func (p *G2Elt) Set(p2 kyber.Point) kyber.Point { p.inner = p2.(*G2Elt).inner; return p } + +func (p *G2Elt) Clone() kyber.Point { return new(G2Elt).Set(p) } + +func (p *G2Elt) EmbedLen() int { + panic("bls12-381: unsupported operation") +} + +func (p *G2Elt) Embed(_ []byte, _ cipher.Stream) kyber.Point { + panic("bls12-381: unsupported operation") +} + +func (p *G2Elt) Data() ([]byte, error) { + panic("bls12-381: unsupported operation") +} + +func (p *G2Elt) Add(a, b kyber.Point) kyber.Point { + aa, bb := a.(*G2Elt), b.(*G2Elt) + p.inner.Set(&aa.inner) + p.inner.AddAssign(&bb.inner) + return p +} + +func (p *G2Elt) Sub(a, b kyber.Point) kyber.Point { + aa, bb := a.(*G2Elt), b.(*G2Elt) + p.inner.Set(&aa.inner) + p.inner.SubAssign(&bb.inner) + return p +} + +func (p *G2Elt) Neg(a kyber.Point) kyber.Point { + p.inner.Neg(&a.(*G2Elt).inner) + return p +} + +func (p *G2Elt) Mul(s kyber.Scalar, q kyber.Point) kyber.Point { + if q == nil { + q = new(G2Elt).Base() + } + ss, qq := s.(*Scalar), q.(*G2Elt) + var scalar big.Int + ss.inner.BigInt(&scalar) + p.inner.ScalarMultiplication(&qq.inner, &scalar) + return p +} + +func (p *G2Elt) IsInCorrectGroup() bool { + return !(p.inner.X.IsZero() && p.inner.Y.IsZero() && p.inner.X.IsZero()) && + p.inner.IsOnCurve() && p.inner.IsInSubGroup() +} + +var domainG2 = []byte("BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_NUL_") + +func (p *G2Elt) Hash(msg []byte) kyber.Point { return p.Hash2(msg, domainG2) } +func (p *G2Elt) Hash2(msg, dst []byte) kyber.Point { + g1aff, err := bls12381.HashToG2(msg, dst) + if err != nil { + panic(fmt.Errorf("error while hashing: %w", err)) + } + p.inner.FromAffine(&g1aff) + return p +} diff --git a/internal/bls/gnark/group.go b/internal/bls/gnark/group.go new file mode 100644 index 00000000..e08a4cf9 --- /dev/null +++ b/internal/bls/gnark/group.go @@ -0,0 +1,23 @@ +package gnark + +import ( + fr "github.com/consensys/gnark-crypto/ecc/bls12-381/fr" + "go.dedis.ch/kyber/v4" +) + +var ( + G1 kyber.Group = &groupBls{name: "bls12-381.G1", newPoint: func() kyber.Point { return new(G1Elt).Null() }} + G2 kyber.Group = &groupBls{name: "bls12-381.G2", newPoint: func() kyber.Point { return new(G2Elt).Null() }} + GT kyber.Group = &groupBls{name: "bls12-381.GT", newPoint: func() kyber.Point { return new(GTElt).Null() }} +) + +type groupBls struct { + name string + newPoint func() kyber.Point +} + +func (g groupBls) String() string { return g.name } +func (g groupBls) ScalarLen() int { return fr.Bytes } +func (g groupBls) Scalar() kyber.Scalar { return new(Scalar).SetInt64(0) } +func (g groupBls) PointLen() int { return g.newPoint().MarshalSize() } +func (g groupBls) Point() kyber.Point { return g.newPoint() } diff --git a/internal/bls/gnark/gt.go b/internal/bls/gnark/gt.go new file mode 100644 index 00000000..13f684bc --- /dev/null +++ b/internal/bls/gnark/gt.go @@ -0,0 +1,108 @@ +package gnark + +import ( + "crypto/cipher" + "io" + "math/big" + + bls12381 "github.com/consensys/gnark-crypto/ecc/bls12-381" + "go.dedis.ch/kyber/v4" +) + +var gtBase *bls12381.GT + +func init() { + _, _, g1, g2 := bls12381.Generators() + gt, err := bls12381.Pair([]bls12381.G1Affine{g1}, []bls12381.G2Affine{g2}) + if err != nil { + panic(err) + } + gtBase = > +} + +var _ kyber.Point = >Elt{} + +// GTElt is a wrapper around the Circl Gt point type. +type GTElt struct{ inner bls12381.GT } + +// MarshalBinary returns a compressed point, without any domain separation tag information +func (p *GTElt) MarshalBinary() (data []byte, err error) { + res := p.inner.Bytes() + return res[:], nil +} + +// UnmarshalBinary populates the point from a compressed point representation. +func (p *GTElt) UnmarshalBinary(data []byte) error { return p.inner.Unmarshal(data) } + +func (p *GTElt) String() string { return p.inner.String() } + +func (p *GTElt) MarshalSize() int { return bls12381.SizeOfGT } + +// MarshalTo writes a compressed point to the Writer, without any domain separation tag information +func (p *GTElt) MarshalTo(w io.Writer) (int, error) { + buf, err := p.MarshalBinary() + if err != nil { + return 0, err + } + return w.Write(buf) +} + +// UnmarshalFrom populates the point from a compressed point representation read from the Reader. +func (p *GTElt) UnmarshalFrom(r io.Reader) (int, error) { + buf := make([]byte, p.MarshalSize()) + n, err := io.ReadFull(r, buf) + if err != nil { + return n, err + } + return n, p.UnmarshalBinary(buf) +} + +func (p *GTElt) Equal(p2 kyber.Point) bool { x := p2.(*GTElt); return p.inner.Equal(&x.inner) } + +func (p *GTElt) Null() kyber.Point { p.inner.SetOne(); return p } + +func (p *GTElt) Base() kyber.Point { p.inner = *gtBase; return p } + +func (p *GTElt) Pick(_ cipher.Stream) kyber.Point { + panic("bls12-381: unsupported operation") +} + +func (p *GTElt) Set(p2 kyber.Point) kyber.Point { p.inner = p2.(*GTElt).inner; return p } + +func (p *GTElt) Clone() kyber.Point { return new(GTElt).Set(p) } + +func (p *GTElt) EmbedLen() int { + panic("bls12-381: unsupported operation") +} + +func (p *GTElt) Embed(_ []byte, _ cipher.Stream) kyber.Point { + panic("bls12-381: unsupported operation") +} + +func (p *GTElt) Data() ([]byte, error) { + panic("bls12-381: unsupported operation") +} + +func (p *GTElt) Add(a, b kyber.Point) kyber.Point { + aa, bb := a.(*GTElt), b.(*GTElt) + p.inner.Mul(&aa.inner, &bb.inner) + return p +} + +func (p *GTElt) Sub(a, b kyber.Point) kyber.Point { + return p.Add(a, new(GTElt).Neg(b)) +} + +func (p *GTElt) Neg(a kyber.Point) kyber.Point { + aa := a.(*GTElt) + p.inner.Inverse(&aa.inner) + return p +} + +func (p *GTElt) Mul(s kyber.Scalar, q kyber.Point) kyber.Point { + qq, ss := q.(*GTElt), s.(*Scalar) + var scalar big.Int + ss.inner.BigInt(&scalar) + p.inner.Exp(qq.inner, &scalar) + return p +} diff --git a/internal/bls/gnark/scalar.go b/internal/bls/gnark/scalar.go new file mode 100644 index 00000000..38047b52 --- /dev/null +++ b/internal/bls/gnark/scalar.go @@ -0,0 +1,109 @@ +package gnark + +import ( + "crypto/cipher" + "io" + "math/big" + + fr "github.com/consensys/gnark-crypto/ecc/bls12-381/fr" + "go.dedis.ch/kyber/v4" + "go.dedis.ch/kyber/v4/util/random" +) + +var _ kyber.Scalar = &Scalar{} + +type Scalar struct{ inner fr.Element } + +func (s *Scalar) MarshalBinary() (data []byte, err error) { res := s.inner.Bytes(); return res[:], nil } + +func (s *Scalar) UnmarshalBinary(data []byte) error { s.inner.SetBytes(data); return nil } + +func (s *Scalar) String() string { return s.inner.String() } + +func (s *Scalar) MarshalSize() int { return fr.Bytes } + +func (s *Scalar) MarshalTo(w io.Writer) (int, error) { + buf := s.inner.Bytes() + return w.Write(buf[:]) +} + +func (s *Scalar) UnmarshalFrom(r io.Reader) (int, error) { + buf := make([]byte, s.MarshalSize()) + n, err := io.ReadFull(r, buf) + if err != nil { + return n, err + } + s.inner.SetBytes(buf) + return n, nil +} + +func (s *Scalar) Equal(s2 kyber.Scalar) bool { + x := s2.(*Scalar) + return s.inner.Cmp(&x.inner) == 0 +} + +func (s *Scalar) Set(a kyber.Scalar) kyber.Scalar { + aa := a.(*Scalar) + s.inner.Set(&aa.inner) + return s +} + +func (s *Scalar) Clone() kyber.Scalar { return new(Scalar).Set(s) } + +func (s *Scalar) SetInt64(v int64) kyber.Scalar { + s.inner.SetInt64(v) + + return s +} + +func (s *Scalar) Zero() kyber.Scalar { s.inner.SetUint64(0); return s } + +func (s *Scalar) Add(a, b kyber.Scalar) kyber.Scalar { + aa, bb := a.(*Scalar), b.(*Scalar) + s.inner.Add(&aa.inner, &bb.inner) + return s +} + +func (s *Scalar) Sub(a, b kyber.Scalar) kyber.Scalar { + aa, bb := a.(*Scalar), b.(*Scalar) + s.inner.Sub(&aa.inner, &bb.inner) + return s +} + +func (s *Scalar) Neg(a kyber.Scalar) kyber.Scalar { + s.Set(a) + s.inner.Neg(&s.inner) + return s +} + +func (s *Scalar) One() kyber.Scalar { s.inner.SetUint64(1); return s } + +func (s *Scalar) Mul(a, b kyber.Scalar) kyber.Scalar { + aa, bb := a.(*Scalar), b.(*Scalar) + s.inner.Mul(&aa.inner, &bb.inner) + return s +} + +func (s *Scalar) Div(a, b kyber.Scalar) kyber.Scalar { return s.Mul(new(Scalar).Inv(b), a) } + +func (s *Scalar) Inv(a kyber.Scalar) kyber.Scalar { + aa := a.(*Scalar) + s.inner.Inverse(&aa.inner) + return s +} + +func (s *Scalar) Pick(stream cipher.Stream) kyber.Scalar { + n := random.Int(fr.Modulus(), stream) + s.inner.SetBigInt(n) + return s +} + +func (s *Scalar) SetBytes(data []byte) kyber.Scalar { s.inner.SetBytes(data); return s } + +func (s *Scalar) ByteOrder() kyber.ByteOrder { + return kyber.BigEndian +} + +func (s *Scalar) GroupOrder() *big.Int { + return fr.Modulus() +} diff --git a/internal/bls/gnark/suite.go b/internal/bls/gnark/suite.go new file mode 100644 index 00000000..f500f33b --- /dev/null +++ b/internal/bls/gnark/suite.go @@ -0,0 +1,83 @@ +package gnark + +import ( + "crypto/cipher" + "crypto/sha256" + "fmt" + "hash" + "io" + + bls12381 "github.com/consensys/gnark-crypto/ecc/bls12-381" + "go.dedis.ch/kyber/v4" + "go.dedis.ch/kyber/v4/pairing" + "go.dedis.ch/kyber/v4/util/random" + "go.dedis.ch/kyber/v4/xof/blake2xb" +) + +var _ pairing.Suite = Suite{} + +type Suite struct{} + +func NewSuite() (s Suite) { return } + +func (s Suite) String() string { return "bls12381" } +func (s Suite) G1() kyber.Group { return G1 } +func (s Suite) G2() kyber.Group { return G2 } +func (s Suite) GT() kyber.Group { return GT } + +func (s Suite) Pair(p1, p2 kyber.Point) kyber.Point { + aa, bb := p1.(*G1Elt), p2.(*G2Elt) + var g1aff bls12381.G1Affine + g1aff.FromJacobian(&aa.inner) + var g2aff bls12381.G2Affine + g2aff.FromJacobian(&bb.inner) + gt, err := bls12381.Pair([]bls12381.G1Affine{g1aff}, []bls12381.G2Affine{g2aff}) + if err != nil { + panic(fmt.Errorf("error in gnark pairing: %w", err)) + } + + return >Elt{gt} +} + +func (s Suite) ValidatePairing(p1, p2, p3, p4 kyber.Point) bool { + a, b := p1.(*G1Elt), p2.(*G2Elt) + c, d := p3.(*G1Elt), p4.(*G2Elt) + + var aAff, cAff bls12381.G1Affine + var bAff, dAff bls12381.G2Affine + aAff.FromJacobian(&a.inner) + bAff.FromJacobian(&b.inner) + cAff.FromJacobian(&c.inner) + dAff.FromJacobian(&d.inner) + + cAff.Neg(&cAff) + + out, err := bls12381.PairingCheck( + []bls12381.G1Affine{aAff, cAff}, + []bls12381.G2Affine{bAff, dAff}, + ) + if err != nil { + panic(fmt.Errorf("error in gnark pairing: %w", err)) + } + return out +} + +func (s Suite) Read(_ io.Reader, _ ...interface{}) error { + panic("Suite.Read(): deprecated in drand") +} + +func (s Suite) Write(_ io.Writer, _ ...interface{}) error { + panic("Suite.Write(): deprecated in drand") +} + +func (s Suite) Hash() hash.Hash { + return sha256.New() +} + +func (s Suite) XOF(seed []byte) kyber.XOF { + return blake2xb.New(seed) +} + +func (s Suite) RandomStream() cipher.Stream { + return random.New() +} diff --git a/internal/bls/gnark/suite_test.go b/internal/bls/gnark/suite_test.go new file mode 100644 index 00000000..532441b2 --- /dev/null +++ b/internal/bls/gnark/suite_test.go @@ -0,0 +1,49 @@ +package gnark + +import ( + "crypto/sha256" + "encoding/binary" + "encoding/hex" + "testing" + + "go.dedis.ch/kyber/v4/pairing" + + "go.dedis.ch/kyber/v4" +) + +func TestVerifySigOnG2(t *testing.T) { + pk := "868f005eb8e6e4ca0a47c8a77ceaa5309a47978a7c71bc5cce96366b5d7a569937c529eeda66c7293784a9402801af31" + sig := "8d61d9100567de44682506aea1a7a6fa6e5491cd27a0a0ed349ef6910ac5ac20ff7bc3e09d7c046566c9f7f3c6f3b10104990e7cb424998203d8f7de586fb7fa5f60045417a432684f85093b06ca91c769f0e7ca19268375e659c2a2352b4655" + prevSig := "176f93498eac9ca337150b46d21dd58673ea4e3581185f869672e59fa4cb390a" + round := uint64(1) + + suite := NewSuite() + pkb, _ := hex.DecodeString(pk) + pubkeyP := suite.G1().Point() + pubkeyP.UnmarshalBinary(pkb) + sigb, _ := hex.DecodeString(sig) + sigP := suite.G2().Point() + sigP.UnmarshalBinary(sigb) + prev, _ := hex.DecodeString(prevSig) + h := sha256.New() + h.Write(prev) + _ = binary.Write(h, binary.BigEndian, round) + msg := h.Sum(nil) + + base := suite.G1().Point().Base().Clone() + MsgP := suite.G2().Point().(kyber.HashablePoint).Hash(msg) + if !suite.ValidatePairing(base, sigP, pubkeyP, MsgP) { + t.Fatalf("Error validating pairing") + } +} + +func TestImplementInterfaces(_ *testing.T) { + var _ kyber.Point = &G1Elt{} + var _ kyber.Point = &G2Elt{} + var _ kyber.Point = >Elt{} + var _ kyber.HashablePoint = &G1Elt{} + var _ kyber.HashablePoint = &G2Elt{} + // var _ kyber.hashablePoint = &KyberGT{} // GT is not hashable for now + var _ kyber.Group = &groupBls{} + var _ pairing.Suite = &Suite{} +} diff --git a/sim/adversary/decide.go b/sim/adversary/decide.go index c9e4a5d0..815aba05 100644 --- a/sim/adversary/decide.go +++ b/sim/adversary/decide.go @@ -97,30 +97,32 @@ func (i *ImmediateDecide) StartInstanceAt(instance uint64, _when time.Time) erro } var ( - pubkeys []gpbft.PubKey - sigs [][]byte + mask []int + sigs [][]byte ) if err := signers.ForEach(func(j uint64) error { - pubkey := gpbft.PubKey("fake pubkeyaaaaa") - sig := []byte("fake sig") - if j < uint64(len(committee.PowerTable.Entries)) { - pubkey = committee.PowerTable.Entries[j].PubKey - var err error - sig, err = i.host.Sign(context.Background(), pubkey, sigPayload) - if err != nil { - return err - } + if j >= uint64(len(committee.PowerTable.Entries)) { + return nil + } + pubkey := committee.PowerTable.Entries[j].PubKey + sig, err := i.host.Sign(context.Background(), pubkey, sigPayload) + if err != nil { + return err } - pubkeys = append(pubkeys, pubkey) + mask = append(mask, int(j)) sigs = append(sigs, sig) return nil }); err != nil { panic(err) } - aggregatedSig, err := i.host.Aggregate(pubkeys, sigs) + agg, err := i.host.Aggregate(committee.PowerTable.Entries.PublicKeys()) + if err != nil { + panic(err) + } + aggregatedSig, err := agg.Aggregate(mask, sigs) if err != nil { panic(err) } diff --git a/sim/adversary/withhold.go b/sim/adversary/withhold.go index 0610b763..211a667f 100644 --- a/sim/adversary/withhold.go +++ b/sim/adversary/withhold.go @@ -106,15 +106,19 @@ func (w *WithholdCommit) StartInstanceAt(instance uint64, _when time.Time) error sort.Ints(signers) signatures := make([][]byte, 0) - pubKeys := make([]gpbft.PubKey, 0) + mask := make([]int, 0) prepareMarshalled := w.host.MarshalPayloadForSigning(w.host.NetworkName(), &preparePayload) for _, signerIndex := range signers { entry := committee.PowerTable.Entries[signerIndex] signatures = append(signatures, w.sign(entry.PubKey, prepareMarshalled)) - pubKeys = append(pubKeys, entry.PubKey) + mask = append(mask, signerIndex) justification.Signers.Set(uint64(signerIndex)) } - justification.Signature, err = w.host.Aggregate(pubKeys, signatures) + agg, err := w.host.Aggregate(committee.PowerTable.Entries.PublicKeys()) + if err != nil { + panic(err) + } + justification.Signature, err = agg.Aggregate(mask, signatures) if err != nil { panic(err) } diff --git a/sim/ec.go b/sim/ec.go index e6b9c81b..3d39453f 100644 --- a/sim/ec.go +++ b/sim/ec.go @@ -36,8 +36,9 @@ type ECInstance struct { // SupplementalData is the additional data for this instance. SupplementalData *gpbft.SupplementalData - ec *simEC - decisions map[gpbft.ActorID]*gpbft.Justification + ec *simEC + decisions map[gpbft.ActorID]*gpbft.Justification + aggregateVerifier gpbft.Aggregate } type errGroup []error @@ -64,6 +65,12 @@ func (ec *simEC) BeginInstance(baseChain gpbft.ECChain, pt *gpbft.PowerTable) *E // Note a real beacon value will come from a finalised chain with some lookback. beacon := baseChain.Head().Key nextInstanceID := uint64(ec.Len()) + + agg, err := ec.verifier.Aggregate(pt.Entries.PublicKeys()) + if err != nil { + panic(err) + } + instance := &ECInstance{ Instance: nextInstanceID, BaseChain: baseChain, @@ -72,8 +79,9 @@ func (ec *simEC) BeginInstance(baseChain gpbft.ECChain, pt *gpbft.PowerTable) *E SupplementalData: &gpbft.SupplementalData{ PowerTable: gpbft.MakeCid([]byte(fmt.Sprintf("supp-data-pt@%d", nextInstanceID))), }, - ec: ec, - decisions: make(map[gpbft.ActorID]*gpbft.Justification), + ec: ec, + aggregateVerifier: agg, + decisions: make(map[gpbft.ActorID]*gpbft.Justification), } ec.instances = append(ec.instances, instance) return instance @@ -123,14 +131,14 @@ func (eci *ECInstance) validateDecision(decision *gpbft.Justification) error { // Extract signers. justificationPower := gpbft.NewStoragePower(0) - signers := make([]gpbft.PubKey, 0) + signers := make([]int, 0) powerTable := eci.PowerTable if err := decision.Signers.ForEach(func(bit uint64) error { if int(bit) >= len(powerTable.Entries) { return fmt.Errorf("invalid signer index: %d", bit) } justificationPower = big.Add(justificationPower, powerTable.Entries[bit].Power) - signers = append(signers, powerTable.Entries[bit].PubKey) + signers = append(signers, int(bit)) return nil }); err != nil { return fmt.Errorf("failed to iterate over signers: %w", err) @@ -144,7 +152,8 @@ func (eci *ECInstance) validateDecision(decision *gpbft.Justification) error { } // Verify aggregate signature payload := eci.ec.verifier.MarshalPayloadForSigning(eci.ec.networkName, &decision.Vote) - if err := eci.ec.verifier.VerifyAggregate(payload, decision.Signature, signers); err != nil { + + if err := eci.aggregateVerifier.VerifyAggregate(signers, payload, decision.Signature); err != nil { return fmt.Errorf("invalid aggregate signature: %v: %w", decision, err) } diff --git a/sim/host.go b/sim/host.go index 75330f19..0cdb983b 100644 --- a/sim/host.go +++ b/sim/host.go @@ -90,7 +90,9 @@ func (v *simHost) GetCommittee(instance uint64) (*gpbft.Committee, error) { return nil, ErrInstanceUnavailable } return &gpbft.Committee{ - PowerTable: i.PowerTable, Beacon: i.Beacon, + PowerTable: i.PowerTable, + Beacon: i.Beacon, + AggregateVerifier: i.aggregateVerifier, }, nil } diff --git a/sim/justification.go b/sim/justification.go index 336c2f02..d75e2f53 100644 --- a/sim/justification.go +++ b/sim/justification.go @@ -67,6 +67,8 @@ func MakeJustification(backend signing.Backend, nn gpbft.NetworkName, chain gpbf slices.SortFunc(votes, func(a, b vote) int { return cmp.Compare(a.index, b.index) }) + signers = signers[:len(votes)] + slices.Sort(signers) pks := make([]gpbft.PubKey, len(votes)) sigs := make([][]byte, len(votes)) for i, vote := range votes { @@ -74,7 +76,12 @@ func MakeJustification(backend signing.Backend, nn gpbft.NetworkName, chain gpbf sigs[i] = vote.sig } - sig, err := backend.Aggregate(pks, sigs) + agg, err := backend.Aggregate(powerTable.PublicKeys()) + if err != nil { + return nil, err + } + + sig, err := agg.Aggregate(signers, sigs) if err != nil { return nil, err } diff --git a/sim/signing/bls.go b/sim/signing/bls.go index e9a3878d..10ac51e2 100644 --- a/sim/signing/bls.go +++ b/sim/signing/bls.go @@ -5,11 +5,12 @@ import ( "errors" "sync" - bls12381 "github.com/drand/kyber-bls12381" - "github.com/drand/kyber/pairing" - "github.com/drand/kyber/sign/bdn" "github.com/filecoin-project/go-f3/blssig" "github.com/filecoin-project/go-f3/gpbft" + "go.dedis.ch/kyber/v4/pairing" + + "github.com/filecoin-project/go-f3/internal/bls/bdn" + bls12381 "github.com/filecoin-project/go-f3/internal/bls/gnark" ) var _ Backend = (*BLSBackend)(nil) @@ -36,7 +37,7 @@ func (b *BLSBackend) Sign(ctx context.Context, sender gpbft.PubKey, msg []byte) } func NewBLSBackend() *BLSBackend { - suite := bls12381.NewBLS12381Suite() + suite := bls12381.NewSuiteBLS12381() return &BLSBackend{ Verifier: blssig.VerifierWithKeyOnG1(), signersByPubKey: make(map[string]*blssig.Signer), diff --git a/sim/signing/fake.go b/sim/signing/fake.go index c962101e..2d107e89 100644 --- a/sim/signing/fake.go +++ b/sim/signing/fake.go @@ -76,35 +76,18 @@ func (s *FakeBackend) Verify(signer gpbft.PubKey, msg, sig []byte) error { } } -func (*FakeBackend) Aggregate(signers []gpbft.PubKey, sigs [][]byte) ([]byte, error) { - if len(signers) != len(sigs) { - return nil, errors.New("public keys and signatures length mismatch") - } - hasher := sha256.New() - for i, signer := range signers { +func (s *FakeBackend) Aggregate(keys []gpbft.PubKey) (gpbft.Aggregate, error) { + for i, signer := range keys { if len(signer) != 16 { - return nil, fmt.Errorf("wrong signer pubkey length: %d != 16", len(signer)) + return nil, fmt.Errorf("wrong signer %d pubkey length: %d != 16", i, len(signer)) } - hasher.Write(signer) - hasher.Write(sigs[i]) } - return hasher.Sum(nil), nil -} -func (s *FakeBackend) VerifyAggregate(payload, aggSig []byte, signers []gpbft.PubKey) error { - hasher := sha256.New() - for _, signer := range signers { - sig, err := s.generateSignature(signer, payload) - if err != nil { - return err - } - hasher.Write(signer) - hasher.Write(sig) - } - if !bytes.Equal(aggSig, hasher.Sum(nil)) { - return errors.New("signature is not valid") - } - return nil + return &fakeAggregate{ + keys: keys, + backend: s, + }, nil + } func (v *FakeBackend) MarshalPayloadForSigning(nn gpbft.NetworkName, p *gpbft.Payload) []byte { @@ -142,3 +125,44 @@ func (v *FakeBackend) MarshalPayloadForSigning(nn gpbft.NetworkName, p *gpbft.Pa } return buf.Bytes() } + +type fakeAggregate struct { + keys []gpbft.PubKey + backend *FakeBackend +} + +// Aggregate implements gpbft.Aggregate. +func (f *fakeAggregate) Aggregate(signerMask []int, sigs [][]byte) ([]byte, error) { + if len(signerMask) != len(sigs) { + return nil, errors.New("public keys and signatures length mismatch") + } + hasher := sha256.New() + for i, bit := range signerMask { + if bit >= len(f.keys) { + return nil, fmt.Errorf("signer %d out of range", bit) + } + binary.Write(hasher, binary.BigEndian, int64(bit)) + hasher.Write(f.keys[bit]) + hasher.Write(sigs[i]) + } + return hasher.Sum(nil), nil +} + +// VerifyAggregate implements gpbft.Aggregate. +func (f *fakeAggregate) VerifyAggregate(signerMask []int, payload []byte, aggSig []byte) error { + hasher := sha256.New() + for _, bit := range signerMask { + signer := f.keys[bit] + sig, err := f.backend.generateSignature(signer, payload) + if err != nil { + return err + } + binary.Write(hasher, binary.BigEndian, int64(bit)) + hasher.Write(signer) + hasher.Write(sig) + } + if !bytes.Equal(aggSig, hasher.Sum(nil)) { + return errors.New("signature is not valid") + } + return nil +} diff --git a/test/signing_suite_test.go b/test/signing_suite_test.go index 95dd9cb9..9d26a3f1 100644 --- a/test/signing_suite_test.go +++ b/test/signing_suite_test.go @@ -5,13 +5,14 @@ import ( "slices" "testing" - bls12381 "github.com/drand/kyber-bls12381" - "github.com/drand/kyber/sign/bdn" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + "github.com/filecoin-project/go-f3/blssig" "github.com/filecoin-project/go-f3/gpbft" + "github.com/filecoin-project/go-f3/internal/bls/bdn" + bls12381 "github.com/filecoin-project/go-f3/internal/bls/gnark" "github.com/filecoin-project/go-f3/sim/signing" - "github.com/stretchr/testify/require" - "github.com/stretchr/testify/suite" ) type ( @@ -25,7 +26,7 @@ type ( func TestBLSSigning(t *testing.T) { var ( - blsSuit = bls12381.NewBLS12381Suite() + blsSuit = bls12381.NewSuiteBLS12381() blsSchema = bdn.NewSchemeOnG2(blsSuit) ) t.Parallel() @@ -89,44 +90,60 @@ func (s *SigningTestSuite) TestAggregateAndVerify() { pubKey2, signer2 := s.signerTestSubject(s.T()) pubKeys := []gpbft.PubKey{pubKey1, pubKey2} + aggregator, err := s.verifier.Aggregate(pubKeys) + require.NoError(s.T(), err) + + mask := []int{0, 1} sigs := make([][]byte, len(pubKeys)) - var err error sigs[0], err = signer1.Sign(ctx, pubKey1, msg) require.NoError(s.T(), err) sigs[1], err = signer2.Sign(ctx, pubKey2, msg) require.NoError(s.T(), err) - aggSig, err := s.verifier.Aggregate(pubKeys, sigs) + aggSig, err := aggregator.Aggregate(mask, sigs) require.NoError(t, err) - err = s.verifier.VerifyAggregate(msg, aggSig, pubKeys) + err = aggregator.VerifyAggregate(mask, msg, aggSig) require.NoError(t, err) - aggSig, err = s.verifier.Aggregate(pubKeys[0:1], sigs[0:1]) + aggSig, err = aggregator.Aggregate(mask[0:1], sigs[0:1]) require.NoError(t, err) - err = s.verifier.VerifyAggregate(msg, aggSig, pubKeys) + err = aggregator.VerifyAggregate(mask, msg, aggSig) require.Error(t, err) - aggSig, err = s.verifier.Aggregate(pubKeys, [][]byte{sigs[0], sigs[0]}) + aggSig, err = aggregator.Aggregate(mask, [][]byte{sigs[0], sigs[0]}) require.NoError(t, err) - err = s.verifier.VerifyAggregate(msg, aggSig, pubKeys) + err = aggregator.VerifyAggregate(mask, msg, aggSig) require.Error(t, err) - err = s.verifier.VerifyAggregate(msg, []byte("bad sig"), pubKeys) + err = aggregator.VerifyAggregate(mask, msg, []byte("bad sig")) require.Error(t, err) - _, err = s.verifier.Aggregate(pubKeys, [][]byte{sigs[0]}) + _, err = aggregator.Aggregate(mask, [][]byte{sigs[0]}) require.Error(t, err, "Missmatched pubkeys and sigs lengths should fail") { pubKeys2 := slices.Clone(pubKeys) - pubKeys2[0] = slices.Clone(pubKeys2[0]) - pubKeys2[0] = pubKeys2[0][1:len(pubKeys2)] - _, err = s.verifier.Aggregate(pubKeys2, sigs) - require.Error(t, err, "damaged pubkey should error") + pubKey3, _ := s.signerTestSubject(s.T()) + pubKeys2[0] = pubKey3 + wrongKeyAggregator, err := s.verifier.Aggregate(pubKeys2) + require.NoError(t, err) - require.Error(t, s.verifier.VerifyAggregate(msg, aggSig, pubKeys2), "damaged pubkey should error") + require.Error(t, wrongKeyAggregator.VerifyAggregate(mask, msg, aggSig), "wrong pubkey should error") } + + t.Run("mask out of range", func(t *testing.T) { + _, err = aggregator.Aggregate([]int{0, 3}, [][]byte{sigs[0]}) + require.Error(t, err, "mask out of range") + }) + + t.Run("empty signature is always valid", func(t *testing.T) { + sig, err := aggregator.Aggregate([]int{}, [][]byte{}) + require.NoError(t, err) + + err = aggregator.VerifyAggregate([]int{}, []byte("anything"), sig) + require.NoError(t, err) + }) }