diff --git a/cli/wallet/wallet.go b/cli/wallet/wallet.go index f9334a84b..0cf11483c 100644 --- a/cli/wallet/wallet.go +++ b/cli/wallet/wallet.go @@ -15,6 +15,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/encoding/address" "github.com/nspcc-dev/neo-go/pkg/smartcontract" "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" + "github.com/nspcc-dev/neo-go/pkg/vm" "github.com/nspcc-dev/neo-go/pkg/wallet" "github.com/urfave/cli" ) @@ -120,6 +121,18 @@ func NewCommands() []cli.Command { decryptFlag, }, }, + { + Name: "dump-keys", + Usage: "dump public keys for account", + Action: dumpKeys, + Flags: []cli.Flag{ + walletPathFlag, + cli.StringFlag{ + Name: "address, a", + Usage: "address to print public keys for", + }, + }, + }, { Name: "export", Usage: "export keys for address", @@ -537,6 +550,56 @@ func dumpWallet(ctx *cli.Context) error { return nil } +func dumpKeys(ctx *cli.Context) error { + wall, err := openWallet(ctx.String("wallet")) + if err != nil { + return cli.NewExitError(err, 1) + } + accounts := wall.Accounts + addr := ctx.String("address") + if addr != "" { + u, err := flags.ParseAddress(addr) + if err != nil { + return cli.NewExitError(fmt.Errorf("invalid address: %w", err), 1) + } + acc := wall.GetAccount(u) + if acc == nil { + return cli.NewExitError("account is missing", 1) + } + accounts = []*wallet.Account{acc} + } + + hasPrinted := false + for _, acc := range accounts { + pub, ok := vm.ParseSignatureContract(acc.Contract.Script) + if ok { + if hasPrinted { + fmt.Fprintln(ctx.App.Writer) + } + fmt.Fprintf(ctx.App.Writer, "%s (simple signature contract):\n", acc.Address) + fmt.Fprintln(ctx.App.Writer, hex.EncodeToString(pub)) + hasPrinted = true + continue + } + n, bs, ok := vm.ParseMultiSigContract(acc.Contract.Script) + if ok { + if hasPrinted { + fmt.Fprintln(ctx.App.Writer) + } + fmt.Fprintf(ctx.App.Writer, "%s (%d out of %d multisig contract):\n", acc.Address, n, len(bs)) + for i := range bs { + fmt.Fprintln(ctx.App.Writer, hex.EncodeToString(bs[i])) + } + hasPrinted = true + continue + } + if addr != "" { + return cli.NewExitError(fmt.Errorf("Unknown script type for address %s", addr), 1) + } + } + return nil +} + func createWallet(ctx *cli.Context) error { path := ctx.String("wallet") if len(path) == 0 { diff --git a/cli/wallet_test.go b/cli/wallet_test.go index 259d8ed0f..4ee7e09f2 100644 --- a/cli/wallet_test.go +++ b/cli/wallet_test.go @@ -337,6 +337,49 @@ func TestWalletDump(t *testing.T) { }) } +func TestDumpKeys(t *testing.T) { + e := newExecutor(t, false) + cmd := []string{"neo-go", "wallet", "dump-keys", "--wallet", validatorWallet} + pubRegex := "^0[23][a-hA-H0-9]{64}$" + t.Run("all", func(t *testing.T) { + e.Run(t, cmd...) + e.checkNextLine(t, "NbTiM6h8r99kpRtb428XcsUk1TzKed2gTc") + e.checkNextLine(t, pubRegex) + e.checkNextLine(t, "^\\s*$") + e.checkNextLine(t, "NUVPACMnKFhpuHjsRjhUvXz1XhqfGZYVtY") + for i := 0; i < 4; i++ { + e.checkNextLine(t, pubRegex) + } + e.checkNextLine(t, "^\\s*$") + e.checkNextLine(t, "NVNvVRW5Q5naSx2k2iZm7xRgtRNGuZppAK") + e.checkNextLine(t, pubRegex) + e.checkEOF(t) + }) + t.Run("simple signature", func(t *testing.T) { + cmd := append(cmd, "--address", "NbTiM6h8r99kpRtb428XcsUk1TzKed2gTc") + e.Run(t, cmd...) + e.checkNextLine(t, "simple signature contract") + e.checkNextLine(t, pubRegex) + e.checkEOF(t) + }) + t.Run("3/4 multisig", func(t *testing.T) { + cmd := append(cmd, "-a", "NUVPACMnKFhpuHjsRjhUvXz1XhqfGZYVtY") + e.Run(t, cmd...) + e.checkNextLine(t, "3 out of 4 multisig contract") + for i := 0; i < 4; i++ { + e.checkNextLine(t, pubRegex) + } + e.checkEOF(t) + }) + t.Run("1/1 multisig", func(t *testing.T) { + cmd := append(cmd, "--address", "NVNvVRW5Q5naSx2k2iZm7xRgtRNGuZppAK") + e.Run(t, cmd...) + e.checkNextLine(t, "1 out of 1 multisig contract") + e.checkNextLine(t, pubRegex) + e.checkEOF(t) + }) +} + // Testcase is the wallet of privnet validator. func TestWalletConvert(t *testing.T) { tmpDir := path.Join(os.TempDir(), "neogo.test.convert") diff --git a/pkg/vm/contract_checks.go b/pkg/vm/contract_checks.go index 00b425032..7a721aa3a 100644 --- a/pkg/vm/contract_checks.go +++ b/pkg/vm/contract_checks.go @@ -105,24 +105,32 @@ func ParseMultiSigContract(script []byte) (int, [][]byte, bool) { // IsSignatureContract checks whether the passed script is a signature check // contract. func IsSignatureContract(script []byte) bool { + _, ok := ParseSignatureContract(script) + return ok +} + +// ParseSignatureContract parses simple signature contract and returns +// public key. +func ParseSignatureContract(script []byte) ([]byte, bool) { if len(script) != 41 { - return false + return nil, false } ctx := NewContext(script) instr, param, err := ctx.Next() if err != nil || instr != opcode.PUSHDATA1 || len(param) != 33 { - return false + return nil, false } + pub := param instr, _, err = ctx.Next() if err != nil || instr != opcode.PUSHNULL { - return false + return nil, false } instr, param, err = ctx.Next() if err != nil || instr != opcode.SYSCALL || binary.LittleEndian.Uint32(param) != verifyInteropID { - return false + return nil, false } - return true + return pub, true } // IsStandardContract checks whether the passed script is a signature or diff --git a/pkg/vm/contract_checks_test.go b/pkg/vm/contract_checks_test.go index 88eb1aeab..29146c4f4 100644 --- a/pkg/vm/contract_checks_test.go +++ b/pkg/vm/contract_checks_test.go @@ -25,6 +25,15 @@ func testSignatureContract() []byte { return prog } +func TestParseSignatureContract(t *testing.T) { + prog := testSignatureContract() + pub := randomBytes(33) + copy(prog[2:], pub) + actual, ok := ParseSignatureContract(prog) + require.True(t, ok) + require.Equal(t, pub, actual) +} + func TestIsSignatureContract(t *testing.T) { t.Run("valid contract", func(t *testing.T) { prog := testSignatureContract()