Skip to content
This repository has been archived by the owner on Apr 15, 2024. It is now read-only.

feat: adds support for BIP39 mnemonics #577

Merged
merged 12 commits into from
Nov 7, 2023
143 changes: 142 additions & 1 deletion cmd/blobstream/keys/evm/evm.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
package evm

import (
"bufio"
"bytes"
"crypto/ecdsa"
"errors"
"fmt"
"io"
"os"

"github.com/cosmos/cosmos-sdk/client/input"
"github.com/cosmos/cosmos-sdk/crypto/hd"
"github.com/cosmos/go-bip39"

"github.com/ethereum/go-ethereum/accounts/keystore"

common2 "github.com/celestiaorg/orchestrator-relayer/cmd/blobstream/keys/common"
Expand All @@ -18,6 +25,8 @@ import (
"golang.org/x/term"
)

const mnemonicEntropySize = 256

func Root(serviceName string) *cobra.Command {
evmCmd := &cobra.Command{
Use: "evm",
Expand Down Expand Up @@ -83,11 +92,37 @@ func Add(serviceName string) *cobra.Command {
}
}

account, err := s.EVMKeyStore.NewAccount(passphrase)
fmt.Printf("\nThe provided password is **not** BIP39 passphrase but the store encryption.\n" +
"The account can be retrieved using the mnemonic only, without using this password.\n\n")

// read entropy seed straight from tmcrypto.Rand and convert to mnemonic
entropySeed, err := bip39.NewEntropy(mnemonicEntropySize)
if err != nil {
return err
}

mnemonic, err := bip39.NewMnemonic(entropySeed)
if err != nil {
return err
}

// get the private key using an empty passphrase so that only the mnemonic
// is enough to recover the account
ethPrivKey, err := MnemonicToPrivateKey(mnemonic, "")
if err != nil {
return err
}

account, err := s.EVMKeyStore.ImportECDSA(ethPrivKey, passphrase)
if err != nil {
return err
}

logger.Info("account created successfully", "address", account.Address.String())

fmt.Println("\n\n**Important** write this mnemonic phrase in a safe place." +
"\nIt is the only way to recover your account if you ever forget your password.")
fmt.Printf("\n%s\n\n", mnemonic)
return nil
},
}
Expand Down Expand Up @@ -221,6 +256,7 @@ func Import(serviceName string) *cobra.Command {
importCmd.AddCommand(
ImportFile(serviceName),
ImportECDSA(serviceName),
ImportMnemonic(serviceName),
)

importCmd.SetHelpCommand(&cobra.Command{})
Expand Down Expand Up @@ -379,6 +415,85 @@ func ImportECDSA(serviceName string) *cobra.Command {
return keysConfigFlags(&cmd, serviceName)
}

func ImportMnemonic(serviceName string) *cobra.Command {
cmd := cobra.Command{
Use: "mnemonic",
Args: cobra.ExactArgs(0),
Short: "import an EVM address from a 24 words BIP39 mnemonic phrase",
rach-id marked this conversation as resolved.
Show resolved Hide resolved
RunE: func(cmd *cobra.Command, args []string) error {
config, err := parseKeysConfigFlags(cmd, serviceName)
if err != nil {
return err
}

logger := tmlog.NewTMLogger(os.Stdout)

initOptions := store.InitOptions{NeedEVMKeyStore: true}
isInit := store.IsInit(logger, config.Home, initOptions)

// initialize the store if not initialized
if !isInit {
err := store.Init(logger, config.Home, initOptions)
if err != nil {
return err
}
}

// open store
openOptions := store.OpenOptions{HasEVMKeyStore: true}
s, err := store.OpenStore(logger, config.Home, openOptions)
if err != nil {
return err
}
defer func(s *store.Store, log tmlog.Logger) {
err := s.Close(log, openOptions)
if err != nil {
logger.Error(err.Error())
}
}(s, logger)

// get the mnemonic from user input
inBuf := bufio.NewReader(os.Stdin)
mnemonic, err := input.GetString("Enter your bip39 mnemonic", inBuf)
if err != nil {
return err
}
if !bip39.IsMnemonicValid(mnemonic) {
return errors.New("invalid mnemonic")
}

fmt.Printf("\n\nThe provided password is **not** BIP39 passphrase but the store encryption.\n" +
"The account can be retrieved using the mnemonic only, without using this password.\n\n")

// get the passphrase to use for the seed
passphrase := config.EVMPassphrase
// if the passphrase is not specified as a flag, ask for it.
if passphrase == "" {
passphrase, err = GetNewPassphrase()
if err != nil {
return err
}
}

logger.Info("importing account")

ethPrivKey, err := MnemonicToPrivateKey(mnemonic, passphrase)
if err != nil {
return err
}

account, err := s.EVMKeyStore.ImportECDSA(ethPrivKey, passphrase)
if err != nil {
return err
}

logger.Info("successfully imported key", "address", account.Address.String())
return nil
},
}
return keysConfigFlags(&cmd, serviceName)
}

func Update(serviceName string) *cobra.Command {
cmd := cobra.Command{
Use: "update <account address in hex>",
Expand Down Expand Up @@ -520,9 +635,35 @@ func GetNewPassphrase() (string, error) {
return "", err
}
if bytes.Equal(bzPassphrase, bzPassphraseConfirm) {
fmt.Println()
break
}
fmt.Print("\npassphrase and confirmation mismatch.\n")
}
return string(bzPassphrase), nil
}

// MnemonicToPrivateKey derives a private key from the provided mnemonic.
// It uses the Ledger derivation path, geth.LegacyLedgerBaseDerivationPath, i.e. m/44'/60'/0'/0, to generate
// the first private key.
func MnemonicToPrivateKey(mnemonic string, passphrase string) (*ecdsa.PrivateKey, error) {
// create the master key
seed, err := bip39.NewSeedWithErrorChecking(mnemonic, passphrase)
if err != nil {
return nil, err
}

secret, chainCode := hd.ComputeMastersFromSeed(seed)

// derive the first private key from the master key
key, err := hd.DerivePrivateKeyForPath(secret, chainCode, accounts.LegacyLedgerBaseDerivationPath.String())
if err != nil {
return nil, err
}

ethPrivKey, err := ethcrypto.ToECDSA(key)
if err != nil {
return nil, err
}
return ethPrivKey, nil
}
50 changes: 50 additions & 0 deletions cmd/blobstream/keys/evm/evm_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package evm_test

import (
"testing"

"github.com/celestiaorg/orchestrator-relayer/cmd/blobstream/keys/evm"
"github.com/ethereum/go-ethereum/crypto"
"github.com/stretchr/testify/assert"
)

func TestMnemonicToPrivateKey(t *testing.T) {
tests := []struct {
name string
mnemonic string
expectedError bool
expectedResult string
expectedAddress string
}{
{
name: "Valid Mnemonic and Passphrase",
mnemonic: "rescue any open drink foster thing scale country embark stable segment stem portion ostrich spoon hat debate diesel morning galaxy weird firm capital census",
expectedError: false,
expectedResult: "cb4851012ea2e0421fee67c496b1ae43f0f863903f4e2b57459d3f49f365e926",
expectedAddress: "0x082d835d29b0519e55401084Ef60fC3D720b62b6",
},
{
name: "Invalid Mnemonic",
mnemonic: "wrong mnemonic beginning poverty injury cradle wrong smoke sphere trap tumble girl monkey sibling festival mask second agent slice gadget census glare swear recycle",
expectedError: true,
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
privateKey, err := evm.MnemonicToPrivateKey(test.mnemonic, "1234")

if test.expectedError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
addr := crypto.PubkeyToAddress(privateKey.PublicKey)
assert.Equal(t, test.expectedAddress, addr.Hex())

expectedPrivateKey, err := crypto.HexToECDSA(test.expectedResult)
assert.NoError(t, err)
assert.Equal(t, expectedPrivateKey.D.Bytes(), privateKey.D.Bytes())
}
})
}
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ require (
require (
github.com/celestiaorg/blobstream-contracts/v3 v3.1.0
github.com/cosmos/cosmos-sdk v0.46.14
github.com/cosmos/go-bip39 v1.0.0
github.com/dgraph-io/badger/v2 v2.2007.4
github.com/ipfs/boxo v0.14.0
github.com/ipfs/go-datastore v0.6.0
Expand Down Expand Up @@ -84,7 +85,6 @@ require (
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
github.com/cosmos/btcutil v1.0.5 // indirect
github.com/cosmos/cosmos-proto v1.0.0-alpha8 // indirect
github.com/cosmos/go-bip39 v1.0.0 // indirect
github.com/cosmos/gogoproto v1.4.11 // indirect
github.com/cosmos/gorocksdb v1.2.0 // indirect
github.com/cosmos/iavl v0.19.6 // indirect
Expand Down
Loading