From 8be9158e2f08bb08f3a229f538aa11cc793d72cf Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Thu, 3 Dec 2020 14:59:38 +0300 Subject: [PATCH 1/9] cli: remove explicit `client.Init()` calls This is done inside `GetRPCClient()`. --- cli/smartcontract/smart_contract.go | 3 --- cli/wallet/nep17.go | 12 ------------ 2 files changed, 15 deletions(-) diff --git a/cli/smartcontract/smart_contract.go b/cli/smartcontract/smart_contract.go index dffb75829..14ddf157b 100644 --- a/cli/smartcontract/smart_contract.go +++ b/cli/smartcontract/smart_contract.go @@ -500,9 +500,6 @@ func invokeInternal(ctx *cli.Context, signAndPush bool) error { if err != nil { return err } - if err = c.Init(); err != nil { - return err - } resp, err = c.InvokeFunction(script, operation, params, cosigners) if err != nil { diff --git a/cli/wallet/nep17.go b/cli/wallet/nep17.go index 27eab4ee5..40f32d560 100644 --- a/cli/wallet/nep17.go +++ b/cli/wallet/nep17.go @@ -158,10 +158,6 @@ func getNEP17Balance(ctx *cli.Context) error { if err != nil { return cli.NewExitError(err, 1) } - err = c.Init() - if err != nil { - return cli.NewExitError(fmt.Errorf("failed to init RPC client: %w", err), 1) - } name := ctx.String("token") @@ -366,10 +362,6 @@ func multiTransferNEP17(ctx *cli.Context) error { if err != nil { return cli.NewExitError(err, 1) } - err = c.Init() - if err != nil { - return cli.NewExitError(fmt.Errorf("failed to init RPC client: %w", err), 1) - } if ctx.NArg() == 0 { return cli.NewExitError("empty recipients list", 1) @@ -433,10 +425,6 @@ func transferNEP17(ctx *cli.Context) error { if err != nil { return cli.NewExitError(err, 1) } - err = c.Init() - if err != nil { - return cli.NewExitError(fmt.Errorf("failed to init RPC client: %w", err), 1) - } toFlag := ctx.Generic("to").(*flags.Address) to := toFlag.Uint160() From d71e45bcc5c8f7282e65ef9e592d515c1578fe87 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Fri, 4 Dec 2020 10:59:46 +0300 Subject: [PATCH 2/9] cli/contract: make `--force` flag bool --- cli/smartcontract/smart_contract.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cli/smartcontract/smart_contract.go b/cli/smartcontract/smart_contract.go index 14ddf157b..86839d294 100644 --- a/cli/smartcontract/smart_contract.go +++ b/cli/smartcontract/smart_contract.go @@ -56,7 +56,7 @@ var ( Name: "out", Usage: "file to put JSON transaction to", } - forceFlag = cli.StringFlag{ + forceFlag = cli.BoolFlag{ Name: "force", Usage: "force-push the transaction in case of bad VM state after test script invocation", } @@ -507,7 +507,7 @@ func invokeInternal(ctx *cli.Context, signAndPush bool) error { } if signAndPush && resp.State != "HALT" { errText := fmt.Sprintf("Warning: %s VM state returned from the RPC node: %s\n", resp.State, resp.FaultException) - if ctx.String("force") == "" { + if !ctx.Bool("force") { return cli.NewExitError(errText+". Use --force flag to send the transaction anyway.", 1) } fmt.Fprintln(ctx.App.Writer, errText+". Sending transaction...") From bb316f4652333679bbb240d3f2d21ad997dfa671 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Fri, 4 Dec 2020 11:03:39 +0300 Subject: [PATCH 3/9] cli/test: trim newline char in `checkNextLine` --- cli/executor_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/executor_test.go b/cli/executor_test.go index df8eb7529..c53231615 100644 --- a/cli/executor_test.go +++ b/cli/executor_test.go @@ -132,7 +132,7 @@ func (e *executor) GetTransaction(t *testing.T, h util.Uint256) (*transaction.Tr func (e *executor) getNextLine(t *testing.T) string { line, err := e.Out.ReadString('\n') require.NoError(t, err) - return line + return strings.TrimSuffix(line, "\n") } func (e *executor) checkNextLine(t *testing.T, expected string) { From 573d1d10c065281e93318fa89c18b36b36a9b580 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Thu, 3 Dec 2020 14:57:45 +0300 Subject: [PATCH 4/9] cli: add tests for `contract` command --- cli/contract_test.go | 163 ++++++++++++++++++++++++++-- cli/smartcontract/smart_contract.go | 2 + cli/testdata/deploy/main.go | 5 + pkg/compiler/codegen.go | 8 +- 4 files changed, 167 insertions(+), 11 deletions(-) diff --git a/cli/contract_test.go b/cli/contract_test.go index 3289e907e..69ac75e21 100644 --- a/cli/contract_test.go +++ b/cli/contract_test.go @@ -16,6 +16,61 @@ import ( "github.com/stretchr/testify/require" ) +func TestContractInitAndCompile(t *testing.T) { + tmpDir := path.Join(os.TempDir(), "neogo.inittest") + require.NoError(t, os.Mkdir(tmpDir, os.ModePerm)) + defer os.RemoveAll(tmpDir) + + e := newExecutor(t, false) + defer e.Close(t) + + t.Run("no path is provided", func(t *testing.T) { + e.RunWithError(t, "neo-go", "contract", "init") + }) + t.Run("invalid path", func(t *testing.T) { + e.RunWithError(t, "neo-go", "contract", "init", "--name", "\x00") + }) + + ctrPath := path.Join(tmpDir, "testcontract") + e.Run(t, "neo-go", "contract", "init", "--name", ctrPath) + + t.Run("don't rewrite existing directory", func(t *testing.T) { + e.RunWithError(t, "neo-go", "contract", "init", "--name", ctrPath) + }) + + // For proper nef generation. + config.Version = "0.90.0-test" + + srcPath := path.Join(ctrPath, "main.go") + cfgPath := path.Join(ctrPath, "neo-go.yml") + nefPath := path.Join(tmpDir, "testcontract.nef") + manifestPath := path.Join(tmpDir, "testcontract.manifest.json") + cmd := []string{"neo-go", "contract", "compile"} + t.Run("missing source", func(t *testing.T) { + e.RunWithError(t, cmd...) + }) + + cmd = append(cmd, "--in", srcPath, "--out", nefPath, "--manifest", manifestPath) + t.Run("missing config, but require manifest", func(t *testing.T) { + e.RunWithError(t, cmd...) + }) + t.Run("provided non-existent config", func(t *testing.T) { + cfgName := path.Join(ctrPath, "notexists.yml") + e.RunWithError(t, append(cmd, "--config", cfgName)...) + }) + + cmd = append(cmd, "--config", cfgPath) + e.Run(t, cmd...) + e.checkEOF(t) + require.FileExists(t, nefPath) + require.FileExists(t, manifestPath) + + t.Run("output hex script with --verbose", func(t *testing.T) { + e.Run(t, append(cmd, "--verbose")...) + e.checkNextLine(t, "^[0-9a-hA-H]+$") + }) +} + func TestComlileAndInvokeFunction(t *testing.T) { e := newExecutor(t, true) defer e.Close(t) @@ -23,7 +78,10 @@ func TestComlileAndInvokeFunction(t *testing.T) { // For proper nef generation. config.Version = "0.90.0-test" - tmpDir := os.TempDir() + tmpDir := path.Join(os.TempDir(), "neogo.test.compileandinvoke") + require.NoError(t, os.Mkdir(tmpDir, os.ModePerm)) + defer os.RemoveAll(tmpDir) + nefName := path.Join(tmpDir, "deploy.nef") manifestName := path.Join(tmpDir, "deploy.manifest.json") e.Run(t, "neo-go", "contract", "compile", @@ -31,10 +89,12 @@ func TestComlileAndInvokeFunction(t *testing.T) { "--config", "testdata/deploy/neo-go.yml", "--out", nefName, "--manifest", manifestName) - defer func() { - os.Remove(nefName) - os.Remove(manifestName) - }() + // Check that it is possible to invoke before deploy. + // This doesn't make much sense, because every method has an offset + // which is contained in the manifest. This should be either removed or refactored. + e.Run(t, "neo-go", "contract", "testinvokescript", + "--rpc-endpoint", "http://"+e.RPC.Addr, + "--in", nefName, "--", util.Uint160{1, 2, 3}.StringLE()) e.In.WriteString("one\r") e.Run(t, "neo-go", "contract", "deploy", @@ -49,10 +109,33 @@ func TestComlileAndInvokeFunction(t *testing.T) { require.NoError(t, err) e.checkTxPersisted(t) - e.In.WriteString("one\r") - e.Run(t, "neo-go", "contract", "testinvokefunction", - "--rpc-endpoint", "http://"+e.RPC.Addr, - h.StringLE(), "getValue") + cmd := []string{"neo-go", "contract", "testinvokefunction", + "--rpc-endpoint", "http://" + e.RPC.Addr} + t.Run("missing hash", func(t *testing.T) { + e.RunWithError(t, cmd...) + }) + t.Run("invalid hash", func(t *testing.T) { + e.RunWithError(t, append(cmd, "notahash")...) + }) + + cmd = append(cmd, h.StringLE()) + t.Run("missing method", func(t *testing.T) { + e.RunWithError(t, cmd...) + }) + + cmd = append(cmd, "getValue") + t.Run("invalid params", func(t *testing.T) { + e.RunWithError(t, append(cmd, "[")...) + }) + t.Run("invalid cosigner", func(t *testing.T) { + e.RunWithError(t, append(cmd, "--", "notahash")...) + }) + t.Run("missing RPC address", func(t *testing.T) { + e.RunWithError(t, "neo-go", "contract", "testinvokefunction", + h.StringLE(), "getValue") + }) + + e.Run(t, cmd...) res := new(result.Invoke) require.NoError(t, json.Unmarshal(e.Out.Bytes(), res)) @@ -60,6 +143,32 @@ func TestComlileAndInvokeFunction(t *testing.T) { require.Len(t, res.Stack, 1) require.Equal(t, []byte("on create|sub create"), res.Stack[0].Value()) + t.Run("real invoke", func(t *testing.T) { + cmd := []string{"neo-go", "contract", "invokefunction", + "--rpc-endpoint", "http://" + e.RPC.Addr} + t.Run("missing wallet", func(t *testing.T) { + cmd := append(cmd, h.StringLE(), "getValue") + e.RunWithError(t, cmd...) + }) + t.Run("non-existent wallet", func(t *testing.T) { + cmd := append(cmd, "--wallet", path.Join(tmpDir, "not.exists"), + h.StringLE(), "getValue") + e.RunWithError(t, cmd...) + }) + + cmd = append(cmd, "--wallet", validatorWallet, "--address", validatorAddr) + e.In.WriteString("one\r") + e.Run(t, append(cmd, h.StringLE(), "getValue")...) + + t.Run("failind method", func(t *testing.T) { + e.In.WriteString("one\r") + e.RunWithError(t, append(cmd, h.StringLE(), "fail")...) + + e.In.WriteString("one\r") + e.Run(t, append(cmd, "--force", h.StringLE(), "fail")...) + }) + }) + t.Run("Update", func(t *testing.T) { nefName := path.Join(tmpDir, "updated.nef") manifestName := path.Join(tmpDir, "updated.manifest.json") @@ -101,6 +210,42 @@ func TestComlileAndInvokeFunction(t *testing.T) { }) } +func TestContractInspect(t *testing.T) { + e := newExecutor(t, false) + defer e.Close(t) + + // For proper nef generation. + config.Version = "0.90.0-test" + const srcPath = "testdata/deploy/main.go" + + tmpDir := path.Join(os.TempDir(), "neogo.test.contract.inspect") + require.NoError(t, os.Mkdir(tmpDir, os.ModePerm)) + defer os.RemoveAll(tmpDir) + + nefName := path.Join(tmpDir, "deploy.nef") + manifestName := path.Join(tmpDir, "deploy.manifest.json") + e.Run(t, "neo-go", "contract", "compile", + "--in", srcPath, + "--config", "testdata/deploy/neo-go.yml", + "--out", nefName, "--manifest", manifestName) + + cmd := []string{"neo-go", "contract", "inspect"} + t.Run("missing input", func(t *testing.T) { + e.RunWithError(t, cmd...) + }) + t.Run("with raw '.go'", func(t *testing.T) { + e.RunWithError(t, append(cmd, "--in", srcPath)...) + e.Run(t, append(cmd, "--in", srcPath, "--compile")...) + require.True(t, strings.Contains(e.Out.String(), "SYSCALL")) + }) + t.Run("with nef", func(t *testing.T) { + e.RunWithError(t, append(cmd, "--in", nefName, "--compile")...) + e.RunWithError(t, append(cmd, "--in", path.Join(tmpDir, "not.exists"))...) + e.Run(t, append(cmd, "--in", nefName)...) + require.True(t, strings.Contains(e.Out.String(), "SYSCALL")) + }) +} + func TestCompileExamples(t *testing.T) { const examplePath = "../examples" infos, err := ioutil.ReadDir(examplePath) diff --git a/cli/smartcontract/smart_contract.go b/cli/smartcontract/smart_contract.go index 86839d294..8f37467c2 100644 --- a/cli/smartcontract/smart_contract.go +++ b/cli/smartcontract/smart_contract.go @@ -7,6 +7,7 @@ import ( "fmt" "io/ioutil" "os" + "path" "path/filepath" "strings" @@ -350,6 +351,7 @@ func initSmartContract(ctx *cli.Context) error { } basePath := contractName + contractName = path.Base(contractName) fileName := "main.go" // create base directory diff --git a/cli/testdata/deploy/main.go b/cli/testdata/deploy/main.go index f1d39360e..6182751c9 100644 --- a/cli/testdata/deploy/main.go +++ b/cli/testdata/deploy/main.go @@ -17,6 +17,11 @@ func _deploy(isUpdate bool) { storage.Put(ctx, key, value) } +// Fail just fails. +func Fail() { + panic("as expected") +} + // Update updates contract with the new one. func Update(script, manifest []byte) { contract.Update(script, manifest) diff --git a/pkg/compiler/codegen.go b/pkg/compiler/codegen.go index daf288664..06548e535 100644 --- a/pkg/compiler/codegen.go +++ b/pkg/compiler/codegen.go @@ -879,8 +879,12 @@ func (c *codegen) Visit(node ast.Node) ast.Visitor { for _, p := range n.Args[1:] { params = append(params, c.scTypeFromExpr(p)) } - name := constant.StringVal(tv.Value) - c.emittedEvents[name] = append(c.emittedEvents[name], params) + // Sometimes event name is stored in a var. + // Skip in this case. + if tv.Value != nil { + name := constant.StringVal(tv.Value) + c.emittedEvents[name] = append(c.emittedEvents[name], params) + } } c.convertSyscall(n, f.pkg.Name(), f.name) default: From 66402923b96a16a3617779280c9a5a0f16742110 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Thu, 3 Dec 2020 15:57:57 +0300 Subject: [PATCH 5/9] cli/server: remove unused `readBlock()` It became obsolete after 6f7284906a. --- cli/server/server.go | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/cli/server/server.go b/cli/server/server.go index bd4c3f973..6341bbbad 100644 --- a/cli/server/server.go +++ b/cli/server/server.go @@ -295,17 +295,6 @@ func restoreDB(ctx *cli.Context) error { return nil } -// readBlock performs reading of block size and then bytes with the length equal to that size. -func readBlock(reader *io.BinReader) ([]byte, error) { - var size = reader.ReadU32LE() - bytes := make([]byte, size) - reader.ReadBytes(bytes) - if reader.Err != nil { - return nil, reader.Err - } - return bytes, nil -} - func startServer(ctx *cli.Context) error { cfg, err := getConfigFromContext(ctx) if err != nil { From f4cbb1ae74a1d5cd046952fd5d83a2daf1a4ad01 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Thu, 3 Dec 2020 17:25:17 +0300 Subject: [PATCH 6/9] cli/test: clean input buffer after execution --- cli/executor_test.go | 4 +++- cli/multisig_test.go | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/cli/executor_test.go b/cli/executor_test.go index c53231615..be56c31ec 100644 --- a/cli/executor_test.go +++ b/cli/executor_test.go @@ -184,7 +184,9 @@ func (e *executor) Run(t *testing.T, args ...string) { func (e *executor) run(args ...string) error { e.Out.Reset() e.Err.Reset() - return e.CLI.Run(args) + err := e.CLI.Run(args) + e.In.Reset() + return err } func (e *executor) checkTxPersisted(t *testing.T, prefix ...string) (*transaction.Transaction, uint32) { diff --git a/cli/multisig_test.go b/cli/multisig_test.go index d8aebfef5..7694d0c73 100644 --- a/cli/multisig_test.go +++ b/cli/multisig_test.go @@ -36,8 +36,8 @@ func TestSignMultisigTx(t *testing.T) { defer os.Remove(wallet2Path) addAccount := func(w string, wif string) { - e.In.WriteString("acc\rpass\rpass\r") e.Run(t, "neo-go", "wallet", "init", "--wallet", w) + e.In.WriteString("acc\rpass\rpass\r") e.Run(t, "neo-go", "wallet", "import-multisig", "--wallet", w, "--wif", wif, From 4cd5747ab7ef1b8236649bcf7e038e9e842559ea Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Thu, 3 Dec 2020 17:30:09 +0300 Subject: [PATCH 7/9] cli/test: add test for `wallet dump --decrypt` --- cli/wallet_test.go | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/cli/wallet_test.go b/cli/wallet_test.go index 601639c5a..e425e247f 100644 --- a/cli/wallet_test.go +++ b/cli/wallet_test.go @@ -286,10 +286,27 @@ func TestWalletDump(t *testing.T) { e := newExecutor(t, false) defer e.Close(t) - e.Run(t, "neo-go", "wallet", "dump", "--wallet", "testdata/testwallet.json") + cmd := []string{"neo-go", "wallet", "dump", "--wallet", "testdata/testwallet.json"} + e.Run(t, cmd...) rawStr := strings.TrimSpace(e.Out.String()) w := new(wallet.Wallet) require.NoError(t, json.Unmarshal([]byte(rawStr), w)) require.Equal(t, 1, len(w.Accounts)) require.Equal(t, "NNuJqXDnRqvwgzhSzhH4jnVFWB1DyZ34EM", w.Accounts[0].Address) + + t.Run("with decrypt", func(t *testing.T) { + cmd = append(cmd, "--decrypt") + t.Run("invalid password", func(t *testing.T) { + e.In.WriteString("invalidpass\r") + e.RunWithError(t, cmd...) + }) + + e.In.WriteString("testpass\r") + e.Run(t, cmd...) + rawStr := strings.TrimSpace(e.Out.String()) + w := new(wallet.Wallet) + require.NoError(t, json.Unmarshal([]byte(rawStr), w)) + require.Equal(t, 1, len(w.Accounts)) + require.Equal(t, "NNuJqXDnRqvwgzhSzhH4jnVFWB1DyZ34EM", w.Accounts[0].Address) + }) } From 239a8c3de75281391843c7af6a6ce8c68fee6e26 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Thu, 3 Dec 2020 17:33:52 +0300 Subject: [PATCH 8/9] cli: fix `wallet convert` and add tests Close #1589. --- cli/testdata/wallets/testwallet_NEO2.json | 1 + cli/testdata/wallets/testwallet_NEO3.json | 55 ++++++++ cli/wallet/legacy.go | 165 ++++++++++++++++++++++ cli/wallet/legacy_test.go | 111 +++++++++++++++ cli/wallet/wallet.go | 25 +--- cli/wallet_test.go | 46 ++++++ 6 files changed, 383 insertions(+), 20 deletions(-) create mode 100644 cli/testdata/wallets/testwallet_NEO2.json create mode 100644 cli/testdata/wallets/testwallet_NEO3.json create mode 100644 cli/wallet/legacy.go create mode 100644 cli/wallet/legacy_test.go diff --git a/cli/testdata/wallets/testwallet_NEO2.json b/cli/testdata/wallets/testwallet_NEO2.json new file mode 100644 index 000000000..d03726fd9 --- /dev/null +++ b/cli/testdata/wallets/testwallet_NEO2.json @@ -0,0 +1 @@ +{"name":"wallet1","version":"1.0","scrypt":{"n":16384,"r":8,"p":8},"accounts":[{"address":"AKkkumHbBipZ46UMZJoFynJMXzSRnBvKcs","label":null,"isDefault":false,"lock":false,"key":"6PYLmjBYJ4wQTCEfqvnznGJwZeW9pfUcV5m5oreHxqryUgqKpTRAFt9L8Y","contract":{"script":"2102b3622bf4017bdfe317c58aed5f4c753f206b7db896046fa7d774bbc4bf7f8dc2ac","parameters":[{"name":"parameter0","type":"Signature"}],"deployed":false},"extra":null},{"address":"AZ81H31DMWzbSnFDLFkzh9vHwaDLayV7fU","label":null,"isDefault":false,"lock":false,"key":"6PYLmjBYJ4wQTCEfqvnznGJwZeW9pfUcV5m5oreHxqryUgqKpTRAFt9L8Y","contract":{"script":"532102103a7f7dd016558597f7960d27c516a4394fd968b9e65155eb4b013e4040406e2102a7bc55fe8684e0119768d104ba30795bdcc86619e864add26156723ed185cd622102b3622bf4017bdfe317c58aed5f4c753f206b7db896046fa7d774bbc4bf7f8dc22103d90c07df63e690ce77912e10ab51acc944b66860237b608c4f8f8309e71ee69954ae","parameters":[{"name":"parameter0","type":"Signature"},{"name":"parameter1","type":"Signature"},{"name":"parameter2","type":"Signature"}],"deployed":false},"extra":null}],"extra":null} \ No newline at end of file diff --git a/cli/testdata/wallets/testwallet_NEO3.json b/cli/testdata/wallets/testwallet_NEO3.json new file mode 100644 index 000000000..27a48a3ed --- /dev/null +++ b/cli/testdata/wallets/testwallet_NEO3.json @@ -0,0 +1,55 @@ +{ + "version": "3.0", + "accounts": [ + { + "address": "NbTiM6h8r99kpRtb428XcsUk1TzKed2gTc", + "key": "6PYN7LvaWqBNw7Xb7a52LSbPnP91kyuzYi3HncGvQwQoYAY2W8DncTgpux", + "label": "", + "contract": { + "script": "DCECs2Ir9AF73+MXxYrtX0x1PyBrfbiWBG+n13S7xL9/jcILQZVEDXg=", + "parameters": [ + { + "name": "parameter0", + "type": "Signature" + } + ], + "deployed": false + }, + "lock": false, + "isdefault": false + }, + { + "address": "NUVPACMnKFhpuHjsRjhUvXz1XhqfGZYVtY", + "key": "6PYN7LvaWqBNw7Xb7a52LSbPnP91kyuzYi3HncGvQwQoYAY2W8DncTgpux", + "label": "", + "contract": { + "script": "EwwhAhA6f33QFlWFl/eWDSfFFqQ5T9loueZRVetLAT5AQEBuDCECp7xV/oaE4BGXaNEEujB5W9zIZhnoZK3SYVZyPtGFzWIMIQKzYiv0AXvf4xfFiu1fTHU/IGt9uJYEb6fXdLvEv3+NwgwhA9kMB99j5pDOd5EuEKtRrMlEtmhgI3tgjE+PgwnnHuaZFAtBE43vrw==", + "parameters": [ + { + "name": "parameter0", + "type": "Signature" + }, + { + "name": "parameter1", + "type": "Signature" + }, + { + "name": "parameter2", + "type": "Signature" + } + ], + "deployed": false + }, + "lock": false, + "isdefault": false + } + ], + "scrypt": { + "n": 16384, + "r": 8, + "p": 8 + }, + "extra": { + "Tokens": null + } +} diff --git a/cli/wallet/legacy.go b/cli/wallet/legacy.go new file mode 100644 index 000000000..163353513 --- /dev/null +++ b/cli/wallet/legacy.go @@ -0,0 +1,165 @@ +package wallet + +import ( + "crypto/elliptic" + "encoding/binary" + "encoding/hex" + "encoding/json" + "errors" + "os" + + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + "github.com/nspcc-dev/neo-go/pkg/encoding/address" + "github.com/nspcc-dev/neo-go/pkg/smartcontract" + "github.com/nspcc-dev/neo-go/pkg/wallet" +) + +type ( + walletV2 struct { + Version string `json:"version"` + Accounts []accountV2 `json:"accounts"` + Scrypt keys.ScryptParams `json:"scrypt"` + Extra wallet.Extra `json:"extra"` + } + accountV2 struct { + Address string `json:"address"` + EncryptedWIF string `json:"key"` + Label string `json:"label"` + Contract *struct { + Script string `json:"script"` + Parameters []wallet.ContractParam `json:"parameters"` + Deployed bool `json:"deployed"` + } `json:"contract"` + Locked bool `json:"lock"` + Default bool `json:"isdefault"` + } +) + +// newWalletV2FromFile reads NEO2 wallet from file. +// This should be used read-only, no operations are supported on returned wallet. +func newWalletV2FromFile(path string) (*walletV2, error) { + file, err := os.OpenFile(path, os.O_RDWR, os.ModeAppend) + if err != nil { + return nil, err + } + defer file.Close() + + wall := new(walletV2) + return wall, json.NewDecoder(file).Decode(wall) +} + +const simpleSigLen = 35 + +func (a *accountV2) convert(pass string) (*wallet.Account, error) { + address.Prefix = address.NEO2Prefix + priv, err := keys.NEP2Decrypt(a.EncryptedWIF, pass) + if err != nil { + return nil, err + } + + address.Prefix = address.NEO3Prefix + newAcc, err := wallet.NewAccountFromWIF(priv.WIF()) + if err != nil { + return nil, err + } + if a.Contract != nil { + script, err := hex.DecodeString(a.Contract.Script) + if err != nil { + return nil, err + } + // If it is simple signature script, newAcc does already have it. + if len(script) != simpleSigLen { + nsigs, pubs, ok := parseMultisigContract(script) + if !ok { + return nil, errors.New("invalid multisig contract") + } + script, err := smartcontract.CreateMultiSigRedeemScript(nsigs, pubs) + if err != nil { + return nil, errors.New("can't create new multisig contract") + } + newAcc.Contract.Script = script + newAcc.Contract.Parameters = a.Contract.Parameters + newAcc.Contract.Deployed = a.Contract.Deployed + } + } + newAcc.Address = address.Uint160ToString(newAcc.Contract.ScriptHash()) + newAcc.Default = a.Default + newAcc.Label = a.Label + newAcc.Locked = a.Locked + return newAcc, newAcc.Encrypt(pass) +} + +const ( + opPush1 = 0x51 + opPush16 = 0x60 + opPushBytes1 = 0x01 + opPushBytes2 = 0x02 + opPushBytes33 = 0x21 + opCheckMultisig = 0xAE + opRet = 0x66 +) + +func getNumOfThingsFromInstr(script []byte) (int, int, bool) { + var op = script[0] + switch { + case opPush1 <= op && op <= opPush16: + return int(op-opPush1) + 1, 1, true + case op == opPushBytes1 && len(script) >= 2: + return int(script[1]), 2, true + case op == opPushBytes2 && len(script) >= 3: + return int(binary.LittleEndian.Uint16(script[1:])), 3, true + default: + return 0, 0, false + } +} + +const minMultisigLen = 37 + +// parseMultisigContract accepts multisig verification script from NEO2 +// and returns list of public keys in the same order as in script.. +func parseMultisigContract(script []byte) (int, keys.PublicKeys, bool) { + // It should contain at least 1 public key. + if len(script) < minMultisigLen { + return 0, nil, false + } + + nsigs, offset, ok := getNumOfThingsFromInstr(script) + if !ok { + return 0, nil, false + } + var pubs [][]byte + var nkeys int + for offset < len(script) && script[offset] == opPushBytes33 { + if len(script[offset:]) < 34 { + return 0, nil, false + } + pubs = append(pubs, script[offset+1:offset+34]) + nkeys++ + offset += 34 + } + if nkeys < nsigs || offset >= len(script) { + return 0, nil, false + } + nkeys2, off, ok := getNumOfThingsFromInstr(script[offset:]) + if !ok || nkeys2 != nkeys { + return 0, nil, false + } + end := script[offset+off:] + switch { + case len(end) == 1 && end[0] == opCheckMultisig: + case len(end) == 2 && end[0] == opCheckMultisig && end[1] == opRet: + default: + return 0, nil, false + } + + ret := make(keys.PublicKeys, len(pubs)) + for i := range pubs { + pub, err := keys.NewPublicKeyFromBytes(pubs[i], elliptic.P256()) + if err != nil { + return 0, nil, false + } + ret[i] = pub + + } + return nsigs, ret, true +} diff --git a/cli/wallet/legacy_test.go b/cli/wallet/legacy_test.go new file mode 100644 index 000000000..af952fff5 --- /dev/null +++ b/cli/wallet/legacy_test.go @@ -0,0 +1,111 @@ +package wallet + +import ( + "crypto/elliptic" + "encoding/hex" + "testing" + + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + "github.com/stretchr/testify/require" +) + +func testParseMultisigContract(t *testing.T, s []byte, nsigs int, keys ...*keys.PublicKey) { + ns, ks, ok := parseMultisigContract(s) + if len(keys) == 0 { + require.False(t, ok) + return + } + require.True(t, ok) + require.Equal(t, nsigs, ns) + require.Equal(t, len(keys), len(ks)) + for i := range keys { + require.Equal(t, keys[i], ks[i]) + } +} + +func TestParseMultisigContract(t *testing.T) { + t.Run("single multisig", func(t *testing.T) { + s := fromHex(t, "512102b3622bf4017bdfe317c58aed5f4c753f206b7db896046fa7d774bbc4bf7f8dc251ae") + pub := pubFromHex(t, "02b3622bf4017bdfe317c58aed5f4c753f206b7db896046fa7d774bbc4bf7f8dc2") + t.Run("good, no ret", func(t *testing.T) { + testParseMultisigContract(t, s, 1, pub) + }) + t.Run("good, with ret", func(t *testing.T) { + s := append(s, opRet) + testParseMultisigContract(t, s, 1, pub) + }) + t.Run("bad, no check multisig", func(t *testing.T) { + sBad := make([]byte, len(s)) + copy(sBad, s) + sBad[len(sBad)-1] ^= 0xFF + testParseMultisigContract(t, sBad, 0) + }) + t.Run("bad, invalid number of keys", func(t *testing.T) { + sBad := make([]byte, len(s)) + copy(sBad, s) + sBad[len(sBad)-2] = opPush1 + 1 + testParseMultisigContract(t, sBad, 0) + }) + t.Run("bad, invalid first instruction", func(t *testing.T) { + sBad := make([]byte, len(s)) + copy(sBad, s) + sBad[0] = 0xFF + testParseMultisigContract(t, sBad, 0) + }) + t.Run("bad, invalid public key", func(t *testing.T) { + sBad := make([]byte, len(s)) + copy(sBad, s) + sBad[2] = 0xFF + testParseMultisigContract(t, sBad, 0) + }) + t.Run("bad, many sigs", func(t *testing.T) { + sBad := make([]byte, len(s)) + copy(sBad, s) + sBad[0] = opPush1 + 1 + testParseMultisigContract(t, sBad, 0) + }) + t.Run("empty, no panic", func(t *testing.T) { + testParseMultisigContract(t, []byte{}, 0) + }) + }) + t.Run("3/4 multisig", func(t *testing.T) { + // From privnet consensus wallet. + s := fromHex(t, "532102103a7f7dd016558597f7960d27c516a4394fd968b9e65155eb4b013e4040406e2102a7bc55fe8684e0119768d104ba30795bdcc86619e864add26156723ed185cd622102b3622bf4017bdfe317c58aed5f4c753f206b7db896046fa7d774bbc4bf7f8dc22103d90c07df63e690ce77912e10ab51acc944b66860237b608c4f8f8309e71ee69954ae") + ks := keys.PublicKeys{ + pubFromHex(t, "02103a7f7dd016558597f7960d27c516a4394fd968b9e65155eb4b013e4040406e"), + pubFromHex(t, "02a7bc55fe8684e0119768d104ba30795bdcc86619e864add26156723ed185cd62"), + pubFromHex(t, "02b3622bf4017bdfe317c58aed5f4c753f206b7db896046fa7d774bbc4bf7f8dc2"), + pubFromHex(t, "03d90c07df63e690ce77912e10ab51acc944b66860237b608c4f8f8309e71ee699"), + } + t.Run("good", func(t *testing.T) { + testParseMultisigContract(t, s, 3, ks...) + }) + t.Run("good, with pushbytes1", func(t *testing.T) { + s := append([]byte{opPushBytes1, 3}, s[1:]...) + testParseMultisigContract(t, s, 3, ks...) + }) + t.Run("good, with pushbytes2", func(t *testing.T) { + s := append([]byte{opPushBytes2, 3, 0}, s[1:]...) + testParseMultisigContract(t, s, 3, ks...) + }) + t.Run("bad, no panic on prefix", func(t *testing.T) { + for i := minMultisigLen; i < len(s)-1; i++ { + testParseMultisigContract(t, s[:i], 0) + } + }) + }) + +} + +func fromHex(t *testing.T, s string) []byte { + bs, err := hex.DecodeString(s) + require.NoError(t, err) + return bs +} + +func pubFromHex(t *testing.T, s string) *keys.PublicKey { + bs := fromHex(t, s) + pub, err := keys.NewPublicKeyFromBytes(bs, elliptic.P256()) + require.NoError(t, err) + return pub +} diff --git a/cli/wallet/wallet.go b/cli/wallet/wallet.go index 6ab5eaa38..31ce122d4 100644 --- a/cli/wallet/wallet.go +++ b/cli/wallet/wallet.go @@ -247,40 +247,25 @@ func claimGas(ctx *cli.Context) error { } func convertWallet(ctx *cli.Context) error { - wall, err := openWallet(ctx.String("wallet")) + wall, err := newWalletV2FromFile(ctx.String("wallet")) if err != nil { return cli.NewExitError(err, 1) } - defer wall.Close() newWallet, err := wallet.NewWallet(ctx.String("out")) if err != nil { - return cli.NewExitError(err, -1) + return cli.NewExitError(err, 1) } defer newWallet.Close() for _, acc := range wall.Accounts { - address.Prefix = address.NEO2Prefix - pass, err := input.ReadPassword(ctx.App.Writer, fmt.Sprintf("Enter passphrase for account %s (label '%s') > ", acc.Address, acc.Label)) if err != nil { - return cli.NewExitError(err, -1) - } else if err := acc.Decrypt(pass); err != nil { - return cli.NewExitError("invalid passphrase", -1) + return cli.NewExitError(err, 1) } - - address.Prefix = address.NEO3Prefix - newAcc, err := wallet.NewAccountFromWIF(acc.PrivateKey().WIF()) + newAcc, err := acc.convert(pass) if err != nil { - return cli.NewExitError(fmt.Errorf("can't convert account: %w", err), -1) - } - newAcc.Address = address.Uint160ToString(acc.Contract.ScriptHash()) - newAcc.Contract = acc.Contract - newAcc.Default = acc.Default - newAcc.Label = acc.Label - newAcc.Locked = acc.Locked - if err := newAcc.Encrypt(pass); err != nil { - return cli.NewExitError(fmt.Errorf("can't encrypt converted account: %w", err), -1) + return cli.NewExitError(err, 1) } newWallet.AddAccount(newAcc) } diff --git a/cli/wallet_test.go b/cli/wallet_test.go index e425e247f..986adf4ac 100644 --- a/cli/wallet_test.go +++ b/cli/wallet_test.go @@ -310,3 +310,49 @@ func TestWalletDump(t *testing.T) { require.Equal(t, "NNuJqXDnRqvwgzhSzhH4jnVFWB1DyZ34EM", w.Accounts[0].Address) }) } + +// Testcase is the wallet of privnet validator. +func TestWalletConvert(t *testing.T) { + tmpDir := path.Join(os.TempDir(), "neogo.test.convert") + require.NoError(t, os.Mkdir(tmpDir, os.ModePerm)) + defer os.RemoveAll(tmpDir) + + e := newExecutor(t, false) + defer e.Close(t) + + outPath := path.Join(tmpDir, "wallet.json") + cmd := []string{"neo-go", "wallet", "convert"} + t.Run("missing wallet", func(t *testing.T) { + e.RunWithError(t, cmd...) + }) + + cmd = append(cmd, "--wallet", "testdata/wallets/testwallet_NEO2.json", "--out", outPath) + t.Run("invalid password", func(t *testing.T) { + // missing password + e.RunWithError(t, cmd...) + // invalid password + e.In.WriteString("two\r") + e.RunWithError(t, cmd...) + }) + + // 2 accounts. + e.In.WriteString("one\r") + e.In.WriteString("one\r") + e.Run(t, "neo-go", "wallet", "convert", + "--wallet", "testdata/wallets/testwallet_NEO2.json", + "--out", outPath) + + actual, err := wallet.NewWalletFromFile(outPath) + require.NoError(t, err) + expected, err := wallet.NewWalletFromFile("testdata/wallets/testwallet_NEO3.json") + require.NoError(t, err) + require.Equal(t, len(actual.Accounts), len(expected.Accounts)) + for _, exp := range expected.Accounts { + addr, err := address.StringToUint160(exp.Address) + require.NoError(t, err) + + act := actual.GetAccount(addr) + require.NotNil(t, act) + require.Equal(t, exp, act) + } +} From cd5219086a6707fab4995dc85d0a304fb05d9172 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Fri, 4 Dec 2020 12:40:22 +0300 Subject: [PATCH 9/9] wallet: export `NewAccountFromPrivateKey()` Don't perform back-and-forth conversion, don't handle error which never occur. --- cli/wallet/legacy.go | 5 +---- pkg/core/helper_test.go | 6 ++---- pkg/rpc/server/client_test.go | 18 ++++++------------ pkg/rpc/server/server_test.go | 3 +-- pkg/wallet/account.go | 10 +++++----- 5 files changed, 15 insertions(+), 27 deletions(-) diff --git a/cli/wallet/legacy.go b/cli/wallet/legacy.go index 163353513..3ddbd6e5f 100644 --- a/cli/wallet/legacy.go +++ b/cli/wallet/legacy.go @@ -58,10 +58,7 @@ func (a *accountV2) convert(pass string) (*wallet.Account, error) { } address.Prefix = address.NEO3Prefix - newAcc, err := wallet.NewAccountFromWIF(priv.WIF()) - if err != nil { - return nil, err - } + newAcc := wallet.NewAccountFromPrivateKey(priv) if a.Contract != nil { script, err := hex.DecodeString(a.Contract.Script) if err != nil { diff --git a/pkg/core/helper_test.go b/pkg/core/helper_test.go index 91dfcbeca..e63207516 100644 --- a/pkg/core/helper_test.go +++ b/pkg/core/helper_test.go @@ -195,8 +195,7 @@ func TestCreateBasicChain(t *testing.T) { priv0 := testchain.PrivateKeyByID(0) priv1 := testchain.PrivateKeyByID(1) priv0ScriptHash := priv0.GetScriptHash() - acc0, err := wallet.NewAccountFromWIF(priv0.WIF()) - require.NoError(t, err) + acc0 := wallet.NewAccountFromPrivateKey(priv0) // Prepare some transaction for future submission. txSendRaw := newNEP17Transfer(bc.contracts.NEO.Hash, priv0ScriptHash, priv1.GetScriptHash(), int64(util.Fixed8FromInt64(1000))) @@ -268,8 +267,7 @@ func initBasicChain(t *testing.T, bc *Blockchain) { b.Header().EncodeBinary(buf.BinWriter) t.Logf("header: %s", hex.EncodeToString(buf.Bytes())) - acc0, err := wallet.NewAccountFromWIF(priv0.WIF()) - require.NoError(t, err) + acc0 := wallet.NewAccountFromPrivateKey(priv0) // Push some contract into the chain. txDeploy, cHash := newDeployTx(t, priv0ScriptHash, prefix+"test_contract.go", "Rubl") diff --git a/pkg/rpc/server/client_test.go b/pkg/rpc/server/client_test.go index 31467f76e..086fc4a98 100644 --- a/pkg/rpc/server/client_test.go +++ b/pkg/rpc/server/client_test.go @@ -132,16 +132,14 @@ func TestAddNetworkFee(t *testing.T) { t.Run("Contract", func(t *testing.T) { tx := transaction.New(testchain.Network(), []byte{byte(opcode.PUSH1)}, 0) priv := testchain.PrivateKeyByID(0) - acc1, err := wallet.NewAccountFromWIF(priv.WIF()) - require.NoError(t, err) + acc1 := wallet.NewAccountFromPrivateKey(priv) acc1.Contract.Deployed = true acc1.Contract.Script, _ = hex.DecodeString(verifyContractAVM) h, _ := util.Uint160DecodeStringLE(verifyContractHash) tx.ValidUntilBlock = chain.BlockHeight() + 10 t.Run("Valid", func(t *testing.T) { - acc0, err := wallet.NewAccountFromWIF(priv.WIF()) - require.NoError(t, err) + acc0 := wallet.NewAccountFromPrivateKey(priv) tx.Signers = []transaction.Signer{ { Account: acc0.PrivateKey().GetScriptHash(), @@ -173,8 +171,7 @@ func TestAddNetworkFee(t *testing.T) { require.Error(t, c.AddNetworkFee(tx, 10, acc0, acc1)) }) t.Run("InvalidContract", func(t *testing.T) { - acc0, err := wallet.NewAccountFromWIF(priv.WIF()) - require.NoError(t, err) + acc0 := wallet.NewAccountFromPrivateKey(priv) tx.Signers = []transaction.Signer{ { Account: acc0.PrivateKey().GetScriptHash(), @@ -200,8 +197,7 @@ func TestSignAndPushInvocationTx(t *testing.T) { require.NoError(t, c.Init()) priv := testchain.PrivateKey(0) - acc, err := wallet.NewAccountFromWIF(priv.WIF()) - require.NoError(t, err) + acc := wallet.NewAccountFromPrivateKey(priv) h, err := c.SignAndPushInvocationTx([]byte{byte(opcode.PUSH1)}, acc, 30, 0, []transaction.Signer{{ Account: priv.GetScriptHash(), Scopes: transaction.CalledByEntry, @@ -239,8 +235,7 @@ func TestCreateTxFromScript(t *testing.T) { require.NoError(t, c.Init()) priv := testchain.PrivateKey(0) - acc, err := wallet.NewAccountFromWIF(priv.WIF()) - require.NoError(t, err) + acc := wallet.NewAccountFromPrivateKey(priv) t.Run("NoSystemFee", func(t *testing.T) { tx, err := c.CreateTxFromScript([]byte{byte(opcode.PUSH1)}, acc, -1, 10) require.NoError(t, err) @@ -269,8 +264,7 @@ func TestCreateNEP17TransferTx(t *testing.T) { require.NoError(t, c.Init()) priv := testchain.PrivateKeyByID(0) - acc, err := wallet.NewAccountFromWIF(priv.WIF()) - require.NoError(t, err) + acc := wallet.NewAccountFromPrivateKey(priv) gasContractHash, err := c.GetNativeContractHash("gas") require.NoError(t, err) diff --git a/pkg/rpc/server/server_test.go b/pkg/rpc/server/server_test.go index 42bd9a93c..8f9e2cc08 100644 --- a/pkg/rpc/server/server_test.go +++ b/pkg/rpc/server/server_test.go @@ -996,8 +996,7 @@ func testRPCProtocol(t *testing.T, doRPCCall func(string, string, *testing.T) [] }) priv0 := testchain.PrivateKeyByID(0) - acc0, err := wallet.NewAccountFromWIF(priv0.WIF()) - require.NoError(t, err) + acc0 := wallet.NewAccountFromPrivateKey(priv0) addNetworkFee := func(tx *transaction.Transaction) { size := io.GetVarSize(tx) diff --git a/pkg/wallet/account.go b/pkg/wallet/account.go index 7b871759b..0ecacfeb8 100644 --- a/pkg/wallet/account.go +++ b/pkg/wallet/account.go @@ -90,7 +90,7 @@ func NewAccount() (*Account, error) { if err != nil { return nil, err } - return newAccountFromPrivateKey(priv), nil + return NewAccountFromPrivateKey(priv), nil } // SignTx signs transaction t and updates it's Witnesses. @@ -172,7 +172,7 @@ func NewAccountFromWIF(wif string) (*Account, error) { if err != nil { return nil, err } - return newAccountFromPrivateKey(privKey), nil + return NewAccountFromPrivateKey(privKey), nil } // NewAccountFromEncryptedWIF creates a new Account from the given encrypted WIF. @@ -182,7 +182,7 @@ func NewAccountFromEncryptedWIF(wif string, pass string) (*Account, error) { return nil, err } - a := newAccountFromPrivateKey(priv) + a := NewAccountFromPrivateKey(priv) a.EncryptedWIF = wif return a, nil @@ -216,8 +216,8 @@ func (a *Account) ConvertMultisig(m int, pubs []*keys.PublicKey) error { return nil } -// newAccountFromPrivateKey creates a wallet from the given PrivateKey. -func newAccountFromPrivateKey(p *keys.PrivateKey) *Account { +// NewAccountFromPrivateKey creates a wallet from the given PrivateKey. +func NewAccountFromPrivateKey(p *keys.PrivateKey) *Account { pubKey := p.PublicKey() pubAddr := p.Address() wif := p.WIF()