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/config"
"github.com/nspcc-dev/neo-go/pkg/core/interop/storage" "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/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/address"
"github.com/nspcc-dev/neo-go/pkg/encoding/fixedn" "github.com/nspcc-dev/neo-go/pkg/encoding/fixedn"
"github.com/nspcc-dev/neo-go/pkg/rpc/response/result" "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/util"
"github.com/nspcc-dev/neo-go/pkg/vm" "github.com/nspcc-dev/neo-go/pkg/vm"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem" "github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
"github.com/nspcc-dev/neo-go/pkg/wallet"
"github.com/stretchr/testify/require" "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) { t.Run("test Storage.Find", func(t *testing.T) {
cmd := []string{"neo-go", "contract", "testinvokefunction", cmd := []string{"neo-go", "contract", "testinvokefunction",
"--rpc-endpoint", "http://" + e.RPC.Addr, "--rpc-endpoint", "http://" + e.RPC.Addr,

View file

@ -32,6 +32,7 @@ import (
const ( const (
validatorWIF = "KxyjQ8eUa4FHt3Gvioyt1Wz29cTUrE4eTqX3yFSk1YFCsPL8uNsY" validatorWIF = "KxyjQ8eUa4FHt3Gvioyt1Wz29cTUrE4eTqX3yFSk1YFCsPL8uNsY"
validatorAddr = "NVNvVRW5Q5naSx2k2iZm7xRgtRNGuZppAK" validatorAddr = "NVNvVRW5Q5naSx2k2iZm7xRgtRNGuZppAK"
multisigAddr = "NUVPACMnKFhpuHjsRjhUvXz1XhqfGZYVtY"
validatorWallet = "testdata/wallet1_solo.json" 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*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) e.checkEOF(t)
}) })
t.Run("Bad token", func(t *testing.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/address"
"github.com/nspcc-dev/neo-go/pkg/encoding/fixedn" "github.com/nspcc-dev/neo-go/pkg/encoding/fixedn"
"github.com/nspcc-dev/neo-go/pkg/io" "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/rpc/response/result"
"github.com/nspcc-dev/neo-go/pkg/smartcontract" "github.com/nspcc-dev/neo-go/pkg/smartcontract"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag" "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) params = make([]smartcontract.Parameter, 0)
paramsStart = 1 paramsStart = 1
cosigners []transaction.Signer cosigners []transaction.Signer
cosignersAccounts []client.SignerAccount
cosignersOffset = 0 cosignersOffset = 0
resp *result.Invoke resp *result.Invoke
acc *wallet.Account acc *wallet.Account
wall *wallet.Wallet
) )
args := ctx.Args() args := ctx.Args()
@ -554,10 +557,20 @@ func invokeInternal(ctx *cli.Context, signAndPush bool) error {
if signAndPush { if signAndPush {
gas = flags.Fixed8FromContext(ctx, "gas") gas = flags.Fixed8FromContext(ctx, "gas")
acc, err = getAccFromContext(ctx) acc, wall, err = getAccFromContext(ctx)
if err != nil { if err != nil {
return err 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) gctx, cancel := options.GetTimeoutContext(ctx)
defer cancel() defer cancel()
@ -579,7 +592,7 @@ func invokeInternal(ctx *cli.Context, signAndPush bool) error {
fmt.Fprintln(ctx.App.Writer, errText+". Sending transaction...") fmt.Fprintln(ctx.App.Writer, errText+". Sending transaction...")
} }
if out := ctx.String("out"); out != "" { 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 { if err != nil {
return cli.NewExitError(fmt.Errorf("failed to create tx: %w", err), 1) 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 { if len(resp.Script) == 0 {
return cli.NewExitError(errors.New("no script returned from the RPC node"), 1) 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 { if err != nil {
return cli.NewExitError(fmt.Errorf("failed to push invocation tx: %w", err), 1) 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 return nil
} }
func getAccFromContext(ctx *cli.Context) (*wallet.Account, error) { func getAccFromContext(ctx *cli.Context) (*wallet.Account, *wallet.Wallet, error) {
var addr util.Uint160 var addr util.Uint160
wPath := ctx.String("wallet") wPath := ctx.String("wallet")
if len(wPath) == 0 { if len(wPath) == 0 {
return nil, cli.NewExitError(errNoWallet, 1) return nil, nil, cli.NewExitError(errNoWallet, 1)
} }
wall, err := wallet.NewWalletFromFile(wPath) wall, err := wallet.NewWalletFromFile(wPath)
if err != nil { if err != nil {
return nil, cli.NewExitError(err, 1) return nil, nil, cli.NewExitError(err, 1)
} }
addrFlag := ctx.Generic("address").(*flags.Address) addrFlag := ctx.Generic("address").(*flags.Address)
if addrFlag.IsSet { if addrFlag.IsSet {
@ -767,20 +780,20 @@ func getAccFromContext(ctx *cli.Context) (*wallet.Account, error) {
} }
acc := wall.GetAccount(addr) acc := wall.GetAccount(addr)
if acc == nil { 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( rawPass, err := input.ReadPassword(
fmt.Sprintf("Enter account %s password > ", address.Uint160ToString(addr))) fmt.Sprintf("Enter account %s password > ", address.Uint160ToString(addr)))
if err != nil { if err != nil {
return nil, cli.NewExitError(err, 1) return nil, nil, cli.NewExitError(err, 1)
} }
pass := strings.TrimRight(string(rawPass), "\n") pass := strings.TrimRight(string(rawPass), "\n")
err = acc.Decrypt(pass) err = acc.Decrypt(pass)
if err != nil { 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. // contractDeploy deploys contract.
@ -795,7 +808,7 @@ func contractDeploy(ctx *cli.Context) error {
} }
gas := flags.Fixed8FromContext(ctx, "gas") gas := flags.Fixed8FromContext(ctx, "gas")
acc, err := getAccFromContext(ctx) acc, _, err := getAccFromContext(ctx)
if err != nil { if err != nil {
return err return err
} }

View file

@ -1,8 +1,10 @@
package testdata package testdata
import "github.com/nspcc-dev/neo-go/pkg/interop"
func Verify() bool { func Verify() bool {
return true 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, "lock": false,
"isdefault": false "isdefault": false
},
{
"address" : "NbxpLNCCSWZ9BkYpCYT8NfN1uoxq9Rfbrn",
"key" : "6PYXDze5Ak4HahYKygcNzk6n65ACjWdDCYLSuKgA5KG8vyMJSFboUNSiPD",
"label" : "",
"contract" : {
"script" : "VwEAEUBXAANA",
"deployed" : true,
"parameters" : []
},
"lock" : false,
"isdefault" : false
} }
], ],
"scrypt": { "scrypt": {

View file

@ -11,6 +11,7 @@ import (
"github.com/nspcc-dev/neo-go/pkg/crypto/keys" "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/address"
"github.com/nspcc-dev/neo-go/pkg/io" "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/smartcontract/callflag"
"github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm/emit" "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() w := io.NewBufBinWriter()
emit.AppCall(w.BinWriter, neoContractHash, method, callflag.States, acc.PrivateKey().PublicKey().Bytes()) emit.AppCall(w.BinWriter, neoContractHash, method, callflag.States, acc.PrivateKey().PublicKey().Bytes())
emit.Opcodes(w.BinWriter, opcode.ASSERT) 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(), Account: acc.Contract.ScriptHash(),
Scopes: transaction.CalledByEntry, Scopes: transaction.CalledByEntry,
}}) },
Account: acc,
},
})
if err != nil { if err != nil {
return cli.NewExitError(err, 1) return cli.NewExitError(err, 1)
} else if err = acc.SignTx(tx); err != nil { } 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.AppCall(w.BinWriter, neoContractHash, "vote", callflag.States, addr.BytesBE(), pubArg)
emit.Opcodes(w.BinWriter, opcode.ASSERT) 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(), Account: acc.Contract.ScriptHash(),
Scopes: transaction.CalledByEntry, Scopes: transaction.CalledByEntry,
},
Account: acc,
}}) }})
if err != nil { if err != nil {
return cli.NewExitError(err, 1) return cli.NewExitError(err, 1)

View file

@ -24,6 +24,13 @@ type TransferTarget struct {
Amount int64 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. // NEP17Decimals invokes `decimals` NEP17 method on a specified contract.
func (c *Client) NEP17Decimals(tokenHash util.Uint160) (int64, error) { func (c *Client) NEP17Decimals(tokenHash util.Uint160) (int64, error) {
result, err := c.InvokeFunction(tokenHash, "decimals", []smartcontract.Parameter{}, nil) 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 { if err != nil {
return nil, fmt.Errorf("bad account address: %v", err) 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, Account: accAddr,
Scopes: transaction.CalledByEntry, 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 // If sysFee <= 0, it is determined via result of `invokescript` RPC. You should
// initialize network magic with Init before calling CreateTxFromScript. // initialize network magic with Init before calling CreateTxFromScript.
func (c *Client) CreateTxFromScript(script []byte, acc *wallet.Account, sysFee, netFee int64, func (c *Client) CreateTxFromScript(script []byte, acc *wallet.Account, sysFee, netFee int64,
cosigners []transaction.Signer) (*transaction.Transaction, error) { cosigners []SignerAccount) (*transaction.Transaction, error) {
from, err := address.StringToUint160(acc.Address) signers, accounts, err := getSigners(acc, cosigners)
if err != nil { 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 { if sysFee < 0 {
result, err := c.InvokeScript(script, signers) result, err := c.InvokeScript(script, signers)
if err != nil { 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) 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 { if err != nil {
return nil, fmt.Errorf("failed to add network fee: %w", err) 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 // SignAndPushInvocationTx signs and pushes given script as an invocation
// transaction using given wif to sign it and spending the amount of gas // 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. // 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 txHash util.Uint256
var err error var err error
@ -544,25 +544,33 @@ func (c *Client) SignAndPushInvocationTx(script []byte, acc *wallet.Account, sys
return txHash, nil return txHash, nil
} }
// getSigners returns an array of transaction signers from given sender and cosigners. // getSigners returns an array of transaction signers and corresponding accounts from
// If cosigners list already contains sender, the sender will be placed at the start of // given sender and cosigners. If cosigners list already contains sender, the sender
// the list. // will be placed at the start of the list.
func getSigners(sender util.Uint160, cosigners []transaction.Signer) []transaction.Signer { 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{ s := transaction.Signer{
Account: sender, Account: from,
Scopes: transaction.None, Scopes: transaction.None,
} }
for i, c := range cosigners { for _, c := range cosigners {
if c.Account == sender { if c.Signer.Account == from {
if i == 0 { s.Scopes = c.Signer.Scopes
return cosigners continue
} }
s.Scopes = c.Scopes signers = append(signers, c.Signer)
cosigners = append(cosigners[:i], cosigners[i+1:]...) accounts = append(accounts, c.Account)
break
} }
} signers = append([]transaction.Signer{s}, signers...)
return append([]transaction.Signer{s}, cosigners...) accounts = append([]*wallet.Account{sender}, accounts...)
return signers, accounts, nil
} }
// SignAndPushP2PNotaryRequest creates and pushes P2PNotary request constructed from the main // 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) priv := testchain.PrivateKey(0)
acc := wallet.NewAccountFromPrivateKey(priv) 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(), Account: priv.GetScriptHash(),
Scopes: transaction.CalledByEntry, Scopes: transaction.CalledByEntry,
}}) },
Account: acc,
},
})
require.NoError(t, err) require.NoError(t, err)
mp := chain.GetMemPool() mp := chain.GetMemPool()