Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add bls12381 #1

Merged
merged 5 commits into from
Mar 12, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: Test
on:
pull_request:
merge_group:
push:
paths:
- "**.go"
branches:
- main
- release/**

jobs:
tests:
runs-on: ubuntu-latest
strategy:
fail-fast: false
steps:
- uses: actions/setup-go@v5
with:
go-version: "1.21"
- uses: actions/checkout@v4

- name: Run Go Tests
run: |
go test -v -race ./... -tags bls12381
3 changes: 3 additions & 0 deletions READEME.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Crypto

The Cosmos crypto repository serves as an alignment point for the Cosmos crypto users (Cosmos SDK, CometBFT, etc..).
9 changes: 9 additions & 0 deletions curves/bls12381/alias.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
//go:build ((linux && amd64) || (linux && arm64) || (darwin && amd64) || (darwin && arm64) || (windows && amd64)) && bls12381

package blst

import blst "github.com/supranational/blst/bindings/go"

// Internal types for blst.
type blstPublicKey = blst.P1Affine
type blstSignature = blst.P2Affine
6 changes: 6 additions & 0 deletions curves/bls12381/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Package blst implements a go-wrapper around a library implementing the
// BLS12-381 curve and signature scheme. This package exposes a public API for
// verifying and aggregating BLS signatures used by Ethereum.
//
// This implementation uses the library written by Supranational, blst.
package blst
15 changes: 15 additions & 0 deletions curves/bls12381/helper_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//go:build ((linux && amd64) || (linux && arm64) || (darwin && amd64) || (darwin && arm64) || (windows && amd64)) && bls12381

package blst

// Note: These functions are for tests to access private globals, such as pubkeyCache.

// DisableCaches sets the cache sizes to 0.
func DisableCaches() {
pubkeyCache.Resize(0)
}

// EnableCaches sets the cache sizes to the default values.
func EnableCaches() {
pubkeyCache.Resize(maxKeys)
}
27 changes: 27 additions & 0 deletions curves/bls12381/init.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
//go:build ((linux && amd64) || (linux && arm64) || (darwin && amd64) || (darwin && arm64) || (windows && amd64)) && bls12381

package blst

import (
"fmt"
"runtime"

blst "github.com/supranational/blst/bindings/go"

"github.com/cosmos/crypto/utils/cache"
)

func init() {
// Reserve 1 core for general application work
maxProcs := runtime.GOMAXPROCS(0) - 1

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

uber go maxprocs is good here

if maxProcs <= 0 {
maxProcs = 1
}
blst.SetMaxProcs(maxProcs)
onEvict := func(_ [48]byte, _ PubKey) {}
keysCache, err := cache.NewLRU(maxKeys, onEvict)
if err != nil {
panic(fmt.Sprintf("Could not initiate public keys cache: %v", err))
}
pubkeyCache = keysCache
}
21 changes: 21 additions & 0 deletions curves/bls12381/interface.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package blst

type PubKey interface {
Marshal() []byte
Copy() PubKey
Equals(p2 PubKey) bool
}

// SignatureI represents a BLS signature.
type SignatureI interface {
Verify(pubKey PubKey, msg []byte) bool
Marshal() []byte
Copy() SignatureI
}

// SecretKey represents a BLS secret or private key.
type SecretKey interface {
PublicKey() PubKey
Sign(msg []byte) SignatureI
Marshal() []byte
}
76 changes: 76 additions & 0 deletions curves/bls12381/pubkey.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
//go:build ((linux && amd64) || (linux && arm64) || (darwin && amd64) || (darwin && arm64) || (windows && amd64)) && bls12381

package blst

import (
"errors"
"fmt"

"github.com/cosmos/crypto/utils/cache"
)

const (
SignatureLength = 96
PubkeyLength = 48 // PubkeyLength defines the byte length of a BLSSignature.
)

var maxKeys = 2_000_000
var pubkeyCache *cache.LRU[[48]byte, PubKey]

// PublicKey used in the BLS signature scheme.
type PublicKey struct {
p *blstPublicKey
}

// Marshal a public key into a LittleEndian byte slice.
func (p *PublicKey) Marshal() []byte {
return p.p.Compress()
}

// Copy the public key to a new pointer reference.
func (p *PublicKey) Copy() PubKey {
np := *p.p
return &PublicKey{p: &np}
}

// Equals checks if the provided public key is equal to
// the current one.
func (p *PublicKey) Equals(p2 PubKey) bool {
return p.p.Equals(p2.(*PublicKey).p)
}

// PublicKeyFromBytes creates a BLS public key from a BigEndian byte slice.
func PublicKeyFromBytes(pubKey []byte) (PubKey, error) {
return publicKeyFromBytes(pubKey, true)
}

func publicKeyFromBytes(pubKey []byte, cacheCopy bool) (PubKey, error) {
if len(pubKey) != PubkeyLength { //TODO: make this a parameter
return nil, fmt.Errorf("public key must be %d bytes", PubkeyLength)
}

newKey := (*[PubkeyLength]byte)(pubKey)
if cv, ok := pubkeyCache.Get(*newKey); ok {
if cacheCopy {
return cv.Copy(), nil
}
return cv, nil
}

// Subgroup check NOT done when decompressing pubkey.
p := new(blstPublicKey).Uncompress(pubKey)
if p == nil {
return nil, errors.New("could not unmarshal bytes into public key")
}
// Subgroup and infinity check
if !p.KeyValidate() {
// NOTE: the error is not quite accurate since it includes group check
return nil, errors.New("publickey is infinite")
}

pubKeyObj := &PublicKey{p: p}
copiedKey := pubKeyObj.Copy()
cacheKey := *newKey
pubkeyCache.Add(cacheKey, copiedKey)
return pubKeyObj, nil
}
97 changes: 97 additions & 0 deletions curves/bls12381/pubkey_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
//go:build ((linux && amd64) || (linux && arm64) || (darwin && amd64) || (darwin && arm64) || (windows && amd64)) && bls12381

package blst_test

import (
"bytes"
"errors"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

blst "github.com/cosmos/crypto/curves/bls12381"
)

func TestPublicKeyFromBytes(t *testing.T) {
tests := []struct {
name string
input []byte
err error
}{
{
name: "Nil",
err: errors.New("public key must be 48 bytes"),
},
{
name: "Empty",
input: []byte{},
err: errors.New("public key must be 48 bytes"),
},
{
name: "Short",
input: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
err: errors.New("public key must be 48 bytes"),
},
{
name: "Long",
input: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
err: errors.New("public key must be 48 bytes"),
},
{
name: "Bad",
input: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
err: errors.New("could not unmarshal bytes into public key"),
},
{
name: "Good",
input: []byte{0xa9, 0x9a, 0x76, 0xed, 0x77, 0x96, 0xf7, 0xbe, 0x22, 0xd5, 0xb7, 0xe8, 0x5d, 0xee, 0xb7, 0xc5, 0x67, 0x7e, 0x88, 0xe5, 0x11, 0xe0, 0xb3, 0x37, 0x61, 0x8f, 0x8c, 0x4e, 0xb6, 0x13, 0x49, 0xb4, 0xbf, 0x2d, 0x15, 0x3f, 0x64, 0x9f, 0x7b, 0x53, 0x35, 0x9f, 0xe8, 0xb9, 0x4a, 0x38, 0xe4, 0x4c},
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
res, err := blst.PublicKeyFromBytes(test.input)
if test.err != nil {
assert.NotEqual(t, nil, err, "No error returned")
assert.ErrorContains(t, test.err, err.Error(), "Unexpected error returned")
} else {
assert.NoError(t, err)
assert.Equal(t, 0, bytes.Compare(res.Marshal(), test.input))
}
})
}
}

func TestPublicKey_Copy(t *testing.T) {
priv, err := blst.RandKey()
require.NoError(t, err)
pubkeyA := priv.PublicKey()
pubkeyBytes := pubkeyA.Marshal()

require.Equal(t, pubkeyA.Marshal(), pubkeyBytes, "Pubkey was mutated after copy")
}

func BenchmarkPublicKeyFromBytes(b *testing.B) {
priv, err := blst.RandKey()
require.NoError(b, err)
pubkey := priv.PublicKey()
pubkeyBytes := pubkey.Marshal()

b.Run("cache on", func(b *testing.B) {
blst.EnableCaches()
for i := 0; i < b.N; i++ {
_, err := blst.PublicKeyFromBytes(pubkeyBytes)
require.NoError(b, err)
}
})

b.Run("cache off", func(b *testing.B) {
// blst.DisableCaches()
for i := 0; i < b.N; i++ {
_, err := blst.PublicKeyFromBytes(pubkeyBytes)
require.NoError(b, err)
}
})

}
75 changes: 75 additions & 0 deletions curves/bls12381/secret_key.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
//go:build ((linux && amd64) || (linux && arm64) || (darwin && amd64) || (darwin && arm64) || (windows && amd64)) && bls12381

package blst

import (
"crypto/subtle"
"errors"
"fmt"

blst "github.com/supranational/blst/bindings/go"

"github.com/cosmos/crypto/utils/rand"
)

// bls12SecretKey used in the BLS signature scheme.
type bls12SecretKey struct {
p *blst.SecretKey

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there a downside to embedding this?

}

// RandKey creates a new private key using a random method provided as an io.Reader.
func RandKey() (SecretKey, error) {
// Generate 32 bytes of randomness
var ikm [32]byte
_, err := rand.NewGenerator().Read(ikm[:])

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can one liner this

if err != nil {
return nil, err
}
// Defensive check, that we have not generated a secret key,
secKey := &bls12SecretKey{blst.KeyGen(ikm[:])}
if IsZero(secKey.Marshal()) {
return nil, errors.New("received secret key is zero")
}
return secKey, nil
}

// SecretKeyFromBytes creates a BLS private key from a BigEndian byte slice.
func SecretKeyFromBytes(privKey []byte) (SecretKey, error) {
if len(privKey) != 32 {
return nil, fmt.Errorf("secret key must be %d bytes", 32)
}
if IsZero(privKey) {
return nil, errors.New("received secret key is zero")
}
secKey := new(blst.SecretKey).Deserialize(privKey)
if secKey == nil {
return nil, errors.New("could not unmarshal bytes into secret key")
}
wrappedKey := &bls12SecretKey{p: secKey}
return wrappedKey, nil
}

// IsZero checks if the secret key is a zero key.
func IsZero(sKey []byte) bool {
b := byte(0)
for _, s := range sKey {
b |= s
}
return subtle.ConstantTimeByteEq(b, 0) == 1
}

func (s *bls12SecretKey) Sign(msg []byte) SignatureI {
signature := new(blstSignature).Sign(s.p, msg, dst)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can 1 liner

return &Signature{s: signature}
}

// Marshal a secret key into a LittleEndian byte slice.
func (s *bls12SecretKey) Marshal() []byte {
keyBytes := s.p.Serialize()

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 liner

return keyBytes
}

// PublicKey obtains the public key corresponding to the BLS secret key.
func (s *bls12SecretKey) PublicKey() PubKey {
return &PublicKey{p: new(blstPublicKey).From(s.p)}
}
Loading
Loading