rpc: provide cosigners accounts to CreateTxFromScript

We need to define network fee for each of cosigners, and the only way to
do it is to access the cosigner's script.
This commit is contained in:
Anna Shaleva 2021-03-02 15:43:09 +03:00
parent b1b9a8cf66
commit 6c0faa4ea3
11 changed files with 193 additions and 56 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"
@ -515,9 +516,11 @@ func invokeInternal(ctx *cli.Context, signAndPush bool) error {
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,10 +109,14 @@ 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{{
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)
} else if err = acc.SignTx(tx); err != nil {
@ -171,9 +176,12 @@ 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{{
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,9 +147,12 @@ 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{{
return c.CreateTxFromScript(w.Bytes(), acc, -1, gas, []SignerAccount{{
Signer: transaction.Signer{
Account: accAddr,
Scopes: transaction.CalledByEntry,
},
Account: acc,
}})
}
@ -150,13 +160,11 @@ func (c *Client) CreateNEP17MultiTransferTx(acc *wallet.Account, gas int64, reci
// 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,7 +522,7 @@ 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
@ -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
for _, c := range cosigners {
if c.Signer.Account == from {
s.Scopes = c.Signer.Scopes
continue
}
s.Scopes = c.Scopes
cosigners = append(cosigners[:i], cosigners[i+1:]...)
break
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{{
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()