From f83395e89761e6371150a06fbedc1e23c6884e36 Mon Sep 17 00:00:00 2001 From: Evgeniy Stratonikov Date: Mon, 2 Aug 2021 11:19:30 +0300 Subject: [PATCH 1/5] cli/test: move test wallet path to constant Signed-off-by: Evgeniy Stratonikov --- cli/executor_test.go | 3 +++ cli/wallet_test.go | 21 ++++++++------------- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/cli/executor_test.go b/cli/executor_test.go index 835ac77dd..d1e4b1402 100644 --- a/cli/executor_test.go +++ b/cli/executor_test.go @@ -34,6 +34,9 @@ const ( validatorAddr = "NfgHwwTi3wHAS8aFAN243C5vGbkYDpqLHP" multisigAddr = "NVTiAjNgagDkTr5HTzDmQP9kPwPHN5BgVq" + testWalletPath = "testdata/testwallet.json" + testWalletAccount = "Nfyz4KcsgYepRJw1W5C2uKCi6QWKf7v6gG" + validatorWallet = "testdata/wallet1_solo.json" ) diff --git a/cli/wallet_test.go b/cli/wallet_test.go index 098660d42..7c6d80791 100644 --- a/cli/wallet_test.go +++ b/cli/wallet_test.go @@ -228,24 +228,19 @@ func TestWalletExport(t *testing.T) { func TestClaimGas(t *testing.T) { e := newExecutor(t, true) - const walletPath = "testdata/testwallet.json" - w, err := wallet.NewWalletFromFile(walletPath) - require.NoError(t, err) - t.Cleanup(w.Close) - args := []string{ "neo-go", "wallet", "nep17", "multitransfer", "--rpc-endpoint", "http://" + e.RPC.Addr, "--wallet", validatorWallet, "--from", validatorAddr, - "NEO:" + w.Accounts[0].Address + ":1000", - "GAS:" + w.Accounts[0].Address + ":1000", // for tx send + "NEO:" + testWalletAccount + ":1000", + "GAS:" + testWalletAccount + ":1000", // for tx send } e.In.WriteString("one\r") e.Run(t, args...) e.checkTxPersisted(t) - h, err := address.StringToUint160(w.Accounts[0].Address) + h, err := address.StringToUint160(testWalletAccount) require.NoError(t, err) balanceBefore := e.Chain.GetUtilityTokenBalance(h) @@ -257,8 +252,8 @@ func TestClaimGas(t *testing.T) { e.In.WriteString("testpass\r") e.Run(t, "neo-go", "wallet", "claim", "--rpc-endpoint", "http://"+e.RPC.Addr, - "--wallet", walletPath, - "--address", w.Accounts[0].Address) + "--wallet", testWalletPath, + "--address", testWalletAccount) tx, height := e.checkTxPersisted(t) balanceBefore.Sub(balanceBefore, big.NewInt(tx.NetworkFee+tx.SystemFee)) balanceBefore.Add(balanceBefore, cl) @@ -337,13 +332,13 @@ func TestImportDeployed(t *testing.T) { func TestWalletDump(t *testing.T) { e := newExecutor(t, false) - cmd := []string{"neo-go", "wallet", "dump", "--wallet", "testdata/testwallet.json"} + cmd := []string{"neo-go", "wallet", "dump", "--wallet", testWalletPath} 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, "Nfyz4KcsgYepRJw1W5C2uKCi6QWKf7v6gG", w.Accounts[0].Address) + require.Equal(t, testWalletAccount, w.Accounts[0].Address) t.Run("with decrypt", func(t *testing.T) { cmd = append(cmd, "--decrypt") @@ -358,7 +353,7 @@ func TestWalletDump(t *testing.T) { w := new(wallet.Wallet) require.NoError(t, json.Unmarshal([]byte(rawStr), w)) require.Equal(t, 1, len(w.Accounts)) - require.Equal(t, "Nfyz4KcsgYepRJw1W5C2uKCi6QWKf7v6gG", w.Accounts[0].Address) + require.Equal(t, testWalletAccount, w.Accounts[0].Address) }) } From 6fe2ace43bf599721439dcb33adaecd2ec0fb6be Mon Sep 17 00:00:00 2001 From: Evgeniy Stratonikov Date: Mon, 2 Aug 2021 13:42:15 +0300 Subject: [PATCH 2/5] cli/smartcontract: refactor contract deploy a bit Provide cosigners explicitly during deploy and don't read wallet twice. This is needed because manifest validation requires valid sender address. Signed-off-by: Evgeniy Stratonikov --- cli/smartcontract/smart_contract.go | 35 +++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/cli/smartcontract/smart_contract.go b/cli/smartcontract/smart_contract.go index 48ec47ee1..c715856fc 100644 --- a/cli/smartcontract/smart_contract.go +++ b/cli/smartcontract/smart_contract.go @@ -560,27 +560,33 @@ func invokeInternal(ctx *cli.Context, signAndPush bool) error { return exitErr } - _, err = invokeWithArgs(ctx, signAndPush, script, operation, params, cosigners) + var ( + acc *wallet.Account + w *wallet.Wallet + ) + if signAndPush { + acc, w, err = getAccFromContext(ctx) + if err != nil { + return cli.NewExitError(err, 1) + } + } + + _, err = invokeWithArgs(ctx, acc, w, script, operation, params, cosigners) return err } -func invokeWithArgs(ctx *cli.Context, signAndPush bool, script util.Uint160, operation string, params []smartcontract.Parameter, cosigners []transaction.Signer) (util.Uint160, error) { +func invokeWithArgs(ctx *cli.Context, acc *wallet.Account, wall *wallet.Wallet, script util.Uint160, operation string, params []smartcontract.Parameter, cosigners []transaction.Signer) (util.Uint160, error) { var ( err error gas, sysgas fixedn.Fixed8 cosignersAccounts []client.SignerAccount resp *result.Invoke - acc *wallet.Account - wall *wallet.Wallet sender util.Uint160 + signAndPush = acc != nil ) if signAndPush { gas = flags.Fixed8FromContext(ctx, "gas") sysgas = flags.Fixed8FromContext(ctx, "sysgas") - acc, wall, err = getAccFromContext(ctx) - if err != nil { - return sender, err - } sender, err = address.StringToUint160(acc.Address) if err != nil { return sender, err @@ -826,7 +832,18 @@ func contractDeploy(ctx *cli.Context) error { if err != nil { return cli.NewExitError(fmt.Errorf("failed to get management contract's hash: %w", err), 1) } - sender, extErr := invokeWithArgs(ctx, true, mgmtHash, "deploy", appCallParams, nil) + + acc, w, err := getAccFromContext(ctx) + if err != nil { + return cli.NewExitError(fmt.Errorf("can't get sender address: %w", err), 1) + } + + cosigners := []transaction.Signer{{ + Account: acc.Contract.ScriptHash(), + Scopes: transaction.CalledByEntry, + }} + + sender, extErr := invokeWithArgs(ctx, acc, w, mgmtHash, "deploy", appCallParams, cosigners) if extErr != nil { return extErr } From 868198a36ecad98654dcbf68e26f4c525a995e55 Mon Sep 17 00:00:00 2001 From: Evgeniy Stratonikov Date: Mon, 2 Aug 2021 11:10:26 +0300 Subject: [PATCH 3/5] cli/smartcontract: add `manifest add-group` command Signed-off-by: Evgeniy Stratonikov --- cli/contract_test.go | 41 ++++++++++ cli/smartcontract/manifest.go | 122 ++++++++++++++++++++++++++++ cli/smartcontract/smart_contract.go | 48 ++++++++++- 3 files changed, 207 insertions(+), 4 deletions(-) create mode 100644 cli/smartcontract/manifest.go diff --git a/cli/contract_test.go b/cli/contract_test.go index 964d782d5..6e865a746 100644 --- a/cli/contract_test.go +++ b/cli/contract_test.go @@ -227,6 +227,47 @@ func TestContractDeployWithData(t *testing.T) { require.Equal(t, []byte("take_me_to_church"), res.Stack[0].Value()) } +func TestContractManifestGroups(t *testing.T) { + e := newExecutor(t, true) + + // For proper nef generation. + config.Version = "0.90.0-test" + + tmpDir, err := ioutil.TempDir("", "neogo.test.deployfail") + require.NoError(t, err) + t.Cleanup(func() { + os.RemoveAll(tmpDir) + }) + + w, err := wallet.NewWalletFromFile(testWalletPath) + require.NoError(t, err) + defer w.Close() + + nefName := path.Join(tmpDir, "deploy.nef") + manifestName := path.Join(tmpDir, "deploy.manifest.json") + e.Run(t, "neo-go", "contract", "compile", + "--in", "testdata/deploy/main.go", // compile single file + "--config", "testdata/deploy/neo-go.yml", + "--out", nefName, "--manifest", manifestName) + + cmd := []string{"neo-go", "contract", "manifest", "add-group", + "--nef", nefName, "--manifest", manifestName} + + e.In.WriteString("testpass\r") + e.Run(t, append(cmd, "--wallet", testWalletPath, + "--sender", testWalletAccount, "--account", testWalletAccount)...) + + e.In.WriteString("testpass\r") // should override signature with the previous sender + e.Run(t, append(cmd, "--wallet", testWalletPath, + "--sender", validatorAddr, "--account", testWalletAccount)...) + + e.In.WriteString("one\r") + e.Run(t, "neo-go", "contract", "deploy", + "--rpc-endpoint", "http://"+e.RPC.Addr, + "--in", nefName, "--manifest", manifestName, + "--wallet", validatorWallet, "--address", validatorAddr) +} + func deployVerifyContract(t *testing.T, e *executor) util.Uint160 { return deployContract(t, e, "testdata/verify.go", "testdata/verify.yml", validatorWallet, validatorAddr, "one") } diff --git a/cli/smartcontract/manifest.go b/cli/smartcontract/manifest.go new file mode 100644 index 000000000..e7f5d4ee7 --- /dev/null +++ b/cli/smartcontract/manifest.go @@ -0,0 +1,122 @@ +package smartcontract + +import ( + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "os" + + "github.com/nspcc-dev/neo-go/cli/flags" + "github.com/nspcc-dev/neo-go/pkg/core/state" + "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" + "github.com/nspcc-dev/neo-go/pkg/smartcontract/nef" + "github.com/nspcc-dev/neo-go/pkg/wallet" + "github.com/urfave/cli" +) + +func manifestAddGroup(ctx *cli.Context) error { + walletPath := ctx.String("wallet") + if len(walletPath) == 0 { + return cli.NewExitError(errNoWallet, 1) + } + + w, err := wallet.NewWalletFromFile(walletPath) + if err != nil { + return cli.NewExitError(err, 1) + } + defer w.Close() + + addr, err := flags.ParseAddress(ctx.String("account")) + if err != nil { + return cli.NewExitError(errors.New("address is missing"), 1) + } + + sender, err := flags.ParseAddress(ctx.String("sender")) + if err != nil { + return cli.NewExitError("invalid sender", 1) + } + + nf, _, err := readNEFFile(ctx.String("nef")) + if err != nil { + return cli.NewExitError(fmt.Errorf("can't read NEF file: %w", err), 1) + } + + mPath := ctx.String("manifest") + m, _, err := readManifest(mPath) + if err != nil { + return cli.NewExitError(fmt.Errorf("can't read contract manifest: %w", err), 1) + } + + h := state.CreateContractHash(sender, nf.Checksum, m.Name) + + gAcc, err := getUnlockedAccount(w, addr) + if err != nil { + return err + } + + var found bool + + sig := gAcc.PrivateKey().Sign(h.BytesBE()) + pub := gAcc.PrivateKey().PublicKey() + for i := range m.Groups { + if m.Groups[i].PublicKey.Equal(pub) { + m.Groups[i].Signature = sig + found = true + break + } + } + if !found { + m.Groups = append(m.Groups, manifest.Group{ + PublicKey: pub, + Signature: sig, + }) + } + + rawM, err := json.Marshal(m) + if err != nil { + return cli.NewExitError(fmt.Errorf("can't marshal manifest: %w", err), 1) + } + + err = ioutil.WriteFile(mPath, rawM, os.ModePerm) + if err != nil { + return cli.NewExitError(fmt.Errorf("can't write manifest file: %w", err), 1) + } + return nil +} + +func readNEFFile(filename string) (*nef.File, []byte, error) { + if len(filename) == 0 { + return nil, nil, errors.New("no nef file was provided") + } + + f, err := ioutil.ReadFile(filename) + if err != nil { + return nil, nil, err + } + + nefFile, err := nef.FileFromBytes(f) + if err != nil { + return nil, nil, fmt.Errorf("can't parse NEF file: %w", err) + } + + return &nefFile, f, nil +} + +func readManifest(filename string) (*manifest.Manifest, []byte, error) { + if len(filename) == 0 { + return nil, nil, errNoManifestFile + } + + manifestBytes, err := ioutil.ReadFile(filename) + if err != nil { + return nil, nil, err + } + + m := new(manifest.Manifest) + err = json.Unmarshal(manifestBytes, m) + if err != nil { + return nil, nil, err + } + return m, manifestBytes, nil +} diff --git a/cli/smartcontract/smart_contract.go b/cli/smartcontract/smart_contract.go index c715856fc..0c9cd6770 100644 --- a/cli/smartcontract/smart_contract.go +++ b/cli/smartcontract/smart_contract.go @@ -370,6 +370,36 @@ func NewCommands() []cli.Command { }, }, }, + { + Name: "manifest", + Usage: "manifest-related commands", + Subcommands: []cli.Command{ + { + Name: "add-group", + Usage: "adds group to the manifest", + Action: manifestAddGroup, + Flags: []cli.Flag{ + walletFlag, + cli.StringFlag{ + Name: "sender, s", + Usage: "deploy transaction sender", + }, + cli.StringFlag{ + Name: "account, a", + Usage: "account to sign group with", + }, + cli.StringFlag{ + Name: "nef, n", + Usage: "path to the NEF file", + }, + cli.StringFlag{ + Name: "manifest, m", + Usage: "path to the manifest", + }, + }, + }, + }, + }, }, }} } @@ -750,22 +780,32 @@ func getAccFromContext(ctx *cli.Context) (*wallet.Account, *wallet.Wallet, error } else { addr = wall.GetChangeAddress() } + + acc, err := getUnlockedAccount(wall, addr) + return acc, wall, err +} + +func getUnlockedAccount(wall *wallet.Wallet, addr util.Uint160) (*wallet.Account, error) { acc := wall.GetAccount(addr) if acc == nil { - return nil, nil, cli.NewExitError(fmt.Errorf("wallet contains no account for '%s'", address.Uint160ToString(addr)), 1) + return nil, cli.NewExitError(fmt.Errorf("wallet contains no account for '%s'", address.Uint160ToString(addr)), 1) + } + + if acc.PrivateKey() != nil { + return acc, nil } rawPass, err := input.ReadPassword( fmt.Sprintf("Enter account %s password > ", address.Uint160ToString(addr))) if err != nil { - return nil, nil, cli.NewExitError(err, 1) + return nil, cli.NewExitError(err, 1) } pass := strings.TrimRight(string(rawPass), "\n") err = acc.Decrypt(pass, wall.Scrypt) if err != nil { - return nil, nil, cli.NewExitError(err, 1) + return nil, cli.NewExitError(err, 1) } - return acc, wall, nil + return acc, nil } // contractDeploy deploys contract. From adcae12331893cb853a46848d2df9ef51f9ac9a0 Mon Sep 17 00:00:00 2001 From: Evgeniy Stratonikov Date: Mon, 2 Aug 2021 13:47:19 +0300 Subject: [PATCH 4/5] cli/smartcontract: reuse read functions in `contractDeploy` Signed-off-by: Evgeniy Stratonikov --- cli/smartcontract/smart_contract.go | 23 ++--------------------- 1 file changed, 2 insertions(+), 21 deletions(-) diff --git a/cli/smartcontract/smart_contract.go b/cli/smartcontract/smart_contract.go index 0c9cd6770..dc7ac6512 100644 --- a/cli/smartcontract/smart_contract.go +++ b/cli/smartcontract/smart_contract.go @@ -810,34 +810,15 @@ func getUnlockedAccount(wall *wallet.Wallet, addr util.Uint160) (*wallet.Account // contractDeploy deploys contract. func contractDeploy(ctx *cli.Context) error { - in := ctx.String("in") - if len(in) == 0 { - return cli.NewExitError(errNoInput, 1) - } - manifestFile := ctx.String("manifest") - if len(manifestFile) == 0 { - return cli.NewExitError(errNoManifestFile, 1) - } - - f, err := ioutil.ReadFile(in) + nefFile, f, err := readNEFFile(ctx.String("in")) if err != nil { return cli.NewExitError(err, 1) } - // Check the file. - nefFile, err := nef.FileFromBytes(f) - if err != nil { - return cli.NewExitError(fmt.Errorf("failed to read .nef file: %w", err), 1) - } - manifestBytes, err := ioutil.ReadFile(manifestFile) + m, manifestBytes, err := readManifest(ctx.String("manifest")) if err != nil { return cli.NewExitError(fmt.Errorf("failed to read manifest file: %w", err), 1) } - m := &manifest.Manifest{} - err = json.Unmarshal(manifestBytes, m) - if err != nil { - return cli.NewExitError(fmt.Errorf("failed to restore manifest file: %w", err), 1) - } appCallParams := []smartcontract.Parameter{ { From 653ecbe50edd0ffef6f0062cf9d7621090bfd60d Mon Sep 17 00:00:00 2001 From: Evgeniy Stratonikov Date: Thu, 5 Aug 2021 11:21:52 +0300 Subject: [PATCH 5/5] docs/compiler.md: document manifest groups Signed-off-by: Evgeniy Stratonikov --- docs/compiler.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/compiler.md b/docs/compiler.md index 2ba82e7ce..5a10d927e 100644 --- a/docs/compiler.md +++ b/docs/compiler.md @@ -276,6 +276,16 @@ Using either constant or literal for contract hash and method will allow compile to perform more extensive analysis. This check can be disabled with `--no-permissions` flag. +#### Manifest file +Any contract can be included in a group identified by a public key which is used in [permissions](#Permissions). +This is achieved with `manifest add-group` command. +``` +./bin/neo-go contract manifest add-group -n contract.nef -m contract.manifest.json --sender --wallet /path/to/wallet.json --account +``` +It accepts contract `.nef` and manifest files emitted by `compile` command as well as +sender and signer accounts. `--sender` is the account who will send deploy transaction later (not necessarily in wallet). +`--account` is the wallet account which signs contract hash using group private key. + #### Neo Express support It's possible to deploy contracts written in Go using [Neo