Merge pull request #1797 from nspcc-dev/rpc/fix-createtxfromscript

rpc, cli: allow to provide cosigners accounts to (*Client).CreateTxFromScript
This commit is contained in:
Roman Khimov 2021-03-04 11:28:09 +03:00 committed by GitHub
commit 0e7bb5cc77
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 197 additions and 60 deletions

View file

@ -14,6 +14,7 @@ import (
"github.com/nspcc-dev/neo-go/pkg/config"
"github.com/nspcc-dev/neo-go/pkg/core/interop/storage"
"github.com/nspcc-dev/neo-go/pkg/core/state"
"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/encoding/fixedn"
"github.com/nspcc-dev/neo-go/pkg/rpc/response/result"
@ -22,6 +23,7 @@ import (
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
"github.com/nspcc-dev/neo-go/pkg/wallet"
"github.com/stretchr/testify/require"
)
@ -275,6 +277,78 @@ func TestComlileAndInvokeFunction(t *testing.T) {
})
})
t.Run("real invoke and save tx", func(t *testing.T) {
txout := path.Join(tmpDir, "test_contract_tx.json")
nefName = path.Join(tmpDir, "verify.nef")
manifestName = path.Join(tmpDir, "verify.manifest.json")
e.Run(t, "neo-go", "contract", "compile",
"--in", "testdata/verify.go",
"--config", "testdata/verify.yml",
"--out", nefName, "--manifest", manifestName)
e.In.WriteString("one\r")
e.Run(t, "neo-go", "contract", "deploy",
"--rpc-endpoint", "http://"+e.RPC.Addr,
"--wallet", validatorWallet, "--address", validatorAddr,
"--in", nefName, "--manifest", manifestName)
line, err := e.Out.ReadString('\n')
require.NoError(t, err)
line = strings.TrimSpace(strings.TrimPrefix(line, "Contract: "))
hVerify, err := util.Uint160DecodeStringLE(line)
require.NoError(t, err)
e.checkTxPersisted(t)
cmd = []string{"neo-go", "contract", "invokefunction",
"--rpc-endpoint", "http://" + e.RPC.Addr,
"--out", txout,
"--wallet", validatorWallet, "--address", validatorAddr,
}
t.Run("without cosigner", func(t *testing.T) {
e.In.WriteString("one\r")
e.Run(t, append(cmd, hVerify.StringLE(), "verify")...)
})
t.Run("with cosigner", func(t *testing.T) {
t.Run("cosigner is sender", func(t *testing.T) {
e.In.WriteString("one\r")
e.Run(t, append(cmd, hVerify.StringLE(), "verify", "--", validatorAddr+":Global")...)
})
acc, err := wallet.NewAccount()
require.NoError(t, err)
pk, err := keys.NewPrivateKey()
require.NoError(t, err)
acc.ConvertMultisig(2, keys.PublicKeys{acc.PrivateKey().PublicKey(), pk.PublicKey()})
t.Run("cosigner is multisig account", func(t *testing.T) {
t.Run("missing in the wallet", func(t *testing.T) {
e.In.WriteString("one\r")
e.RunWithError(t, append(cmd, hVerify.StringLE(), "verify", "--", acc.Address)...)
})
t.Run("good", func(t *testing.T) {
e.In.WriteString("one\r")
e.Run(t, append(cmd, hVerify.StringLE(), "verify", "--", multisigAddr)...)
})
})
t.Run("cosigner is deployed contract", func(t *testing.T) {
t.Run("missing in the wallet", func(t *testing.T) {
e.In.WriteString("one\r")
e.RunWithError(t, append(cmd, hVerify.StringLE(), "verify", "--", h.StringLE())...)
})
t.Run("good", func(t *testing.T) {
e.In.WriteString("one\r")
e.Run(t, append(cmd, hVerify.StringLE(), "verify", "--", hVerify.StringLE())...)
})
})
})
})
t.Run("test Storage.Find", func(t *testing.T) {
cmd := []string{"neo-go", "contract", "testinvokefunction",
"--rpc-endpoint", "http://" + e.RPC.Addr,

View file

@ -32,6 +32,7 @@ import (
const (
validatorWIF = "KxyjQ8eUa4FHt3Gvioyt1Wz29cTUrE4eTqX3yFSk1YFCsPL8uNsY"
validatorAddr = "NVNvVRW5Q5naSx2k2iZm7xRgtRNGuZppAK"
multisigAddr = "NUVPACMnKFhpuHjsRjhUvXz1XhqfGZYVtY"
validatorWallet = "testdata/wallet1_solo.json"
)

View file

@ -83,6 +83,11 @@ func TestNEP17Balance(t *testing.T) {
e.checkNextLine(t, "^\\s*Updated\\s*:\\s*"+strconv.FormatUint(uint64(index), 10))
}
}
e.checkNextLine(t, "^\\s*$")
addr4, err := address.StringToUint160("NbxpLNCCSWZ9BkYpCYT8NfN1uoxq9Rfbrn") // deployed verify.go contract
require.NoError(t, err)
e.checkNextLine(t, "^Account "+address.Uint160ToString(addr4))
e.checkEOF(t)
})
t.Run("Bad token", func(t *testing.T) {

View file

@ -22,6 +22,7 @@ import (
"github.com/nspcc-dev/neo-go/pkg/encoding/address"
"github.com/nspcc-dev/neo-go/pkg/encoding/fixedn"
"github.com/nspcc-dev/neo-go/pkg/io"
"github.com/nspcc-dev/neo-go/pkg/rpc/client"
"github.com/nspcc-dev/neo-go/pkg/rpc/response/result"
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
@ -509,15 +510,17 @@ func invokeFunction(ctx *cli.Context) error {
func invokeInternal(ctx *cli.Context, signAndPush bool) error {
var (
err error
gas fixedn.Fixed8
operation string
params = make([]smartcontract.Parameter, 0)
paramsStart = 1
cosigners []transaction.Signer
cosignersOffset = 0
resp *result.Invoke
acc *wallet.Account
err error
gas fixedn.Fixed8
operation string
params = make([]smartcontract.Parameter, 0)
paramsStart = 1
cosigners []transaction.Signer
cosignersAccounts []client.SignerAccount
cosignersOffset = 0
resp *result.Invoke
acc *wallet.Account
wall *wallet.Wallet
)
args := ctx.Args()
@ -554,10 +557,20 @@ func invokeInternal(ctx *cli.Context, signAndPush bool) error {
if signAndPush {
gas = flags.Fixed8FromContext(ctx, "gas")
acc, err = getAccFromContext(ctx)
acc, wall, err = getAccFromContext(ctx)
if err != nil {
return err
}
for i := range cosigners {
cosignerAcc := wall.GetAccount(cosigners[i].Account)
if cosignerAcc == nil {
return cli.NewExitError(fmt.Errorf("can't calculate network fee: no account was found in the wallet for cosigner #%d", i), 1)
}
cosignersAccounts = append(cosignersAccounts, client.SignerAccount{
Signer: cosigners[i],
Account: cosignerAcc,
})
}
}
gctx, cancel := options.GetTimeoutContext(ctx)
defer cancel()
@ -579,7 +592,7 @@ func invokeInternal(ctx *cli.Context, signAndPush bool) error {
fmt.Fprintln(ctx.App.Writer, errText+". Sending transaction...")
}
if out := ctx.String("out"); out != "" {
tx, err := c.CreateTxFromScript(resp.Script, acc, resp.GasConsumed, int64(gas), cosigners...)
tx, err := c.CreateTxFromScript(resp.Script, acc, resp.GasConsumed, int64(gas), cosignersAccounts)
if err != nil {
return cli.NewExitError(fmt.Errorf("failed to create tx: %w", err), 1)
}
@ -593,7 +606,7 @@ func invokeInternal(ctx *cli.Context, signAndPush bool) error {
if len(resp.Script) == 0 {
return cli.NewExitError(errors.New("no script returned from the RPC node"), 1)
}
txHash, err := c.SignAndPushInvocationTx(resp.Script, acc, resp.GasConsumed, gas, cosigners)
txHash, err := c.SignAndPushInvocationTx(resp.Script, acc, resp.GasConsumed, gas, cosignersAccounts)
if err != nil {
return cli.NewExitError(fmt.Errorf("failed to push invocation tx: %w", err), 1)
}
@ -747,17 +760,17 @@ func inspect(ctx *cli.Context) error {
return nil
}
func getAccFromContext(ctx *cli.Context) (*wallet.Account, error) {
func getAccFromContext(ctx *cli.Context) (*wallet.Account, *wallet.Wallet, error) {
var addr util.Uint160
wPath := ctx.String("wallet")
if len(wPath) == 0 {
return nil, cli.NewExitError(errNoWallet, 1)
return nil, nil, cli.NewExitError(errNoWallet, 1)
}
wall, err := wallet.NewWalletFromFile(wPath)
if err != nil {
return nil, cli.NewExitError(err, 1)
return nil, nil, cli.NewExitError(err, 1)
}
addrFlag := ctx.Generic("address").(*flags.Address)
if addrFlag.IsSet {
@ -767,20 +780,20 @@ func getAccFromContext(ctx *cli.Context) (*wallet.Account, error) {
}
acc := wall.GetAccount(addr)
if acc == nil {
return nil, cli.NewExitError(fmt.Errorf("wallet contains no account for '%s'", address.Uint160ToString(addr)), 1)
return nil, nil, cli.NewExitError(fmt.Errorf("wallet contains no account for '%s'", address.Uint160ToString(addr)), 1)
}
rawPass, err := input.ReadPassword(
fmt.Sprintf("Enter account %s password > ", address.Uint160ToString(addr)))
if err != nil {
return nil, cli.NewExitError(err, 1)
return nil, nil, cli.NewExitError(err, 1)
}
pass := strings.TrimRight(string(rawPass), "\n")
err = acc.Decrypt(pass)
if err != nil {
return nil, cli.NewExitError(err, 1)
return nil, nil, cli.NewExitError(err, 1)
}
return acc, nil
return acc, wall, nil
}
// contractDeploy deploys contract.
@ -795,7 +808,7 @@ func contractDeploy(ctx *cli.Context) error {
}
gas := flags.Fixed8FromContext(ctx, "gas")
acc, err := getAccFromContext(ctx)
acc, _, err := getAccFromContext(ctx)
if err != nil {
return err
}

View file

@ -1,8 +1,10 @@
package testdata
import "github.com/nspcc-dev/neo-go/pkg/interop"
func Verify() bool {
return true
}
func OnNEP17Payment(from []byte, amount int, data interface{}) {
func OnNEP17Payment(from interop.Hash160, amount int, data interface{}) {
}

1
cli/testdata/verify.yml vendored Normal file
View file

@ -0,0 +1 @@
name: Test verify

View file

@ -59,6 +59,18 @@
},
"lock": false,
"isdefault": false
},
{
"address" : "NbxpLNCCSWZ9BkYpCYT8NfN1uoxq9Rfbrn",
"key" : "6PYXDze5Ak4HahYKygcNzk6n65ACjWdDCYLSuKgA5KG8vyMJSFboUNSiPD",
"label" : "",
"contract" : {
"script" : "VwEAEUBXAANA",
"deployed" : true,
"parameters" : []
},
"lock" : false,
"isdefault" : false
}
],
"scrypt": {

View file

@ -11,6 +11,7 @@ import (
"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/io"
"github.com/nspcc-dev/neo-go/pkg/rpc/client"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm/emit"
@ -108,9 +109,13 @@ func handleCandidate(ctx *cli.Context, method string, sysGas int64) error {
w := io.NewBufBinWriter()
emit.AppCall(w.BinWriter, neoContractHash, method, callflag.States, acc.PrivateKey().PublicKey().Bytes())
emit.Opcodes(w.BinWriter, opcode.ASSERT)
tx, err := c.CreateTxFromScript(w.Bytes(), acc, sysGas, int64(gas), transaction.Signer{
Account: acc.Contract.ScriptHash(),
Scopes: transaction.CalledByEntry,
tx, err := c.CreateTxFromScript(w.Bytes(), acc, sysGas, int64(gas), []client.SignerAccount{{
Signer: transaction.Signer{
Account: acc.Contract.ScriptHash(),
Scopes: transaction.CalledByEntry,
},
Account: acc,
},
})
if err != nil {
return cli.NewExitError(err, 1)
@ -171,10 +176,13 @@ func handleVote(ctx *cli.Context) error {
emit.AppCall(w.BinWriter, neoContractHash, "vote", callflag.States, addr.BytesBE(), pubArg)
emit.Opcodes(w.BinWriter, opcode.ASSERT)
tx, err := c.CreateTxFromScript(w.Bytes(), acc, -1, int64(gas), transaction.Signer{
Account: acc.Contract.ScriptHash(),
Scopes: transaction.CalledByEntry,
})
tx, err := c.CreateTxFromScript(w.Bytes(), acc, -1, int64(gas), []client.SignerAccount{{
Signer: transaction.Signer{
Account: acc.Contract.ScriptHash(),
Scopes: transaction.CalledByEntry,
},
Account: acc,
}})
if err != nil {
return cli.NewExitError(err, 1)
}

View file

@ -24,6 +24,13 @@ type TransferTarget struct {
Amount int64
}
// SignerAccount represents combination of the transaction.Signer and the
// corresponding wallet.Account.
type SignerAccount struct {
Signer transaction.Signer
Account *wallet.Account
}
// NEP17Decimals invokes `decimals` NEP17 method on a specified contract.
func (c *Client) NEP17Decimals(tokenHash util.Uint160) (int64, error) {
result, err := c.InvokeFunction(tokenHash, "decimals", []smartcontract.Parameter{}, nil)
@ -140,23 +147,24 @@ func (c *Client) CreateNEP17MultiTransferTx(acc *wallet.Account, gas int64, reci
if err != nil {
return nil, fmt.Errorf("bad account address: %v", err)
}
return c.CreateTxFromScript(w.Bytes(), acc, -1, gas, transaction.Signer{
Account: accAddr,
Scopes: transaction.CalledByEntry,
})
return c.CreateTxFromScript(w.Bytes(), acc, -1, gas, []SignerAccount{{
Signer: transaction.Signer{
Account: accAddr,
Scopes: transaction.CalledByEntry,
},
Account: acc,
}})
}
// CreateTxFromScript creates transaction and properly sets cosigners and NetworkFee.
// If sysFee <= 0, it is determined via result of `invokescript` RPC. You should
// initialize network magic with Init before calling CreateTxFromScript.
func (c *Client) CreateTxFromScript(script []byte, acc *wallet.Account, sysFee, netFee int64,
cosigners ...transaction.Signer) (*transaction.Transaction, error) {
from, err := address.StringToUint160(acc.Address)
cosigners []SignerAccount) (*transaction.Transaction, error) {
signers, accounts, err := getSigners(acc, cosigners)
if err != nil {
return nil, fmt.Errorf("bad account address: %v", err)
return nil, fmt.Errorf("failed to construct tx signers: %w", err)
}
signers := getSigners(from, cosigners)
if sysFee < 0 {
result, err := c.InvokeScript(script, signers)
if err != nil {
@ -179,7 +187,7 @@ func (c *Client) CreateTxFromScript(script []byte, acc *wallet.Account, sysFee,
return nil, fmt.Errorf("failed to add validUntilBlock to transaction: %w", err)
}
err = c.AddNetworkFee(tx, netFee, acc)
err = c.AddNetworkFee(tx, netFee, accounts...)
if err != nil {
return nil, fmt.Errorf("failed to add network fee: %w", err)
}

View file

@ -522,11 +522,11 @@ func (c *Client) SubmitRawOracleResponse(ps request.RawParams) error {
// SignAndPushInvocationTx signs and pushes given script as an invocation
// transaction using given wif to sign it and spending the amount of gas
// specified. It returns a hash of the invocation transaction and an error.
func (c *Client) SignAndPushInvocationTx(script []byte, acc *wallet.Account, sysfee int64, netfee fixedn.Fixed8, cosigners []transaction.Signer) (util.Uint256, error) {
func (c *Client) SignAndPushInvocationTx(script []byte, acc *wallet.Account, sysfee int64, netfee fixedn.Fixed8, cosigners []SignerAccount) (util.Uint256, error) {
var txHash util.Uint256
var err error
tx, err := c.CreateTxFromScript(script, acc, sysfee, int64(netfee), cosigners...)
tx, err := c.CreateTxFromScript(script, acc, sysfee, int64(netfee), cosigners)
if err != nil {
return txHash, fmt.Errorf("failed to create tx: %w", err)
}
@ -544,25 +544,33 @@ func (c *Client) SignAndPushInvocationTx(script []byte, acc *wallet.Account, sys
return txHash, nil
}
// getSigners returns an array of transaction signers from given sender and cosigners.
// If cosigners list already contains sender, the sender will be placed at the start of
// the list.
func getSigners(sender util.Uint160, cosigners []transaction.Signer) []transaction.Signer {
// getSigners returns an array of transaction signers and corresponding accounts from
// given sender and cosigners. If cosigners list already contains sender, the sender
// will be placed at the start of the list.
func getSigners(sender *wallet.Account, cosigners []SignerAccount) ([]transaction.Signer, []*wallet.Account, error) {
var (
signers []transaction.Signer
accounts []*wallet.Account
)
from, err := address.StringToUint160(sender.Address)
if err != nil {
return nil, nil, fmt.Errorf("bad sender account address: %v", err)
}
s := transaction.Signer{
Account: sender,
Account: from,
Scopes: transaction.None,
}
for i, c := range cosigners {
if c.Account == sender {
if i == 0 {
return cosigners
}
s.Scopes = c.Scopes
cosigners = append(cosigners[:i], cosigners[i+1:]...)
break
for _, c := range cosigners {
if c.Signer.Account == from {
s.Scopes = c.Signer.Scopes
continue
}
signers = append(signers, c.Signer)
accounts = append(accounts, c.Account)
}
return append([]transaction.Signer{s}, cosigners...)
signers = append([]transaction.Signer{s}, signers...)
accounts = append([]*wallet.Account{sender}, accounts...)
return signers, accounts, nil
}
// SignAndPushP2PNotaryRequest creates and pushes P2PNotary request constructed from the main

View file

@ -203,10 +203,15 @@ func TestSignAndPushInvocationTx(t *testing.T) {
priv := testchain.PrivateKey(0)
acc := wallet.NewAccountFromPrivateKey(priv)
h, err := c.SignAndPushInvocationTx([]byte{byte(opcode.PUSH1)}, acc, 30, 0, []transaction.Signer{{
Account: priv.GetScriptHash(),
Scopes: transaction.CalledByEntry,
}})
h, err := c.SignAndPushInvocationTx([]byte{byte(opcode.PUSH1)}, acc, 30, 0, []client.SignerAccount{
{
Signer: transaction.Signer{
Account: priv.GetScriptHash(),
Scopes: transaction.CalledByEntry,
},
Account: acc,
},
})
require.NoError(t, err)
mp := chain.GetMemPool()
@ -347,7 +352,7 @@ func TestCreateTxFromScript(t *testing.T) {
priv := testchain.PrivateKey(0)
acc := wallet.NewAccountFromPrivateKey(priv)
t.Run("NoSystemFee", func(t *testing.T) {
tx, err := c.CreateTxFromScript([]byte{byte(opcode.PUSH1)}, acc, -1, 10)
tx, err := c.CreateTxFromScript([]byte{byte(opcode.PUSH1)}, acc, -1, 10, nil)
require.NoError(t, err)
require.True(t, tx.ValidUntilBlock > chain.BlockHeight())
require.EqualValues(t, 30, tx.SystemFee) // PUSH1
@ -355,7 +360,7 @@ func TestCreateTxFromScript(t *testing.T) {
require.Equal(t, acc.PrivateKey().GetScriptHash(), tx.Signers[0].Account)
})
t.Run("ProvideSystemFee", func(t *testing.T) {
tx, err := c.CreateTxFromScript([]byte{byte(opcode.PUSH1)}, acc, 123, 10)
tx, err := c.CreateTxFromScript([]byte{byte(opcode.PUSH1)}, acc, 123, 10, nil)
require.NoError(t, err)
require.True(t, tx.ValidUntilBlock > chain.BlockHeight())
require.EqualValues(t, 123, tx.SystemFee)