Skip to content

Commit

Permalink
feat: BLS Key Separation and ERC2335 Implementation
Browse files Browse the repository at this point in the history
## Description

This PR was created from epic #336, separates BLS key from
priv_validator_key.json, and applies ERC2335.

There are still issues in CI that are being resolved, but I'm uploading
a PR to first discuss the changed structure and ideas.

### ERC2335 structure

- Use
[keystorev4](github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4)
with [prysm](https://github.com/prysmaticlabs/prysm) as reference
- Add ERC2335 encryption/decryption and password generation functions

### Change WrappedFilePV to include BLS structure and use cometbft's
FilePV directly

```go
type WrappedFilePVKey struct {
	CometPVKey privval.FilePVKey
	BlsPVKey   BlsPVKey
}
```

### BLS separation

- At the time of `babylond init`, only priv_validator_key.json is
created.
- `babylond create-bls-key` creates a BLS key with the password entered
as the prompt and encrypts it into an ERC2335 structure.
- The generated BLS key in ERC2335 format is saved as `bls_key.json`,
and the password is saved as `bls_password.txt`.

An example of a `bls_key.json` is below:

```json
{
  "crypto": {
    "checksum": {
      "function": "sha256",
      "message": "d291102754ece0a5ab9d4c3c75f452069d192df6c90d616e466d7e7884f41b3a",
      "params": {}
    },
    "cipher": {
      "function": "aes-128-ctr",
      "message": "e638d3f04b803c3ec934c31749ed20c1ffcd48ab481b4fad6b303aa2e7ff991c",
      "params": {
        "iv": "473e315746ec9997f0d0e423f73e2d3d"
      }
    },
    "kdf": {
      "function": "pbkdf2",
      "message": "",
      "params": {
        "c": 262144,
        "dklen": 32,
        "prf": "hmac-sha256",
        "salt": "3f1b139f3663082f582acc6d48fd9bf8caf4ea9c4e51c6ef75c9bc5b5c04ee9e"
      }
    }
  },
  "version": 4,
  "uuid": "",
  "path": "",
  "pubkey": "a28b189e76e8d61461811eec22bc8d5c926bf1276afde5344a3b9d3a2769a1a61ea42471a867d4811a85640dfde8fed702d81bcc9f56494ec65471151fac6e3c262541245f1b16c9e22000eef2142c43f6ad5ca8c31fc16081261cc54d3f034c",
  "description": "bbn196xcr99y5eehdg6lr38u8re0j6xeq0twup5rl4"
}
```

An example of a `priv_validator_key.json` with BLS separated is below:

```json
{
  "address": "3141C6C8EF5FDAE49931AE2F9EB295F5C9E2212A",
  "pub_key": {
    "type": "tendermint/PubKeyEd25519",
    "value": "CwCehdhn8DmcVTKn4I1RHzlmur5LbEZxSEr+zvf7Ifo="
  },
  "priv_key": {
    "type": "tendermint/PrivKeyEd25519",
    "value": "HIHCMsU9+J3FjEU7gooSed5moKRjar6TfLa3Nb1CSCULAJ6F2GfwOZxVMqfgjVEfOWa6vktsRnFISv7O9/sh+g=="
  }
}
```

### Change DelegatorAddress to be stored in the description field of
ERC2335 structure instead of storing it in `priv_validator_key.json`.

- DelegatorAddress is stored in the description field of the
`bls_key.json` file in erc2335 format.

```go
type BlsPVKey struct {
	PubKey  bls12381.PublicKey  `json:"bls_pub_key"`
	PrivKey bls12381.PrivateKey `json:"bls_priv_key"`
	DelegatorAddress string
        // ...
}
```

## Test script

$ ./babylond init my-node --chain-id test-chain-1

$ ./babylond keys add validator1 --keyring-backend test

$ ./babylond add-genesis-account $(./babylond keys show validator1 -a
--keyring-backend test) 2000000000000ubbn,2000000000000stake

$ ./babylond gentx validator1 1500000000000stake --chain-id test-chain-1
--keyring-backend test --fees 400ubbn

$ ./babylond collect-gentxs

$ ./babylond create-bls-key $(./babylond keys show validator1 -a
--keyring-backend test)

$ ./babylond gen-helpers create-bls

After the create-bls command, json file starting with the prefix
"gen-bls" will be created in {home}/.babylond/config.

> example:
gen-bls-bbnvaloper196xcr99y5eehdg6lr38u8re0j6xeq0twpr77n5.json

Copy data in the created file to the 'checkpointing.genesis_keys' field
in genesis.json.

Here is an example after inserting data to 'checkpointing.genesis_keys'
field in genesis.json

```json
"checkpointing": {
      "genesis_keys": [
        {
          "validator_address": "bbnvaloper196xcr99y5eehdg6lr38u8re0j6xeq0twpr77n5",
          "bls_key": {
            "pubkey": "oosYnnbo1hRhgR7sIryNXJJr8Sdq/eU0SjudOidpoaYepCRxqGfUgRqFZA396P7XAtgbzJ9WSU7GVHEVH6xuPCYlQSRfGxbJ4iAA7vIULEP2rVyowx/BYIEmHMVNPwNM",
            "pop": {
              "ed25519_sig": "9YrVS1/KN0Fq6po0JbWirX+0enhxiShqxVw+lQFn22qam/2KaZXXzNMsOOOBoyQ9ZSD7ARc/TtohYBbhLIF/Cg==",
              "bls_sig": "pRAZUAH2ZDPZqgoiPTsFYTYndBctJXoY1lvN5GsWNNbPDYyIeXihaS28pqauHCcy"
            }
          },
          "val_pubkey": {
            "key": "CwCehdhn8DmcVTKn4I1RHzlmur5LbEZxSEr+zvf7Ifo="
          }
        }
      ]
    },
```

## Subsequent tasks

- Ensure the BLS key file is created atomically with
priv_validator_key.json
- BLS key non-validation logic for non-validator
- Remove DelegatorAddress from the `BlsPV` structure and obtain it from
`GetValidatorByConsAddr()` function of the Epoch(Staking) module.
- ~~Add the option to use the `--bls-password` flag when creating the
BLS key~~
[1f14b31](1f14b31)
- Modify not to keep `WrappedFilePV` since LastSignState is not used

```go
type WrappedFilePV struct {
	Key           WrappedFilePVKey
	LastSignState privval.FilePVLastSignState
}
```
  • Loading branch information
wnjoon authored Jan 15, 2025
1 parent b2cdb43 commit 339276c
Show file tree
Hide file tree
Showing 25 changed files with 673 additions and 326 deletions.
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
32 changes: 21 additions & 11 deletions app/signer/private.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package signer

import (
"path/filepath"
"fmt"

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()
pvKeyFile := filepath.Join(nodeDir, nodeCfg.PrivValidatorKeyFile())
err := cmtos.EnsureDir(filepath.Dir(pvKeyFile), 0777)
if err != nil {
return nil, err
nodeCfg.SetRoot(nodeDir)

pvKeyFile := nodeCfg.PrivValidatorKeyFile()
pvStateFile := nodeCfg.PrivValidatorStateFile()
blsKeyFile := privval.DefaultBlsKeyFile(nodeDir)
blsPasswordFile := privval.DefaultBlsPasswordFile(nodeDir)

if err := privval.EnsureDirs(pvKeyFile, pvStateFile, blsKeyFile, blsPasswordFile); err != nil {
return nil, fmt.Errorf("failed to ensure dirs: %w", err)
}
pvStateFile := filepath.Join(nodeDir, nodeCfg.PrivValidatorStateFile())
err = cmtos.EnsureDir(filepath.Dir(pvStateFile), 0777)
if err != nil {
return nil, err

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,
}
wrappedPV := privval.LoadOrGenWrappedFilePV(pvKeyFile, pvStateFile)

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
49 changes: 18 additions & 31 deletions cmd/babylond/cmd/create_bls_key.go
Original file line number Diff line number Diff line change
@@ -1,23 +1,22 @@
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"
)

const (
FlagPassword = "bls-password"
)

func CreateBlsKeyCmd() *cobra.Command {
bech32PrefixAccAddr := appparams.Bech32PrefixAccAddr

Expand Down Expand Up @@ -47,38 +46,26 @@ $ babylond create-bls-key %s1f5tnl46mk4dfp4nx3n2vnrvyw2h2ydz6ykhk3r --home ./
return err
}

return CreateBlsKey(homeDir, addr)
var password string
password, _ = cmd.Flags().GetString(FlagPassword)
if password == "" {
password = privval.NewBlsPassword()
}
return CreateBlsKey(homeDir, password, addr)
},
}

cmd.Flags().String(flags.FlagHome, app.DefaultNodeHome, "The node home directory")

cmd.Flags().String(FlagPassword, "", "The password for the BLS key. If a flag is set, the non-empty password should be provided. If a flag is not set, the password will be read from the prompt.")
return cmd
}

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
}

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

func CreateBlsKey(home, password string, addr sdk.AccAddress) error {
privval.GenBlsPV(
privval.DefaultBlsKeyFile(home),
privval.DefaultBlsPasswordFile(home),
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
}
28 changes: 24 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,24 @@ 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)

keyPath := nodeCfg.PrivValidatorKeyFile()
statePath := nodeCfg.PrivValidatorStateFile()
filePV := privval.GenWrappedFilePV(keyPath, statePath)
filePV.SetAccAddress(v.Address)
blsKeyFile := privval.DefaultBlsKeyFile(homeDir)
blsPasswordFile := privval.DefaultBlsPasswordFile(homeDir)

err := privval.EnsureDirs(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 +188,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))
}
}
35 changes: 28 additions & 7 deletions cmd/babylond/cmd/genhelpers/bls_create.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package genhelpers

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

Expand All @@ -12,6 +12,7 @@ import (

"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,15 +36,35 @@ $ babylond genbls --home ./
homeDir, _ := cmd.Flags().GetString(flags.FlagHome)

nodeCfg := cmtconfig.DefaultConfig()
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")
nodeCfg.SetRoot(homeDir)
cmtPvKeyFile := nodeCfg.PrivValidatorKeyFile()
cmtPvStateFile := nodeCfg.PrivValidatorStateFile()
blsKeyFile := privval.DefaultBlsKeyFile(homeDir)
blsPasswordFile := privval.DefaultBlsPasswordFile(homeDir)

if err := func(paths ...string) error {
for _, path := range paths {
if !cmtos.FileExists(path) {
return fmt.Errorf("file does not exist in %s", path)
}
}
return nil
}(cmtPvKeyFile, cmtPvStateFile, blsKeyFile, blsPasswordFile); err != nil {
return err
}

wrappedPV := privval.LoadWrappedFilePV(keyPath, statePath)
cmtPV := cmtprivval.LoadFilePV(cmtPvKeyFile, cmtPvStateFile)
blsPV := privval.LoadBlsPV(blsKeyFile, blsPasswordFile)

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

outputFileName, err := wrappedPV.ExportGenBls(filepath.Dir(keyPath))
outputFileName, err := wrappedPV.ExportGenBls(filepath.Dir(cmtPvKeyFile))
if err != nil {
return err
}
Expand Down
25 changes: 19 additions & 6 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()
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)
nodeCfg.SetRoot(home)

keyPath := nodeCfg.PrivValidatorKeyFile()
statePath := nodeCfg.PrivValidatorStateFile()
blsKeyFile := privval.DefaultBlsKeyFile(home)
blsPasswordFile := privval.DefaultBlsPasswordFile(home)

err = privval.EnsureDirs(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))
}
62 changes: 62 additions & 0 deletions crypto/erc2335/erc2335.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package erc2335

import (
"encoding/json"
"fmt"
"os"

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

// Erc2335KeyStore represents an ERC-2335 compatible keystore used in keystorev4.
type Erc2335KeyStore struct {
Crypto map[string]interface{} `json:"crypto"` // Map containing the encryption details for the keystore such as checksum, cipher, and kdf.
Version uint `json:"version"` // Version of the keystore format (e.g., 4 for keystorev4).
UUID string `json:"uuid"` // Unique identifier for the keystore.
Path string `json:"path"` // File path where the keystore is stored.
Pubkey string `json:"pubkey"` // Public key associated with the keystore, stored as a hexadecimal string.
Description string `json:"description"` // Optional description of the keystore, currently used to store the delegator address.
}

// Encrypt encrypts the private key using the keystorev4 encryptor.
func Encrypt(privKey, pubKey []byte, password string) ([]byte, error) {
if privKey == nil {
return nil, fmt.Errorf("private key cannot be nil")
}

encryptor := keystorev4.New()
cryptoFields, err := encryptor.Encrypt(privKey, password)
if err != nil {
return nil, fmt.Errorf("failed to encrypt private key: %w", err)
}

keystoreJSON := Erc2335KeyStore{
Crypto: cryptoFields,
Version: 4,
Pubkey: fmt.Sprintf("%x", pubKey),
}

return json.Marshal(keystoreJSON)
}

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

// LoadKeyStore loads a keystore from a file.
func LoadKeyStore(filePath string) (Erc2335KeyStore, error) {
var keystore Erc2335KeyStore

keyJSONBytes, err := os.ReadFile(filePath)
if err != nil {
return Erc2335KeyStore{}, fmt.Errorf("failed to read keystore file: %w", err)
}

if err := json.Unmarshal(keyJSONBytes, &keystore); err != nil {
return Erc2335KeyStore{}, fmt.Errorf("failed to unmarshal keystore: %w", err)
}

return keystore, nil
}
Loading

0 comments on commit 339276c

Please sign in to comment.