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: BLS Key Separation and ERC2335 Implementation #396

Merged
Show file tree
Hide file tree
Changes from 6 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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)

### Improvements

- [#396](https://github.com/babylonlabs-io/babylon/pull/396) BLS Key Separation and ERC2335 Implementation
- [#391](https://github.com/babylonlabs-io/babylon/pull/391) Fix e2e `TestBTCRewardsDistribution` flunky
check of rewards

Expand Down
26 changes: 18 additions & 8 deletions app/signer/private.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import (
"path/filepath"

cmtconfig "github.com/cometbft/cometbft/config"
cmtos "github.com/cometbft/cometbft/libs/os"

"github.com/babylonlabs-io/babylon/privval"
cmtprivval "github.com/cometbft/cometbft/privval"
)

type PrivSigner struct {
Expand All @@ -15,17 +15,27 @@ type PrivSigner struct {

func InitPrivSigner(nodeDir string) (*PrivSigner, error) {
nodeCfg := cmtconfig.DefaultConfig()
blsCfg := privval.DefaultBlsConfig()

pvKeyFile := filepath.Join(nodeDir, nodeCfg.PrivValidatorKeyFile())
err := cmtos.EnsureDir(filepath.Dir(pvKeyFile), 0777)
if err != nil {
return nil, err
}
pvStateFile := filepath.Join(nodeDir, nodeCfg.PrivValidatorStateFile())
err = cmtos.EnsureDir(filepath.Dir(pvStateFile), 0777)
if err != nil {
blsKeyFile := filepath.Join(nodeDir, blsCfg.BlsKeyFile())
blsPasswordFile := filepath.Join(nodeDir, blsCfg.BlsPasswordFile())

if err := privval.IsValidFilePath(pvKeyFile, pvStateFile, blsKeyFile, blsPasswordFile); err != nil {
return nil, err
}
wrappedPV := privval.LoadOrGenWrappedFilePV(pvKeyFile, pvStateFile)

cometPV := cmtprivval.LoadFilePV(pvKeyFile, pvStateFile)
blsPV := privval.LoadBlsPV(blsKeyFile, blsPasswordFile)

wrappedPV := &privval.WrappedFilePV{
Key: privval.WrappedFilePVKey{
CometPVKey: cometPV.Key,
BlsPVKey: blsPV.Key,
},
LastSignState: cometPV.LastSignState,
gitferry marked this conversation as resolved.
Show resolved Hide resolved
}

return &PrivSigner{
WrappedPV: wrappedPV,
Expand Down
4 changes: 2 additions & 2 deletions app/test_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -239,14 +239,14 @@ func SetupWithBitcoinConf(t *testing.T, isCheckTx bool, btcConf bbn.SupportedBtc

ps, err := signer.SetupTestPrivSigner()
require.NoError(t, err)
valPubKey := ps.WrappedPV.Key.PubKey
valPubKey := ps.WrappedPV.Key.CometPVKey.PubKey
// generate genesis account
acc := authtypes.NewBaseAccount(valPubKey.Address().Bytes(), &cosmosed.PubKey{Key: valPubKey.Bytes()}, 0, 0)
balance := banktypes.Balance{
Address: acc.GetAddress().String(),
Coins: sdk.NewCoins(sdk.NewCoin(appparams.DefaultBondDenom, math.NewInt(100000000000000))),
}
ps.WrappedPV.Key.DelegatorAddress = acc.GetAddress().String()
ps.WrappedPV.Key.BlsPVKey.DelegatorAddress = acc.GetAddress().String()
// create validator set with single validator
genesisKey, err := signer.GenesisKeyFromPrivSigner(ps)
require.NoError(t, err)
Expand Down
5 changes: 5 additions & 0 deletions client/flags/flags.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package flags

const (
FlagBlsPassword = "bls-password"
gitferry marked this conversation as resolved.
Show resolved Hide resolved
)
29 changes: 5 additions & 24 deletions cmd/babylond/cmd/create_bls_key.go
Original file line number Diff line number Diff line change
@@ -1,20 +1,16 @@
package cmd

import (
"errors"
"fmt"
"path/filepath"
"strings"

cmtconfig "github.com/cometbft/cometbft/config"
cmtos "github.com/cometbft/cometbft/libs/os"
"github.com/cosmos/cosmos-sdk/client/flags"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/spf13/cobra"

"github.com/babylonlabs-io/babylon/app"
appparams "github.com/babylonlabs-io/babylon/app/params"
"github.com/babylonlabs-io/babylon/crypto/bls12381"
"github.com/babylonlabs-io/babylon/privval"
)

Expand Down Expand Up @@ -57,28 +53,13 @@ $ babylond create-bls-key %s1f5tnl46mk4dfp4nx3n2vnrvyw2h2ydz6ykhk3r --home ./
}

func CreateBlsKey(home string, addr sdk.AccAddress) error {
nodeCfg := cmtconfig.DefaultConfig()
keyPath := filepath.Join(home, nodeCfg.PrivValidatorKeyFile())
statePath := filepath.Join(home, nodeCfg.PrivValidatorStateFile())

pv, err := LoadWrappedFilePV(keyPath, statePath)
if err != nil {
return err
}
blsCfg := privval.DefaultBlsConfig()
gitferry marked this conversation as resolved.
Show resolved Hide resolved
keyPath := filepath.Join(home, blsCfg.BlsKeyFile())
passwordPath := filepath.Join(home, blsCfg.BlsPasswordFile())
gitferry marked this conversation as resolved.
Show resolved Hide resolved

wrappedPV := privval.NewWrappedFilePV(pv.GetValPrivKey(), bls12381.GenPrivKey(), keyPath, statePath)
wrappedPV.SetAccAddress(addr)
password := privval.GetBlsPassword()

privval.GenBlsPV(keyPath, passwordPath, password, addr.String())
return nil
}

// LoadWrappedFilePV loads the wrapped file private key from the file path.
func LoadWrappedFilePV(keyPath, statePath string) (*privval.WrappedFilePV, error) {
if !cmtos.FileExists(keyPath) {
return nil, errors.New("validator key file does not exist")
}
if !cmtos.FileExists(statePath) {
return nil, errors.New("validator state file does not exist")
}
return privval.LoadWrappedFilePV(keyPath, statePath), nil
}
30 changes: 26 additions & 4 deletions cmd/babylond/cmd/genhelpers/bls_add_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package genhelpers_test
import (
"context"
"fmt"
"os"
"path/filepath"
"testing"

Expand All @@ -20,6 +21,7 @@ import (
cmtconfig "github.com/cometbft/cometbft/config"
tmjson "github.com/cometbft/cometbft/libs/json"
"github.com/cometbft/cometbft/libs/tempfile"
cmtprivval "github.com/cometbft/cometbft/privval"
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/flags"
"github.com/cosmos/cosmos-sdk/server"
Expand Down Expand Up @@ -147,13 +149,26 @@ func Test_CmdAddBlsWithGentx(t *testing.T) {
v := testNetwork.Validators[i]
// build and create genesis BLS key
genBlsCmd := genhelpers.CmdCreateBls()
nodeCfg := cmtconfig.DefaultConfig()
homeDir := filepath.Join(v.Dir, "simd")

nodeCfg := cmtconfig.DefaultConfig()
nodeCfg.SetRoot(homeDir)
blsCfg := privval.DefaultBlsConfig()
blsCfg.SetRoot(homeDir)

keyPath := nodeCfg.PrivValidatorKeyFile()
statePath := nodeCfg.PrivValidatorStateFile()
filePV := privval.GenWrappedFilePV(keyPath, statePath)
filePV.SetAccAddress(v.Address)
blsKeyFile := blsCfg.BlsKeyFile()
blsPasswordFile := blsCfg.BlsPasswordFile()

err := privval.IsValidFilePath(keyPath, statePath, blsKeyFile, blsPasswordFile)
require.NoError(t, err)

filePV := cmtprivval.GenFilePV(keyPath, statePath)
filePV.Key.Save()
filePV.LastSignState.Save()
privval.GenBlsPV(blsKeyFile, blsPasswordFile, "password", v.Address.String())

_, err = cli.ExecTestCLICmd(v.ClientCtx, genBlsCmd, []string{fmt.Sprintf("--%s=%s", flags.FlagHome, homeDir)})
require.NoError(t, err)
genKeyFileName := filepath.Join(filepath.Dir(keyPath), fmt.Sprintf("gen-bls-%s.json", v.ValAddress))
Expand All @@ -175,6 +190,13 @@ func Test_CmdAddBlsWithGentx(t *testing.T) {
require.NotEmpty(t, checkpointingGenState.GenesisKeys)
gks := checkpointingGenState.GetGenesisKeys()
require.Equal(t, genKey, gks[i])
filePV.Clean(keyPath, statePath)
Clean(keyPath, statePath, blsKeyFile, blsPasswordFile)
}
}

// Clean removes PVKey file and PVState file
func Clean(paths ...string) {
for _, path := range paths {
_ = os.RemoveAll(filepath.Dir(path))
}
}
23 changes: 18 additions & 5 deletions cmd/babylond/cmd/genhelpers/bls_create.go
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
package genhelpers

import (
"errors"
"path/filepath"
"strings"

cmtconfig "github.com/cometbft/cometbft/config"
cmtos "github.com/cometbft/cometbft/libs/os"
"github.com/cosmos/cosmos-sdk/client/flags"
"github.com/spf13/cobra"

"github.com/babylonlabs-io/babylon/app"
"github.com/babylonlabs-io/babylon/privval"
cmtprivval "github.com/cometbft/cometbft/privval"
)

// CmdCreateBls CLI command to create BLS file with proof of possession.
Expand All @@ -35,13 +34,27 @@ $ babylond genbls --home ./
homeDir, _ := cmd.Flags().GetString(flags.FlagHome)

nodeCfg := cmtconfig.DefaultConfig()
blsCfg := privval.DefaultBlsConfig()

keyPath := filepath.Join(homeDir, nodeCfg.PrivValidatorKeyFile())
statePath := filepath.Join(homeDir, nodeCfg.PrivValidatorStateFile())
if !cmtos.FileExists(keyPath) {
return errors.New("validator key file does not exist")
blsKeyPath := filepath.Join(homeDir, blsCfg.BlsKeyFile())
blsPasswordPath := filepath.Join(homeDir, blsCfg.BlsPasswordFile())

if err := privval.IsValidFilePath(keyPath, statePath, blsKeyPath, blsPasswordPath); err != nil {
return err
}

wrappedPV := privval.LoadWrappedFilePV(keyPath, statePath)
filePV := cmtprivval.LoadFilePV(keyPath, statePath)
blsPV := privval.LoadBlsPV(blsKeyPath, blsPasswordPath)

wrappedPV := &privval.WrappedFilePV{
Key: privval.WrappedFilePVKey{
CometPVKey: filePV.Key,
BlsPVKey: blsPV.Key,
},
LastSignState: filePV.LastSignState,
}

outputFileName, err := wrappedPV.ExportGenBls(filepath.Dir(keyPath))
if err != nil {
Expand Down
21 changes: 17 additions & 4 deletions cmd/babylond/cmd/genhelpers/bls_create_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
"github.com/babylonlabs-io/babylon/privval"
"github.com/babylonlabs-io/babylon/testutil/signer"
"github.com/babylonlabs-io/babylon/x/checkpointing/types"
cmtprivval "github.com/cometbft/cometbft/privval"
)

func Test_CmdCreateBls(t *testing.T) {
Expand Down Expand Up @@ -70,21 +71,33 @@ func Test_CmdCreateBls(t *testing.T) {

// create BLS keys
nodeCfg := cmtconfig.DefaultConfig()
blsCfg := privval.DefaultBlsConfig()

keyPath := filepath.Join(home, nodeCfg.PrivValidatorKeyFile())
statePath := filepath.Join(home, nodeCfg.PrivValidatorStateFile())
filePV := privval.GenWrappedFilePV(keyPath, statePath)
defer filePV.Clean(keyPath, statePath)
filePV.SetAccAddress(addr)
blsKeyFile := filepath.Join(home, blsCfg.BlsKeyFile())
blsPasswordFile := filepath.Join(home, blsCfg.BlsPasswordFile())

err = privval.IsValidFilePath(keyPath, statePath, blsKeyFile, blsPasswordFile)
require.NoError(t, err)

filePV := cmtprivval.GenFilePV(keyPath, statePath)
filePV.Key.Save()

blsPV := privval.GenBlsPV(blsKeyFile, blsPasswordFile, "password", addr.String())
defer Clean(keyPath, statePath, blsKeyFile, blsPasswordFile)

// execute the gen-bls cmd
err = genBlsCmd.ExecuteContext(ctx)
require.NoError(t, err)
outputFilePath := filepath.Join(filepath.Dir(keyPath), fmt.Sprintf("gen-bls-%s.json", sdk.ValAddress(addr).String()))
require.NoError(t, err)
genKey, err := types.LoadGenesisKeyFromFile(outputFilePath)

require.NoError(t, err)
require.Equal(t, sdk.ValAddress(addr).String(), genKey.ValidatorAddress)
require.True(t, filePV.Key.BlsPubKey.Equal(*genKey.BlsKey.Pubkey))
require.Equal(t, filePV.Key.PubKey.Bytes(), genKey.ValPubkey.Bytes())
require.True(t, blsPV.Key.PubKey.Equal(*genKey.BlsKey.Pubkey))

require.True(t, genKey.BlsKey.Pop.IsValid(*genKey.BlsKey.Pubkey, genKey.ValPubkey))
}
83 changes: 83 additions & 0 deletions crypto/erc2335/erc2335.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package erc2335

import (
"crypto/rand"
"encoding/hex"
"encoding/json"
"fmt"
"os"

"github.com/pkg/errors"
keystorev4 "github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4"
)

type Erc2335KeyStore struct {
Crypto map[string]interface{} `json:"crypto"`
Copy link
Collaborator

Choose a reason for hiding this comment

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

Some doc for this field would be nice like what does this map hold (as type map[string]interface{} does not tell much to the reader)

Copy link
Author

Choose a reason for hiding this comment

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

Version uint `json:"version"`
UUID string `json:"uuid"`
Path string `json:"path"`
Pubkey string `json:"pubkey"`
Description string `json:"description"`
}

// wonjoon: encrypt key pair to erc2335 keystore
// available to handle all keys in []byte format
func Encrypt(privKey, pubKey []byte, password string) ([]byte, error) {
gitferry marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Member

Choose a reason for hiding this comment

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

nit: proper go doc

Copy link
Author

Choose a reason for hiding this comment

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

if privKey == nil {
return nil, errors.New("private key cannot be nil")
}

encryptor := keystorev4.New()
cryptoFields, err := encryptor.Encrypt(privKey, password)
if err != nil {
return nil, errors.Wrap(err, "failed to encrypt private key")
Copy link
Collaborator

Choose a reason for hiding this comment

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

usually in our codebases we are wrapping our errors with fmt.Errorf("error msg: %w", err) (which imo looks nicer though this is highly personal opinion), any reason for using errors ?

Copy link
Author

Choose a reason for hiding this comment

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

}

// Create the keystore json structure
keystoreJSON := Erc2335KeyStore{
Crypto: cryptoFields,
Version: 4,
Pubkey: fmt.Sprintf("%x", pubKey),
}

return json.Marshal(keystoreJSON)
}

func LoadKeyStore(filePath string) (Erc2335KeyStore, error) {
var keystore Erc2335KeyStore

keyJSONBytes, err := os.ReadFile(filePath)
if err != nil {
return Erc2335KeyStore{}, err
Copy link
Collaborator

Choose a reason for hiding this comment

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

lets wrap errors here with some additional useful info given that this is public function which is pretty low level

Copy link
Author

Choose a reason for hiding this comment

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

}

if err := json.Unmarshal(keyJSONBytes, &keystore); err != nil {
return Erc2335KeyStore{}, err
Copy link
Collaborator

Choose a reason for hiding this comment

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

lets wrap the error here

Copy link
Author

Choose a reason for hiding this comment

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

}

return keystore, nil
}

// decrypt private key from erc2335 keystore
func Decrypt(keystore Erc2335KeyStore, password string) ([]byte, error) {
encryptor := keystorev4.New()
return encryptor.Decrypt(keystore.Crypto, password)
}

func SavePasswordToFile(password, filePath string) error {
return os.WriteFile(filePath, []byte(password), 0600)
}

func LoadPaswordFromFile(filePath string) (string, error) {
password, err := os.ReadFile(filePath)
return string(password), err
}

func CreateRandomPassword() string {
password := make([]byte, 32)
_, err := rand.Read(password)
if err != nil {
panic(err)
Copy link
Collaborator

Choose a reason for hiding this comment

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

this is a bit risky in such low level public operation which does not know context of the application.

Maybe:

  • if it is only for testing, lets make it private
  • it is supposed to be public, lets return error

Copy link
Author

Choose a reason for hiding this comment

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

}
return hex.EncodeToString(password)
}
Loading