diff --git a/.github/workflows/tm2.yml b/.github/workflows/tm2.yml index ced6054f9e9..5c7c24e98e1 100644 --- a/.github/workflows/tm2.yml +++ b/.github/workflows/tm2.yml @@ -79,7 +79,7 @@ jobs: working-directory: tm2 run: | export GOPATH=$HOME/go - export GOTEST_FLAGS="-v -p 1 -timeout=20m -coverprofile=coverage.out -covermode=atomic" + export GOTEST_FLAGS="-v -p 1 -timeout=20m -coverprofile=coverage.out -covermode=atomic -tags='ledger_suite'" make ${{ matrix.args }} touch coverage.out - uses: actions/upload-artifact@v4 diff --git a/tm2/Makefile b/tm2/Makefile index 3103ef220b2..f841b989b77 100644 --- a/tm2/Makefile +++ b/tm2/Makefile @@ -20,7 +20,7 @@ GOFMT_FLAGS ?= -w # flags for `make imports`. GOIMPORTS_FLAGS ?= $(GOFMT_FLAGS) # test suite flags. -GOTEST_FLAGS ?= -v -p 1 -timeout=30m +GOTEST_FLAGS ?= -v -p 1 -timeout=30m -tags='ledger_suite' ######################################## # Dev tools diff --git a/tm2/pkg/crypto/keys/client/add.go b/tm2/pkg/crypto/keys/client/add.go index 561d2aa5611..3c0c6aaf343 100644 --- a/tm2/pkg/crypto/keys/client/add.go +++ b/tm2/pkg/crypto/keys/client/add.go @@ -5,28 +5,32 @@ import ( "errors" "flag" "fmt" - "sort" + "regexp" "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/keys" - "github.com/gnolang/gno/tm2/pkg/crypto/multisig" + "github.com/gnolang/gno/tm2/pkg/crypto/secp256k1" ) +var ( + errInvalidMnemonic = errors.New("invalid bip39 mnemonic") + errInvalidDerivationPath = errors.New("invalid derivation path") +) + +var reDerivationPath = regexp.MustCompile(`^44'\/118'\/\d+'\/0\/\d+$`) + type AddCfg struct { RootCfg *BaseCfg - Multisig commands.StringArr - MultisigThreshold int - NoSort bool - PublicKey string - UseLedger bool - Recover bool - NoBackup bool - DryRun bool - Account uint64 - Index uint64 + Recover bool + NoBackup bool + Account uint64 + Index uint64 + + DerivationPath commands.StringArr } func NewAddCmd(rootCfg *BaseCfg, io commands.IO) *commands.Command { @@ -34,7 +38,7 @@ func NewAddCmd(rootCfg *BaseCfg, io commands.IO) *commands.Command { RootCfg: rootCfg, } - return commands.NewCommand( + cmd := commands.NewCommand( commands.Metadata{ Name: "add", ShortUsage: "add [flags] ", @@ -45,43 +49,17 @@ func NewAddCmd(rootCfg *BaseCfg, io commands.IO) *commands.Command { return execAdd(cfg, args, io) }, ) -} - -func (c *AddCfg) RegisterFlags(fs *flag.FlagSet) { - fs.Var( - &c.Multisig, - "multisig", - "construct and store a multisig public key (implies --pubkey)", - ) - - fs.IntVar( - &c.MultisigThreshold, - "threshold", - 1, - "K out of N required signatures. For use in conjunction with --multisig", - ) - fs.BoolVar( - &c.NoSort, - "nosort", - false, - "keys passed to --multisig are taken in the order they're supplied", - ) - - fs.StringVar( - &c.PublicKey, - "pubkey", - "", - "parse a public key in bech32 format and save it to disk", + cmd.AddSubCommands( + NewAddMultisigCmd(cfg, io), + NewAddLedgerCmd(cfg, io), + NewAddBech32Cmd(cfg, io), ) - fs.BoolVar( - &c.UseLedger, - "ledger", - false, - "store a local reference to a private key on a Ledger device", - ) + return cmd +} +func (c *AddCfg) RegisterFlags(fs *flag.FlagSet) { fs.BoolVar( &c.Recover, "recover", @@ -96,13 +74,6 @@ func (c *AddCfg) RegisterFlags(fs *flag.FlagSet) { "don't print out seed phrase (if others are watching the terminal)", ) - fs.BoolVar( - &c.DryRun, - "dryrun", - false, - "perform action, but don't add key to local keystore", - ) - fs.Uint64Var( &c.Account, "account", @@ -116,170 +87,124 @@ func (c *AddCfg) RegisterFlags(fs *flag.FlagSet) { 0, "address index number for HD derivation", ) -} - -// DryRunKeyPass contains the default key password for genesis transactions -const DryRunKeyPass = "12345678" - -/* -input - - bip39 mnemonic - - bip39 passphrase - - bip44 path - - local encryption password -output - - armor encrypted private key (saved to file) -*/ -func execAdd(cfg *AddCfg, args []string, io commands.IO) error { - var ( - kb keys.Keybase - err error - encryptPassword string + fs.Var( + &c.DerivationPath, + "derivation-path", + "derivation path for deriving the address", ) +} +func execAdd(cfg *AddCfg, args []string, io commands.IO) error { + // Check if the key name is provided if len(args) != 1 { return flag.ErrHelp } - name := args[0] - showMnemonic := !cfg.NoBackup - - if cfg.DryRun { - // we throw this away, so don't enforce args, - // we want to get a new random seed phrase quickly - kb = keys.NewInMemory() - encryptPassword = DryRunKeyPass - } else { - kb, err = keys.NewKeyBaseFromDir(cfg.RootCfg.Home) - if err != nil { - return err + // Validate the derivation paths are correct + for _, path := range cfg.DerivationPath { + // Make sure the path is valid + if _, err := hd.NewParamsFromPath(path); err != nil { + return fmt.Errorf( + "%w, %w", + errInvalidDerivationPath, + err, + ) } - if has, err := kb.HasByName(name); err == nil && has { - // account exists, ask for user confirmation - response, err2 := io.GetConfirmation(fmt.Sprintf("Override the existing name %s", name)) - if err2 != nil { - return err2 - } - if !response { - return errors.New("aborted") - } + // Make sure the path conforms to the Gno derivation path + if !reDerivationPath.MatchString(path) { + return errInvalidDerivationPath } + } - multisigKeys := cfg.Multisig - if len(multisigKeys) != 0 { - var pks []crypto.PubKey - - multisigThreshold := cfg.MultisigThreshold - if err := keys.ValidateMultisigThreshold(multisigThreshold, len(multisigKeys)); err != nil { - return err - } - - for _, keyname := range multisigKeys { - k, err := kb.GetByName(keyname) - if err != nil { - return err - } - pks = append(pks, k.GetPubKey()) - } - - // Handle --nosort - if !cfg.NoSort { - sort.Slice(pks, func(i, j int) bool { - return pks[i].Address().Compare(pks[j].Address()) < 0 - }) - } - - pk := multisig.NewPubKeyMultisigThreshold(multisigThreshold, pks) - if _, err := kb.CreateMulti(name, pk); err != nil { - return err - } - - io.Printfln("Key %q saved to disk.\n", name) - return nil - } + name := args[0] - // ask for a password when generating a local key - if cfg.PublicKey == "" && !cfg.UseLedger { - encryptPassword, err = io.GetCheckPassword( - [2]string{ - "Enter a passphrase to encrypt your key to disk:", - "Repeat the passphrase:", - }, - cfg.RootCfg.InsecurePasswordStdin, - ) - if err != nil { - return err - } - } + // Read the keybase from the home directory + kb, err := keys.NewKeyBaseFromDir(cfg.RootCfg.Home) + if err != nil { + return fmt.Errorf("unable to read keybase, %w", err) } - if cfg.PublicKey != "" { - pk, err := crypto.PubKeyFromBech32(cfg.PublicKey) - if err != nil { - return err - } - _, err = kb.CreateOffline(name, pk) - if err != nil { - return err - } - return nil + // Check if the key exists + exists, err := kb.HasByName(name) + if err != nil { + return fmt.Errorf("unable to fetch key, %w", err) } - account := cfg.Account - index := cfg.Index - - // If we're using ledger, only thing we need is the path and the bech32 prefix. - if cfg.UseLedger { - bech32PrefixAddr := crypto.Bech32AddrPrefix - info, err := kb.CreateLedger(name, keys.Secp256k1, bech32PrefixAddr, uint32(account), uint32(index)) + // Get overwrite confirmation, if any + if exists { + overwrite, err := io.GetConfirmation(fmt.Sprintf("Override the existing name %s", name)) if err != nil { - return err + return fmt.Errorf("unable to get confirmation, %w", err) } - return printCreate(info, false, "", io) + if !overwrite { + return errOverwriteAborted + } + } + + // Ask for a password when generating a local key + encryptPassword, err := io.GetCheckPassword( + [2]string{ + "Enter a passphrase to encrypt your key to disk:", + "Repeat the passphrase:", + }, + cfg.RootCfg.InsecurePasswordStdin, + ) + if err != nil { + return fmt.Errorf("unable to parse provided password, %w", err) } // Get bip39 mnemonic - var mnemonic string - const bip39Passphrase string = "" // XXX research. + mnemonic, err := GenerateMnemonic(mnemonicEntropySize) + if err != nil { + return fmt.Errorf("unable to generate mnemonic, %w", err) + } if cfg.Recover { bip39Message := "Enter your bip39 mnemonic" mnemonic, err = io.GetString(bip39Message) if err != nil { - return err + return fmt.Errorf("unable to parse mnemonic, %w", err) } + // Make sure it's valid if !bip39.IsMnemonicValid(mnemonic) { - return errors.New("invalid mnemonic") - } - } - - if len(mnemonic) == 0 { - mnemonic, err = GenerateMnemonic(mnemonicEntropySize) - if err != nil { - return err + return errInvalidMnemonic } } - info, err := kb.CreateAccount(name, mnemonic, bip39Passphrase, encryptPassword, uint32(account), uint32(index)) + // Save the account + info, err := kb.CreateAccount( + name, + mnemonic, + "", + encryptPassword, + uint32(cfg.Account), + uint32(cfg.Index), + ) if err != nil { - return err + return fmt.Errorf("unable to save account to keybase, %w", err) } + // Print the derived address info + printDerive(mnemonic, cfg.DerivationPath, io) + // Recover key from seed passphrase if cfg.Recover { - // Hide mnemonic from output - showMnemonic = false - mnemonic = "" + printCreate(info, false, "", io) + + return nil } - return printCreate(info, showMnemonic, mnemonic, io) + // Print the key create info + printCreate(info, !cfg.NoBackup, mnemonic, io) + + return nil } -func printCreate(info keys.Info, showMnemonic bool, mnemonic string, io commands.IO) error { +func printCreate(info keys.Info, showMnemonic bool, mnemonic string, io commands.IO) { io.Println("") printNewInfo(info, io) @@ -291,8 +216,6 @@ It is the only way to recover your account if you ever forget your password. %v `, mnemonic) } - - return nil } func printNewInfo(info keys.Info, io commands.IO) { @@ -305,3 +228,59 @@ func printNewInfo(info keys.Info, io commands.IO) { io.Printfln("* %s (%s) - addr: %v pub: %v, path: %v", keyname, keytype, keyaddr, keypub, keypath) } + +// printDerive prints the derived accounts, if any +func printDerive( + mnemonic string, + paths []string, + io commands.IO, +) { + if len(paths) == 0 { + // No accounts to print + return + } + + // Generate the accounts + accounts := generateAccounts( + mnemonic, + paths, + ) + + io.Printf("[Derived Accounts]\n\n") + + // Print them out + for index, path := range paths { + io.Printfln( + "%d. %s: %s", + index, + path, + accounts[index].String(), + ) + } +} + +// generateAccounts the accounts using the provided mnemonics +func generateAccounts(mnemonic string, paths []string) []crypto.Address { + addresses := make([]crypto.Address, len(paths)) + + // Generate the seed + seed := bip39.NewSeed(mnemonic, "") + + for index, path := range paths { + key := generateKeyFromSeed(seed, path) + address := key.PubKey().Address() + + addresses[index] = address + } + + return addresses +} + +// generateKeyFromSeed generates a private key from +// the provided seed and path +func generateKeyFromSeed(seed []byte, path string) crypto.PrivKey { + masterPriv, ch := hd.ComputeMastersFromSeed(seed) + derivedPriv, _ := hd.DerivePrivateKeyForPath(masterPriv, ch, path) + + return secp256k1.PrivKeySecp256k1(derivedPriv) +} diff --git a/tm2/pkg/crypto/keys/client/add_bech32.go b/tm2/pkg/crypto/keys/client/add_bech32.go new file mode 100644 index 00000000000..7b7cb8aca2c --- /dev/null +++ b/tm2/pkg/crypto/keys/client/add_bech32.go @@ -0,0 +1,94 @@ +package client + +import ( + "context" + "flag" + "fmt" + + "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/gnolang/gno/tm2/pkg/crypto" + "github.com/gnolang/gno/tm2/pkg/crypto/keys" +) + +type AddBech32Cfg struct { + RootCfg *AddCfg + + PublicKey string +} + +// NewAddBech32Cmd creates a gnokey add bech32 command +func NewAddBech32Cmd(rootCfg *AddCfg, io commands.IO) *commands.Command { + cfg := &AddBech32Cfg{ + RootCfg: rootCfg, + } + + return commands.NewCommand( + commands.Metadata{ + Name: "bech32", + ShortUsage: "add bech32 [flags] ", + ShortHelp: "adds a public key to the keybase, using the bech32 representation", + }, + cfg, + func(_ context.Context, args []string) error { + return execAddBech32(cfg, args, io) + }, + ) +} + +func (c *AddBech32Cfg) RegisterFlags(fs *flag.FlagSet) { + fs.StringVar( + &c.PublicKey, + "pubkey", + "", + "parse a public key in bech32 format and save it to disk", + ) +} + +func execAddBech32(cfg *AddBech32Cfg, args []string, io commands.IO) error { + // Validate a key name was provided + if len(args) != 1 { + return flag.ErrHelp + } + + name := args[0] + + // Read the keybase from the home directory + kb, err := keys.NewKeyBaseFromDir(cfg.RootCfg.RootCfg.Home) + if err != nil { + return fmt.Errorf("unable to read keybase, %w", err) + } + + // Check if the key exists + exists, err := kb.HasByName(name) + if err != nil { + return fmt.Errorf("unable to fetch key, %w", err) + } + + // Get overwrite confirmation, if any + if exists { + overwrite, err := io.GetConfirmation(fmt.Sprintf("Override the existing name %s", name)) + if err != nil { + return fmt.Errorf("unable to get confirmation, %w", err) + } + + if !overwrite { + return errOverwriteAborted + } + } + + // Parse the public key + publicKey, err := crypto.PubKeyFromBech32(cfg.PublicKey) + if err != nil { + return fmt.Errorf("unable to parse public key from bech32, %w", err) + } + + // Save it offline in the keybase + _, err = kb.CreateOffline(name, publicKey) + if err != nil { + return fmt.Errorf("unable to save public key, %w", err) + } + + io.Printfln("Key %q saved to disk.\n", name) + + return nil +} diff --git a/tm2/pkg/crypto/keys/client/add_bech32_test.go b/tm2/pkg/crypto/keys/client/add_bech32_test.go new file mode 100644 index 00000000000..f7697c0184d --- /dev/null +++ b/tm2/pkg/crypto/keys/client/add_bech32_test.go @@ -0,0 +1,202 @@ +package client + +import ( + "context" + "strings" + "testing" + "time" + + "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/gnolang/gno/tm2/pkg/crypto/bip39" + "github.com/gnolang/gno/tm2/pkg/crypto/keys" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestAdd_Bech32(t *testing.T) { + t.Parallel() + + t.Run("valid bech32 addition", func(t *testing.T) { + t.Parallel() + + var ( + kbHome = t.TempDir() + baseOptions = BaseOptions{ + InsecurePasswordStdin: true, + Home: kbHome, + } + + seed = bip39.NewSeed(generateTestMnemonic(t), "") + account = generateKeyFromSeed(seed, "44'/118'/0'/0/0") + + keyName = "key-name" + ) + + ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second) + defer cancelFn() + + io := commands.NewTestIO() + io.SetIn(strings.NewReader("test1234\ntest1234\n")) + + // Create the command + cmd := NewRootCmdWithBaseConfig(io, baseOptions) + + args := []string{ + "add", + "bech32", + "--insecure-password-stdin", + "--home", + kbHome, + "--pubkey", + account.PubKey().String(), + keyName, + } + + require.NoError(t, cmd.ParseAndRun(ctx, args)) + + // Check the keybase + kb, err := keys.NewKeyBaseFromDir(kbHome) + require.NoError(t, err) + + original, err := kb.GetByName(keyName) + require.NoError(t, err) + require.NotNil(t, original) + + assert.Equal(t, account.PubKey().Address().String(), original.GetAddress().String()) + }) + + t.Run("valid bech32 addition, overwrite", func(t *testing.T) { + t.Parallel() + + var ( + kbHome = t.TempDir() + baseOptions = BaseOptions{ + InsecurePasswordStdin: true, + Home: kbHome, + } + + seed = bip39.NewSeed(generateTestMnemonic(t), "") + originalAccount = generateKeyFromSeed(seed, "44'/118'/0'/0/0") + copyAccount = generateKeyFromSeed(seed, "44'/118'/0'/0/1") + + keyName = "key-name" + ) + + ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second) + defer cancelFn() + + io := commands.NewTestIO() + io.SetIn(strings.NewReader("test1234\ntest1234\n")) + + // Create the command + cmd := NewRootCmdWithBaseConfig(io, baseOptions) + + baseArgs := []string{ + "add", + "bech32", + "--insecure-password-stdin", + "--home", + kbHome, + keyName, + } + + initialArgs := append(baseArgs, []string{ + "--pubkey", + originalAccount.PubKey().String(), + }...) + + require.NoError(t, cmd.ParseAndRun(ctx, initialArgs)) + + // Check the keybase + kb, err := keys.NewKeyBaseFromDir(kbHome) + require.NoError(t, err) + + original, err := kb.GetByName(keyName) + require.NoError(t, err) + + require.Equal(t, originalAccount.PubKey().Address().String(), original.GetAddress().String()) + + // Overwrite the key + io.SetIn(strings.NewReader("y\ntest1234\ntest1234\n")) + + secondaryArgs := append(baseArgs, []string{ + "--pubkey", + copyAccount.PubKey().String(), + }...) + + cmd = NewRootCmdWithBaseConfig(io, baseOptions) + require.NoError(t, cmd.ParseAndRun(ctx, secondaryArgs)) + + newKey, err := kb.GetByName(keyName) + require.NoError(t, err) + + require.Equal(t, copyAccount.PubKey().Address().String(), newKey.GetAddress().String()) + }) + + t.Run("no overwrite permission", func(t *testing.T) { + t.Parallel() + + var ( + kbHome = t.TempDir() + baseOptions = BaseOptions{ + InsecurePasswordStdin: true, + Home: kbHome, + } + + seed = bip39.NewSeed(generateTestMnemonic(t), "") + originalAccount = generateKeyFromSeed(seed, "44'/118'/0'/0/0") + copyAccount = generateKeyFromSeed(seed, "44'/118'/0'/0/1") + + keyName = "key-name" + ) + + ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second) + defer cancelFn() + + io := commands.NewTestIO() + io.SetIn(strings.NewReader("test1234\ntest1234\n")) + + // Create the command + cmd := NewRootCmdWithBaseConfig(io, baseOptions) + + baseArgs := []string{ + "add", + "bech32", + "--insecure-password-stdin", + "--home", + kbHome, + keyName, + } + + initialArgs := append(baseArgs, []string{ + "--pubkey", + originalAccount.PubKey().String(), + }...) + + require.NoError(t, cmd.ParseAndRun(ctx, initialArgs)) + + // Check the keybase + kb, err := keys.NewKeyBaseFromDir(kbHome) + require.NoError(t, err) + + original, err := kb.GetByName(keyName) + require.NoError(t, err) + + io.SetIn(strings.NewReader("n\ntest1234\ntest1234\n")) + + // Confirm overwrite + secondaryArgs := append(baseArgs, []string{ + "--pubkey", + copyAccount.PubKey().String(), + }...) + + cmd = NewRootCmdWithBaseConfig(io, baseOptions) + require.ErrorIs(t, cmd.ParseAndRun(ctx, secondaryArgs), errOverwriteAborted) + + newKey, err := kb.GetByName(keyName) + require.NoError(t, err) + + // Make sure the key is not overwritten + assert.Equal(t, original.GetAddress(), newKey.GetAddress()) + }) +} diff --git a/tm2/pkg/crypto/keys/client/add_ledger.go b/tm2/pkg/crypto/keys/client/add_ledger.go new file mode 100644 index 00000000000..97bd4a3bee5 --- /dev/null +++ b/tm2/pkg/crypto/keys/client/add_ledger.go @@ -0,0 +1,76 @@ +package client + +import ( + "context" + "flag" + "fmt" + + "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/gnolang/gno/tm2/pkg/crypto" + "github.com/gnolang/gno/tm2/pkg/crypto/keys" +) + +// NewAddLedgerCmd creates a gnokey add ledger command +func NewAddLedgerCmd(cfg *AddCfg, io commands.IO) *commands.Command { + return commands.NewCommand( + commands.Metadata{ + Name: "ledger", + ShortUsage: "add ledger [flags] ", + ShortHelp: "adds a Ledger key reference to the keybase", + }, + commands.NewEmptyConfig(), + func(_ context.Context, args []string) error { + return execAddLedger(cfg, args, io) + }, + ) +} + +func execAddLedger(cfg *AddCfg, args []string, io commands.IO) error { + // Validate a key name was provided + if len(args) != 1 { + return flag.ErrHelp + } + + name := args[0] + + // Read the keybase from the home directory + kb, err := keys.NewKeyBaseFromDir(cfg.RootCfg.Home) + if err != nil { + return fmt.Errorf("unable to read keybase, %w", err) + } + + // Check if the key exists + exists, err := kb.HasByName(name) + if err != nil { + return fmt.Errorf("unable to fetch key, %w", err) + } + + // Get overwrite confirmation, if any + if exists { + overwrite, err := io.GetConfirmation(fmt.Sprintf("Override the existing name %s", name)) + if err != nil { + return fmt.Errorf("unable to get confirmation, %w", err) + } + + if !overwrite { + return errOverwriteAborted + } + } + + // Create the ledger reference + info, err := kb.CreateLedger( + name, + keys.Secp256k1, + crypto.Bech32AddrPrefix, + uint32(cfg.Account), + uint32(cfg.Index), + ) + if err != nil { + return fmt.Errorf("unable to create Ledger reference in keybase, %w", err) + } + + // Print the information + printCreate(info, false, "", io) + + return nil +} diff --git a/tm2/pkg/crypto/keys/client/add_ledger_skipped_test.go b/tm2/pkg/crypto/keys/client/add_ledger_skipped_test.go new file mode 100644 index 00000000000..8a09d060b16 --- /dev/null +++ b/tm2/pkg/crypto/keys/client/add_ledger_skipped_test.go @@ -0,0 +1,10 @@ +//go:build !ledger_suite +// +build !ledger_suite + +package client + +import "testing" + +func TestAdd_Ledger(t *testing.T) { + t.Skip("Please enable the 'ledger_suite' build tags") +} diff --git a/tm2/pkg/crypto/keys/client/add_ledger_test.go b/tm2/pkg/crypto/keys/client/add_ledger_test.go new file mode 100644 index 00000000000..c1384efcb79 --- /dev/null +++ b/tm2/pkg/crypto/keys/client/add_ledger_test.go @@ -0,0 +1,170 @@ +//go:build ledger_suite +// +build ledger_suite + +package client + +import ( + "context" + "strings" + "testing" + "time" + + "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/gnolang/gno/tm2/pkg/crypto/keys" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// Make sure to run these tests with the following tag enabled: +// -tags='ledger_suite' +func TestAdd_Ledger(t *testing.T) { + t.Parallel() + + t.Run("valid ledger reference added", func(t *testing.T) { + t.Parallel() + + var ( + kbHome = t.TempDir() + baseOptions = BaseOptions{ + InsecurePasswordStdin: true, + Home: kbHome, + } + + keyName = "key-name" + ) + + ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second) + defer cancelFn() + + io := commands.NewTestIO() + io.SetIn(strings.NewReader("test1234\ntest1234\n")) + + // Create the command + cmd := NewRootCmdWithBaseConfig(io, baseOptions) + + args := []string{ + "add", + "ledger", + "--insecure-password-stdin", + "--home", + kbHome, + keyName, + } + + require.NoError(t, cmd.ParseAndRun(ctx, args)) + + // Check the keybase + kb, err := keys.NewKeyBaseFromDir(kbHome) + require.NoError(t, err) + + original, err := kb.GetByName(keyName) + require.NoError(t, err) + require.NotNil(t, original) + }) + + t.Run("valid ledger reference added, overwrite", func(t *testing.T) { + t.Parallel() + + var ( + kbHome = t.TempDir() + baseOptions = BaseOptions{ + InsecurePasswordStdin: true, + Home: kbHome, + } + + keyName = "key-name" + ) + + ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second) + defer cancelFn() + + io := commands.NewTestIO() + io.SetIn(strings.NewReader("test1234\ntest1234\n")) + + // Create the command + cmd := NewRootCmdWithBaseConfig(io, baseOptions) + + args := []string{ + "add", + "ledger", + "--insecure-password-stdin", + "--home", + kbHome, + keyName, + } + + require.NoError(t, cmd.ParseAndRun(ctx, args)) + + // Check the keybase + kb, err := keys.NewKeyBaseFromDir(kbHome) + require.NoError(t, err) + + original, err := kb.GetByName(keyName) + require.NoError(t, err) + require.NotNil(t, original) + + io.SetIn(strings.NewReader("y\ntest1234\ntest1234\n")) + + cmd = NewRootCmdWithBaseConfig(io, baseOptions) + require.NoError(t, cmd.ParseAndRun(ctx, args)) + + newKey, err := kb.GetByName(keyName) + require.NoError(t, err) + + // Make sure the different key is generated and overwritten + assert.NotEqual(t, original.GetAddress(), newKey.GetAddress()) + }) + + t.Run("valid ledger reference added, no overwrite permission", func(t *testing.T) { + t.Parallel() + + var ( + kbHome = t.TempDir() + baseOptions = BaseOptions{ + InsecurePasswordStdin: true, + Home: kbHome, + } + + keyName = "key-name" + ) + + ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second) + defer cancelFn() + + io := commands.NewTestIO() + io.SetIn(strings.NewReader("test1234\ntest1234\n")) + + // Create the command + cmd := NewRootCmdWithBaseConfig(io, baseOptions) + + args := []string{ + "add", + "ledger", + "--insecure-password-stdin", + "--home", + kbHome, + keyName, + } + + require.NoError(t, cmd.ParseAndRun(ctx, args)) + + // Check the keybase + kb, err := keys.NewKeyBaseFromDir(kbHome) + require.NoError(t, err) + + original, err := kb.GetByName(keyName) + require.NoError(t, err) + require.NotNil(t, original) + + io.SetIn(strings.NewReader("n\ntest1234\ntest1234\n")) + + cmd = NewRootCmdWithBaseConfig(io, baseOptions) + require.ErrorIs(t, cmd.ParseAndRun(ctx, args), errOverwriteAborted) + + newKey, err := kb.GetByName(keyName) + require.NoError(t, err) + + // Make sure the key is not overwritten + assert.Equal(t, original.GetAddress(), newKey.GetAddress()) + }) +} diff --git a/tm2/pkg/crypto/keys/client/add_multisig.go b/tm2/pkg/crypto/keys/client/add_multisig.go new file mode 100644 index 00000000000..39b90571143 --- /dev/null +++ b/tm2/pkg/crypto/keys/client/add_multisig.go @@ -0,0 +1,138 @@ +package client + +import ( + "context" + "errors" + "flag" + "fmt" + "sort" + + "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/gnolang/gno/tm2/pkg/crypto" + "github.com/gnolang/gno/tm2/pkg/crypto/keys" + "github.com/gnolang/gno/tm2/pkg/crypto/multisig" +) + +var ( + errOverwriteAborted = errors.New("overwrite aborted") + errUnableToVerifyMultisig = errors.New("unable to verify multisig threshold") +) + +type AddMultisigCfg struct { + RootCfg *AddCfg + + NoSort bool + Multisig commands.StringArr + MultisigThreshold int +} + +// NewAddMultisigCmd creates a gnokey add multisig command +func NewAddMultisigCmd(rootCfg *AddCfg, io commands.IO) *commands.Command { + cfg := &AddMultisigCfg{ + RootCfg: rootCfg, + } + + return commands.NewCommand( + commands.Metadata{ + Name: "multisig", + ShortUsage: "add multisig [flags] ", + ShortHelp: "adds a multisig key reference to the keybase", + }, + cfg, + func(_ context.Context, args []string) error { + return execAddMultisig(cfg, args, io) + }, + ) +} + +func (c *AddMultisigCfg) RegisterFlags(fs *flag.FlagSet) { + fs.BoolVar( + &c.NoSort, + "nosort", + false, + "keys passed to --multisig are taken in the order they're supplied", + ) + + fs.Var( + &c.Multisig, + "multisig", + "construct and store a multisig public key", + ) + + fs.IntVar( + &c.MultisigThreshold, + "threshold", + 1, + "K out of N required signatures", + ) +} + +func execAddMultisig(cfg *AddMultisigCfg, args []string, io commands.IO) error { + // Validate a key name was provided + if len(args) != 1 { + return flag.ErrHelp + } + + // Validate the multisig threshold + if err := keys.ValidateMultisigThreshold( + cfg.MultisigThreshold, + len(cfg.Multisig), + ); err != nil { + return errUnableToVerifyMultisig + } + + name := args[0] + + // Read the keybase from the home directory + kb, err := keys.NewKeyBaseFromDir(cfg.RootCfg.RootCfg.Home) + if err != nil { + return fmt.Errorf("unable to read keybase, %w", err) + } + + // Check if the key exists + exists, err := kb.HasByName(name) + if err != nil { + return fmt.Errorf("unable to fetch key, %w", err) + } + + // Get overwrite confirmation, if any + if exists { + overwrite, err := io.GetConfirmation(fmt.Sprintf("Override the existing name %s", name)) + if err != nil { + return fmt.Errorf("unable to get confirmation, %w", err) + } + + if !overwrite { + return errOverwriteAborted + } + } + + publicKeys := make([]crypto.PubKey, 0) + for _, keyName := range cfg.Multisig { + k, err := kb.GetByName(keyName) + if err != nil { + return fmt.Errorf("unable to fetch key, %w", err) + } + + publicKeys = append(publicKeys, k.GetPubKey()) + } + + // Check if the keys should be sorted + if !cfg.NoSort { + sort.Slice(publicKeys, func(i, j int) bool { + return publicKeys[i].Address().Compare(publicKeys[j].Address()) < 0 + }) + } + + // Create a new public key with the multisig threshold + if _, err := kb.CreateMulti( + name, + multisig.NewPubKeyMultisigThreshold(cfg.MultisigThreshold, publicKeys), + ); err != nil { + return fmt.Errorf("unable to create multisig key reference, %w", err) + } + + io.Printfln("Key %q saved to disk.\n", name) + + return nil +} diff --git a/tm2/pkg/crypto/keys/client/add_multisig_test.go b/tm2/pkg/crypto/keys/client/add_multisig_test.go new file mode 100644 index 00000000000..4a350d5faa9 --- /dev/null +++ b/tm2/pkg/crypto/keys/client/add_multisig_test.go @@ -0,0 +1,121 @@ +package client + +import ( + "context" + "strings" + "testing" + "time" + + "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/gnolang/gno/tm2/pkg/crypto/keys" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestAdd_Multisig(t *testing.T) { + t.Parallel() + + t.Run("invalid multisig threshold", func(t *testing.T) { + t.Parallel() + + var ( + kbHome = t.TempDir() + baseOptions = BaseOptions{ + InsecurePasswordStdin: true, + Home: kbHome, + } + + keyName = "key-name" + ) + + ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second) + defer cancelFn() + + io := commands.NewTestIO() + io.SetIn(strings.NewReader("test1234\ntest1234\n")) + + // Create the command + cmd := NewRootCmdWithBaseConfig(io, baseOptions) + + args := []string{ + "add", + "multisig", + "--insecure-password-stdin", + "--home", + kbHome, + "--multisig", + "example", + "--threshold", + "2", + keyName, + } + + require.ErrorIs(t, cmd.ParseAndRun(ctx, args), errUnableToVerifyMultisig) + }) + + t.Run("valid multisig reference added", func(t *testing.T) { + t.Parallel() + + var ( + kbHome = t.TempDir() + baseOptions = BaseOptions{ + InsecurePasswordStdin: true, + Home: kbHome, + } + mnemonic = generateTestMnemonic(t) + + keyNames = []string{ + "key-1", + "key-2", + } + ) + + ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second) + defer cancelFn() + + io := commands.NewTestIO() + io.SetIn(strings.NewReader("y\ntest1234\ntest1234\n")) + + // Create the command + cmd := NewRootCmdWithBaseConfig(io, baseOptions) + + args := []string{ + "add", + "multisig", + "--insecure-password-stdin", + "--home", + kbHome, + "--multisig", + keyNames[0], + "--multisig", + keyNames[1], + keyNames[0], + } + + // Prepare the multisig keys + kb, err := keys.NewKeyBaseFromDir(kbHome) + require.NoError(t, err) + + for index, keyName := range keyNames { + _, err = kb.CreateAccount( + keyName, + mnemonic, + "", + "123", + 0, + uint32(index), + ) + + require.NoError(t, err) + } + + require.NoError(t, cmd.ParseAndRun(ctx, args)) + + // Verify the key is multisig + original, err := kb.GetByName(keyNames[0]) + require.NoError(t, err) + require.NotNil(t, original) + + assert.Equal(t, original.GetType(), keys.TypeMulti) + }) +} diff --git a/tm2/pkg/crypto/keys/client/add_test.go b/tm2/pkg/crypto/keys/client/add_test.go index 4110ea32c9a..37638f995bd 100644 --- a/tm2/pkg/crypto/keys/client/add_test.go +++ b/tm2/pkg/crypto/keys/client/add_test.go @@ -1,116 +1,364 @@ package client import ( + "bytes" + "context" "fmt" "strings" "testing" + "time" "github.com/gnolang/gno/tm2/pkg/commands" "github.com/gnolang/gno/tm2/pkg/crypto/keys" - "github.com/gnolang/gno/tm2/pkg/crypto/secp256k1" - "github.com/gnolang/gno/tm2/pkg/testutils" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) -func Test_execAddBasic(t *testing.T) { +func TestAdd_Base_Add(t *testing.T) { t.Parallel() - // make new test dir - kbHome, kbCleanUp := testutils.NewTestCaseDir(t) - assert.NotNil(t, kbHome) - defer kbCleanUp() + t.Run("valid key addition, generated mnemonic", func(t *testing.T) { + t.Parallel() - cfg := &AddCfg{ - RootCfg: &BaseCfg{ - BaseOptions: BaseOptions{ + var ( + kbHome = t.TempDir() + baseOptions = BaseOptions{ InsecurePasswordStdin: true, Home: kbHome, - }, - }, - } + } - keyName := "keyname1" + keyName = "key-name" + ) - io := commands.NewTestIO() - io.SetIn(strings.NewReader("test1234\ntest1234\n")) + ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second) + defer cancelFn() - // Create a new key - if err := execAdd(cfg, []string{keyName}, io); err != nil { - t.Fatalf("unable to execute add cmd, %v", err) - } + io := commands.NewTestIO() + io.SetIn(strings.NewReader("test1234\ntest1234\n")) - io.SetIn(strings.NewReader("y\ntest1234\ntest1234\n")) + // Create the command + cmd := NewRootCmdWithBaseConfig(io, baseOptions) - // Confirm overwrite - if err := execAdd(cfg, []string{keyName}, io); err != nil { - t.Fatalf("unable to execute add cmd, %v", err) - } -} + args := []string{ + "add", + "--insecure-password-stdin", + "--home", + kbHome, + keyName, + } -var ( - test2Mnemonic = "hair stove window more scrap patient endorse left early pear lawn school loud divide vibrant family still bulk lyrics firm plate media critic dove" - test2PubkeyBech32 = "gpub1pgfj7ard9eg82cjtv4u4xetrwqer2dntxyfzxz3pqg5y7u93gpzug38k2p8s8322zpdm96t0ch87ax88sre4vnclz2jcy8uyhst" -) + require.NoError(t, cmd.ParseAndRun(ctx, args)) -func Test_execAddPublicKey(t *testing.T) { - t.Parallel() + // Check the keybase + kb, err := keys.NewKeyBaseFromDir(kbHome) + require.NoError(t, err) - kbHome, kbCleanUp := testutils.NewTestCaseDir(t) - assert.NotNil(t, kbHome) - defer kbCleanUp() - - cfg := &AddCfg{ - RootCfg: &BaseCfg{ - BaseOptions: BaseOptions{ - Home: kbHome, - }, - }, - PublicKey: test2PubkeyBech32, // test2 account - } + original, err := kb.GetByName(keyName) + require.NoError(t, err) + require.NotNil(t, original) + }) + + t.Run("valid key addition, overwrite", func(t *testing.T) { + t.Parallel() + + var ( + kbHome = t.TempDir() + baseOptions = BaseOptions{ + InsecurePasswordStdin: true, + Home: kbHome, + } + + keyName = "key-name" + ) + + ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second) + defer cancelFn() + + io := commands.NewTestIO() + io.SetIn(strings.NewReader("test1234\ntest1234\n")) + + // Create the command + cmd := NewRootCmdWithBaseConfig(io, baseOptions) + + args := []string{ + "add", + "--insecure-password-stdin", + "--home", + kbHome, + keyName, + } + + require.NoError(t, cmd.ParseAndRun(ctx, args)) + + // Check the keybase + kb, err := keys.NewKeyBaseFromDir(kbHome) + require.NoError(t, err) + + original, err := kb.GetByName(keyName) + require.NoError(t, err) + + io.SetIn(strings.NewReader("y\ntest1234\ntest1234\n")) + + cmd = NewRootCmdWithBaseConfig(io, baseOptions) + require.NoError(t, cmd.ParseAndRun(ctx, args)) + + newKey, err := kb.GetByName(keyName) + require.NoError(t, err) + + // Make sure the different key is generated and overwritten + assert.NotEqual(t, original.GetAddress(), newKey.GetAddress()) + }) + + t.Run("valid key addition, provided mnemonic", func(t *testing.T) { + t.Parallel() + + var ( + kbHome = t.TempDir() + mnemonic = generateTestMnemonic(t) + baseOptions = BaseOptions{ + InsecurePasswordStdin: true, + Home: kbHome, + } + + keyName = "key-name" + ) + + ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second) + defer cancelFn() + + io := commands.NewTestIO() + io.SetIn(strings.NewReader("test1234" + "\n" + "test1234" + "\n" + mnemonic + "\n")) + + // Create the command + cmd := NewRootCmdWithBaseConfig(io, baseOptions) + + args := []string{ + "add", + "--insecure-password-stdin", + "--home", + kbHome, + "--recover", + keyName, + } + + require.NoError(t, cmd.ParseAndRun(ctx, args)) + // Check the keybase + kb, err := keys.NewKeyBaseFromDir(kbHome) + require.NoError(t, err) + + key, err := kb.GetByName(keyName) + require.NoError(t, err) + require.NotNil(t, key) + + // Get the account + accounts := generateAccounts(mnemonic, []string{"44'/118'/0'/0/0"}) + + assert.Equal(t, accounts[0].String(), key.GetAddress().String()) + }) + + t.Run("no overwrite permission", func(t *testing.T) { + t.Parallel() + + var ( + kbHome = t.TempDir() + baseOptions = BaseOptions{ + InsecurePasswordStdin: true, + Home: kbHome, + } + + keyName = "key-name" + ) - if err := execAdd(cfg, []string{"test2"}, nil); err != nil { - t.Fatalf("unable to execute add cmd, %v", err) + ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second) + defer cancelFn() + + io := commands.NewTestIO() + io.SetIn(strings.NewReader("test1234\ntest1234\n")) + + // Create the command + cmd := NewRootCmdWithBaseConfig(io, baseOptions) + + args := []string{ + "add", + "--insecure-password-stdin", + "--home", + kbHome, + keyName, + } + + require.NoError(t, cmd.ParseAndRun(ctx, args)) + + // Check the keybase + kb, err := keys.NewKeyBaseFromDir(kbHome) + require.NoError(t, err) + + original, err := kb.GetByName(keyName) + require.NoError(t, err) + + io.SetIn(strings.NewReader("n\ntest1234\ntest1234\n")) + + // Confirm overwrite + cmd = NewRootCmdWithBaseConfig(io, baseOptions) + require.ErrorIs(t, cmd.ParseAndRun(ctx, args), errOverwriteAborted) + + newKey, err := kb.GetByName(keyName) + require.NoError(t, err) + + // Make sure the key is not overwritten + assert.Equal(t, original.GetAddress(), newKey.GetAddress()) + }) +} + +func generateDerivationPaths(count int) []string { + paths := make([]string, count) + + for i := 0; i < count; i++ { + paths[i] = fmt.Sprintf("44'/118'/0'/0/%d", i) } + + return paths } -func Test_execAddRecover(t *testing.T) { +func TestAdd_Derive(t *testing.T) { t.Parallel() - kbHome, kbCleanUp := testutils.NewTestCaseDir(t) - assert.NotNil(t, kbHome) - defer kbCleanUp() + t.Run("valid address derivation", func(t *testing.T) { + t.Parallel() + + var ( + kbHome = t.TempDir() + mnemonic = generateTestMnemonic(t) + paths = generateDerivationPaths(10) - cfg := &AddCfg{ - RootCfg: &BaseCfg{ - BaseOptions: BaseOptions{ + baseOptions = BaseOptions{ InsecurePasswordStdin: true, Home: kbHome, - }, - }, - Recover: true, // init test2 account - } + } - test2Name := "test2" - test2Passphrase := "gn0rocks!" + dummyPass = "dummy-pass" + ) - io := commands.NewTestIO() - io.SetIn(strings.NewReader(test2Passphrase + "\n" + test2Passphrase + "\n" + test2Mnemonic + "\n")) + ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second) + defer cancelFn() - if err := execAdd(cfg, []string{test2Name}, io); err != nil { - t.Fatalf("unable to execute add cmd, %v", err) - } + mockOut := bytes.NewBufferString("") + + io := commands.NewTestIO() + io.SetIn(strings.NewReader(dummyPass + "\n" + dummyPass + "\n" + mnemonic + "\n")) + io.SetOut(commands.WriteNopCloser(mockOut)) + + // Create the command + cmd := NewRootCmdWithBaseConfig(io, baseOptions) + + args := []string{ + "add", + "--insecure-password-stdin", + "--home", + kbHome, + "--recover", + "example-key", + } + + for _, path := range paths { + args = append( + args, []string{ + "--derivation-path", + path, + }..., + ) + } + + require.NoError(t, cmd.ParseAndRun(ctx, args)) + + // Verify the addresses are derived correctly + expectedAccounts := generateAccounts( + mnemonic, + paths, + ) + + // Grab the output + deriveOutput := mockOut.String() + + for _, expectedAccount := range expectedAccounts { + assert.Contains(t, deriveOutput, expectedAccount.String()) + } + }) + + t.Run("malformed derivation path", func(t *testing.T) { + t.Parallel() + + var ( + kbHome = t.TempDir() + mnemonic = generateTestMnemonic(t) + dummyPass = "dummy-pass" + baseOptions = BaseOptions{ + InsecurePasswordStdin: true, + Home: kbHome, + } + ) + + ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second) + defer cancelFn() + + mockOut := bytes.NewBufferString("") + + io := commands.NewTestIO() + io.SetIn(strings.NewReader(dummyPass + "\n" + dummyPass + "\n" + mnemonic + "\n")) + io.SetOut(commands.WriteNopCloser(mockOut)) + + // Create the command + cmd := NewRootCmdWithBaseConfig(io, baseOptions) + + args := []string{ + "add", + "--insecure-password-stdin", + "--home", + kbHome, + "--recover", + "example-key", + "--derivation-path", + "malformed path", + } + + require.ErrorIs(t, cmd.ParseAndRun(ctx, args), errInvalidDerivationPath) + }) + + t.Run("invalid derivation path", func(t *testing.T) { + t.Parallel() + + var ( + kbHome = t.TempDir() + mnemonic = generateTestMnemonic(t) + dummyPass = "dummy-pass" + baseOptions = BaseOptions{ + InsecurePasswordStdin: true, + Home: kbHome, + } + ) + + ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second) + defer cancelFn() - kb, err2 := keys.NewKeyBaseFromDir(kbHome) - assert.NoError(t, err2) + mockOut := bytes.NewBufferString("") - infos, err3 := kb.List() - assert.NoError(t, err3) + io := commands.NewTestIO() + io.SetIn(strings.NewReader(dummyPass + "\n" + dummyPass + "\n" + mnemonic + "\n")) + io.SetOut(commands.WriteNopCloser(mockOut)) - info := infos[0] + // Create the command + cmd := NewRootCmdWithBaseConfig(io, baseOptions) - keypub := info.GetPubKey() - keypub = keypub.(secp256k1.PubKeySecp256k1) + args := []string{ + "add", + "--insecure-password-stdin", + "--home", + kbHome, + "--recover", + "example-key", + "--derivation-path", + "44'/500'/0'/0/0", // invalid coin type + } - s := fmt.Sprintf("%s", keypub) - assert.Equal(t, s, test2PubkeyBech32) + require.ErrorIs(t, cmd.ParseAndRun(ctx, args), errInvalidDerivationPath) + }) } diff --git a/tm2/pkg/crypto/keys/keybase_ledger_skipped_test.go b/tm2/pkg/crypto/keys/keybase_ledger_skipped_test.go new file mode 100644 index 00000000000..d406f10f2ed --- /dev/null +++ b/tm2/pkg/crypto/keys/keybase_ledger_skipped_test.go @@ -0,0 +1,18 @@ +//go:build !ledger_suite +// +build !ledger_suite + +package keys + +import "testing" + +func TestCreateLedgerUnsupportedAlgo(t *testing.T) { + t.Parallel() + + t.Skip("this test needs to be run with the `ledger_suite` tag enabled") +} + +func TestCreateLedger(t *testing.T) { + t.Parallel() + + t.Skip("this test needs to be run with the `ledger_suite` tag enabled") +} diff --git a/tm2/pkg/crypto/keys/keybase_ledger_test.go b/tm2/pkg/crypto/keys/keybase_ledger_test.go new file mode 100644 index 00000000000..0f2fca79f90 --- /dev/null +++ b/tm2/pkg/crypto/keys/keybase_ledger_test.go @@ -0,0 +1,43 @@ +//go:build ledger_suite +// +build ledger_suite + +package keys + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestCreateLedgerUnsupportedAlgo(t *testing.T) { + t.Parallel() + + kb := NewInMemory() + _, err := kb.CreateLedger("some_account", Ed25519, "cosmos", 0, 1) + assert.Error(t, err) + assert.Equal(t, "unsupported signing algo: only secp256k1 is supported", err.Error()) +} + +func TestCreateLedger(t *testing.T) { + t.Parallel() + + kb := NewInMemory() + + // test_cover and test_unit will result in different answers + // test_cover does not compile some dependencies so ledger is disabled + // test_unit may add a ledger mock + // both cases are acceptable + _, err := kb.CreateLedger("some_account", Secp256k1, "cosmos", 3, 1) + require.NoError(t, err) + + // Check that restoring the key gets the same results + restoredKey, err := kb.GetByName("some_account") + assert.NotNil(t, restoredKey) + assert.Equal(t, "some_account", restoredKey.GetName()) + assert.Equal(t, TypeLedger, restoredKey.GetType()) + + path, err := restoredKey.GetPath() + assert.NoError(t, err) + assert.Equal(t, "44'/118'/3'/0/1", path.String()) +} diff --git a/tm2/pkg/crypto/keys/keybase_test.go b/tm2/pkg/crypto/keys/keybase_test.go index 0c43dbd8dc5..32cc8788b52 100644 --- a/tm2/pkg/crypto/keys/keybase_test.go +++ b/tm2/pkg/crypto/keys/keybase_test.go @@ -24,53 +24,6 @@ func TestCreateAccountInvalidMnemonic(t *testing.T) { assert.Equal(t, "invalid mnemonic", err.Error()) } -func TestCreateLedgerUnsupportedAlgo(t *testing.T) { - t.Parallel() - - kb := NewInMemory() - _, err := kb.CreateLedger("some_account", Ed25519, "cosmos", 0, 1) - assert.Error(t, err) - assert.Equal(t, "unsupported signing algo: only secp256k1 is supported", err.Error()) -} - -func TestCreateLedger(t *testing.T) { - t.Parallel() - - kb := NewInMemory() - - // test_cover and test_unit will result in different answers - // test_cover does not compile some dependencies so ledger is disabled - // test_unit may add a ledger mock - // both cases are acceptable - ledger, err := kb.CreateLedger("some_account", Secp256k1, "cosmos", 3, 1) - if err != nil { - assert.Error(t, err) - assert.Contains(t, err.Error(), "LedgerHID device (idx 0) not found.") - - assert.Nil(t, ledger) - t.Skip("ledger nano S: support for ledger devices is not available in this executable") - return - } - - // The mock is available, check that the address is correct - pubKey := ledger.GetPubKey() - pubs := crypto.PubKeyToBech32(pubKey) - assert.Equal(t, "cosmospub1addwnpepqdszcr95mrqqs8lw099aa9h8h906zmet22pmwe9vquzcgvnm93eqygufdlv", pubs) - - // Check that restoring the key gets the same results - restoredKey, err := kb.GetByName("some_account") - assert.NotNil(t, restoredKey) - assert.Equal(t, "some_account", restoredKey.GetName()) - assert.Equal(t, TypeLedger, restoredKey.GetType()) - pubKey = restoredKey.GetPubKey() - pubs = crypto.PubKeyToBech32(pubKey) - assert.Equal(t, "cosmospub1addwnpepqdszcr95mrqqs8lw099aa9h8h906zmet22pmwe9vquzcgvnm93eqygufdlv", pubs) - - path, err := restoredKey.GetPath() - assert.NoError(t, err) - assert.Equal(t, "44'/118'/3'/0/1", path.String()) -} - // TestKeyManagement makes sure we can manipulate these keys well func TestKeyManagement(t *testing.T) { t.Parallel() diff --git a/tm2/pkg/crypto/ledger/discover.go b/tm2/pkg/crypto/ledger/discover.go new file mode 100644 index 00000000000..d610b56635e --- /dev/null +++ b/tm2/pkg/crypto/ledger/discover.go @@ -0,0 +1,19 @@ +//go:build !ledger_suite +// +build !ledger_suite + +package ledger + +import ( + ledger_go "github.com/cosmos/ledger-cosmos-go" +) + +// discoverLedger defines a function to be invoked at runtime for discovering +// a connected Ledger device. +var discoverLedger discoverLedgerFn = func() (LedgerSECP256K1, error) { + device, err := ledger_go.FindLedgerCosmosUserApp() + if err != nil { + return nil, err + } + + return device, nil +} diff --git a/tm2/pkg/crypto/ledger/discover_mock.go b/tm2/pkg/crypto/ledger/discover_mock.go new file mode 100644 index 00000000000..1f5bdbafdf3 --- /dev/null +++ b/tm2/pkg/crypto/ledger/discover_mock.go @@ -0,0 +1,69 @@ +//go:build ledger_suite +// +build ledger_suite + +package ledger + +import ( + btcec "github.com/btcsuite/btcd/btcec/v2" + "github.com/gnolang/gno/tm2/pkg/crypto/secp256k1" +) + +// discoverLedger defines a function to be invoked at runtime for discovering +// a connected Ledger device. +var discoverLedger discoverLedgerFn = func() (LedgerSECP256K1, error) { + privateKey := secp256k1.GenPrivKey() + + _, pubKeyObject := btcec.PrivKeyFromBytes(privateKey[:]) + + return &MockLedger{ + GetAddressPubKeySECP256K1Fn: func(data []uint32, str string) ([]byte, string, error) { + return pubKeyObject.SerializeCompressed(), privateKey.PubKey().Address().String(), nil + }, + }, nil +} + +type ( + closeDelegate func() error + getPublicKeySECP256K1Delegate func([]uint32) ([]byte, error) + getAddressPubKeySECP256K1Delegate func([]uint32, string) ([]byte, string, error) + signSECP256K1Delegate func([]uint32, []byte, byte) ([]byte, error) +) + +type MockLedger struct { + CloseFn closeDelegate + GetPublicKeySECP256K1Fn getPublicKeySECP256K1Delegate + GetAddressPubKeySECP256K1Fn getAddressPubKeySECP256K1Delegate + SignSECP256K1Fn signSECP256K1Delegate +} + +func (m *MockLedger) Close() error { + if m.CloseFn != nil { + return m.CloseFn() + } + + return nil +} + +func (m *MockLedger) GetPublicKeySECP256K1(data []uint32) ([]byte, error) { + if m.GetPublicKeySECP256K1Fn != nil { + return m.GetPublicKeySECP256K1Fn(data) + } + + return nil, nil +} + +func (m *MockLedger) GetAddressPubKeySECP256K1(data []uint32, str string) ([]byte, string, error) { + if m.GetAddressPubKeySECP256K1Fn != nil { + return m.GetAddressPubKeySECP256K1Fn(data, str) + } + + return nil, "", nil +} + +func (m *MockLedger) SignSECP256K1(d1 []uint32, d2 []byte, d3 byte) ([]byte, error) { + if m.SignSECP256K1Fn != nil { + return m.SignSECP256K1Fn(d1, d2, d3) + } + + return nil, nil +} diff --git a/tm2/pkg/crypto/ledger/ledger_secp256k1.go b/tm2/pkg/crypto/ledger/ledger_secp256k1.go index f154dbf376c..56877b813a5 100644 --- a/tm2/pkg/crypto/ledger/ledger_secp256k1.go +++ b/tm2/pkg/crypto/ledger/ledger_secp256k1.go @@ -9,7 +9,6 @@ import ( "github.com/btcsuite/btcd/btcec/v2/ecdsa" secp "github.com/decred/dcrd/dcrec/secp256k1/v4" - ledger "github.com/cosmos/ledger-cosmos-go" "github.com/gnolang/gno/tm2/pkg/amino" "github.com/gnolang/gno/tm2/pkg/crypto" "github.com/gnolang/gno/tm2/pkg/crypto/hd" @@ -45,17 +44,6 @@ type ( } ) -// discoverLedger defines a function to be invoked at runtime for discovering -// a connected Ledger device. -var discoverLedger discoverLedgerFn = func() (LedgerSECP256K1, error) { - device, err := ledger.FindLedgerCosmosUserApp() - if err != nil { - return nil, err - } - - return device, nil -} - // NewPrivKeyLedgerSecp256k1Unsafe will generate a new key and store the public key for later use. // // This function is marked as unsafe as it will retrieve a pubkey without user verification.