Skip to content

Commit

Permalink
Add in base gnokey derive command
Browse files Browse the repository at this point in the history
  • Loading branch information
zivkovicmilos committed Oct 9, 2023
1 parent fa8eb77 commit e867c0a
Show file tree
Hide file tree
Showing 3 changed files with 233 additions and 0 deletions.
134 changes: 134 additions & 0 deletions tm2/pkg/crypto/keys/client/derive.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package client

import (
"context"
"errors"
"flag"
"math"

"github.com/gnolang/gno/tm2/pkg/commands"
"github.com/gnolang/gno/tm2/pkg/crypto"
"github.com/gnolang/gno/tm2/pkg/crypto/bip39"
"github.com/gnolang/gno/tm2/pkg/crypto/hd"
"github.com/gnolang/gno/tm2/pkg/crypto/secp256k1"
)

var (
errInvalidMnemonic = errors.New("invalid bip39 mnemonic")
errInvalidNumAccounts = errors.New("invalid number of accounts")
errInvalidAccountIndex = errors.New("invalid account index")
)

type deriveCfg struct {
mnemonic string
numAccounts uint64
accountIndex uint64
}

// newDeriveCmd creates a new gnokey derive subcommand
func newDeriveCmd(io *commands.IO) *commands.Command {
cfg := &deriveCfg{}

return commands.NewCommand(
commands.Metadata{
Name: "derive",
ShortUsage: "derive [flags]",
ShortHelp: "Derives the account addresses from the specified mnemonic",
},
cfg,
func(_ context.Context, _ []string) error {
return execDerive(cfg, io)
},
)
}

func (c *deriveCfg) RegisterFlags(fs *flag.FlagSet) {
fs.StringVar(
&c.mnemonic,
"mnemonic",
"",
"the bip39 mnemonic",
)

fs.Uint64Var(
&c.numAccounts,
"num-accounts",
10,
"the number of accounts to derive from the mnemonic",
)

fs.Uint64Var(
&c.accountIndex,
"account-index",
0,
"the account index in the mnemonic",
)
}

func execDerive(cfg *deriveCfg, io *commands.IO) error {
// Make sure the number of accounts is valid
if cfg.numAccounts == 0 || !isUint32(cfg.numAccounts) {
return errInvalidNumAccounts
}

// Make sure the account index is valid
if !isUint32(cfg.accountIndex) {
return errInvalidAccountIndex
}

// Make sure the mnemonic is valid
if !bip39.IsMnemonicValid(cfg.mnemonic) {
return errInvalidMnemonic
}

// Generate the accounts
accounts := generateAccounts(
cfg.mnemonic,
cfg.accountIndex,
cfg.numAccounts,
)

io.Printf("[Generated Accounts]\n\n")
io.Printf("Account Index: %d\n\n", cfg.accountIndex)

// Print them out
for index, account := range accounts {
io.Printfln("%d. %s", index, account.String())
}

return nil
}

// isUint32 verifies a uint64 value can be represented
// as a uint32
func isUint32(value uint64) bool {
return value <= math.MaxUint32
}

// generateAccounts the accounts using the provided mnemonics
func generateAccounts(mnemonic string, accountIndex, numAccounts uint64) []crypto.Address {
addresses := make([]crypto.Address, numAccounts)

// Generate the seed
seed := bip39.NewSeed(mnemonic, "")

for i := uint64(0); i < numAccounts; i++ {
key := generateKeyFromSeed(seed, uint32(accountIndex), uint32(i))
address := key.PubKey().Address()

addresses[i] = address
}

return addresses
}

// generateKeyFromSeed generates a private key from
// the provided seed and index
func generateKeyFromSeed(seed []byte, account, index uint32) crypto.PrivKey {
pathParams := hd.NewFundraiserParams(account, crypto.CoinType, index)

masterPriv, ch := hd.ComputeMastersFromSeed(seed)
derivedPriv, _ := hd.DerivePrivateKeyForPath(masterPriv, ch, pathParams.String())

return secp256k1.PrivKeySecp256k1(derivedPriv)
}
98 changes: 98 additions & 0 deletions tm2/pkg/crypto/keys/client/derive_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package client

import (
"bytes"
"math"
"testing"

"github.com/gnolang/gno/tm2/pkg/commands"
"github.com/gnolang/gno/tm2/pkg/crypto/bip39"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func Test_execDerive(t *testing.T) {
t.Parallel()

t.Run("invalid number of accounts, no accounts requested", func(t *testing.T) {
t.Parallel()

cfg := &deriveCfg{
numAccounts: 0,
}

assert.ErrorIs(t, execDerive(cfg, nil), errInvalidNumAccounts)
})

t.Run("invalid number of accounts, > uint32", func(t *testing.T) {
t.Parallel()

cfg := &deriveCfg{
numAccounts: math.MaxUint32 + 1, // > uint32
}

assert.ErrorIs(t, execDerive(cfg, nil), errInvalidNumAccounts)
})

t.Run("invalid account index", func(t *testing.T) {
t.Parallel()

cfg := &deriveCfg{
numAccounts: 1,
accountIndex: math.MaxUint32 + 1, // > uint32
}

assert.ErrorIs(t, execDerive(cfg, nil), errInvalidAccountIndex)
})

t.Run("invalid mnemonic", func(t *testing.T) {
t.Parallel()

cfg := &deriveCfg{
numAccounts: 1,
accountIndex: 0,
mnemonic: "one two",
}

assert.ErrorIs(t, execDerive(cfg, nil), errInvalidMnemonic)
})

t.Run("valid accounts generated", func(t *testing.T) {
t.Parallel()

// Generate a dummy mnemonic
entropy, entropyErr := bip39.NewEntropy(mnemonicEntropySize)
require.NoError(t, entropyErr)

mnemonic, mnemonicErr := bip39.NewMnemonic(entropy)
require.NoError(t, mnemonicErr)

cfg := &deriveCfg{
numAccounts: 1,
accountIndex: 0,
mnemonic: mnemonic,
}

// Create a test IO so we can capture output
mockOut := bytes.NewBufferString("")

testIO := commands.NewTestIO()
testIO.SetOut(commands.WriteNopCloser(mockOut))

require.NoError(t, execDerive(cfg, testIO))

// Grab the output
deriveOutput := mockOut.String()

// Verify the addresses are derived correctly
expectedAccounts := generateAccounts(
mnemonic,
cfg.accountIndex,
cfg.numAccounts,
)

for _, expectedAccount := range expectedAccounts {
assert.Contains(t, deriveOutput, expectedAccount.String())
}
})
}
1 change: 1 addition & 0 deletions tm2/pkg/crypto/keys/client/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ func NewRootCmd(io *commands.IO) *commands.Command {
newQueryCmd(cfg, io),
newBroadcastCmd(cfg, io),
newMakeTxCmd(cfg, io),
newDeriveCmd(io),
)

return cmd
Expand Down

0 comments on commit e867c0a

Please sign in to comment.