From 63c26ca2702980b541180c21090095c78fd3b859 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Thu, 7 Apr 2022 18:13:08 +0300 Subject: [PATCH] core, rpc: support [invokefunction, invokescript, invokecontractverify]historic --- docs/rpc.md | 22 ++ pkg/core/blockchain.go | 29 ++ pkg/core/blockchainer/blockchainer.go | 1 + pkg/core/blockchainer/state_root.go | 1 + pkg/core/stateroot/module.go | 29 ++ pkg/core/stateroot_test.go | 17 ++ pkg/rpc/client/rpc.go | 81 ++++++ pkg/rpc/server/client_test.go | 50 ++++ pkg/rpc/server/server.go | 252 ++++++++++++----- pkg/rpc/server/server_test.go | 378 ++++++++++++++++++++++++++ 10 files changed, 793 insertions(+), 67 deletions(-) diff --git a/docs/rpc.md b/docs/rpc.md index ad22e75cb..4fb8c8a86 100644 --- a/docs/rpc.md +++ b/docs/rpc.md @@ -168,6 +168,28 @@ block. It can be removed in future versions, but at the moment you can use it to see how much GAS is burned with particular block (because system fees are burned). +#### `invokecontractverifyhistoric`, `invokefunctionhistoric` and `invokescripthistoric` calls + +These methods provide the ability of *historical* calls and accept block hash or +block index or stateroot hash as the first parameter and the list of parameters +that is the same as of `invokecontractverify`, `invokefunction` and +`invokescript` correspondingly. The historical call assumes that the contracts' +storage state has all its values got from MPT with the specified stateroot and +the transaction will be invoked using interop context with block of the specified +height. This allows to perform test invocation using the specified past chain +state. These methods may be useful for debugging purposes. + +Behavior note: any historical RPC call need the historical chain state to be +presented in the node storage, thus if the node keeps only latest MPT state +the historical call can not be handled properly.The historical calls only +guaranteed to correctly work on archival node that stores all MPT data. If a +node keeps the number of latest states and has the GC on (this setting +corresponds to the `RemoveUntraceableBlocks` set to `true`), then the behaviour +of historical RPC call is undefined. GC can always kick some data out of the +storage while the historical call is executing, thus keep in mind that the call +can be processed with `RemoveUntraceableBlocks` only with limitations on +available data. + #### `submitnotaryrequest` call This method can be used on P2P Notary enabled networks to submit new notary diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index efbf57d0d..2c36e6fd2 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -2148,6 +2148,35 @@ func (bc *Blockchain) GetTestVM(t trigger.Type, tx *transaction.Transaction, b * return systemInterop } +// GetTestHistoricVM returns an interop context with VM set up for a test run. +func (bc *Blockchain) GetTestHistoricVM(t trigger.Type, tx *transaction.Transaction, b *block.Block) (*interop.Context, error) { + if bc.config.KeepOnlyLatestState { + return nil, errors.New("only latest state is supported") + } + if b == nil { + return nil, errors.New("block is mandatory to produce test historic VM") + } + var mode = mpt.ModeAll + if bc.config.RemoveUntraceableBlocks { + if b.Index < bc.BlockHeight()-bc.config.MaxTraceableBlocks { + return nil, fmt.Errorf("state for height %d is outdated and removed from the storage", b.Index) + } + mode |= mpt.ModeGCFlag + } + sr, err := bc.stateRoot.GetStateRoot(b.Index) + if err != nil { + return nil, fmt.Errorf("failed to retrieve stateroot for height %d: %w", b.Index, err) + } + s := mpt.NewTrieStore(sr.Root, mode, storage.NewPrivateMemCachedStore(bc.dao.Store)) + dTrie := dao.NewSimple(s, bc.config.StateRootInHeader, bc.config.P2PSigExtensions) + dTrie.Version = bc.dao.Version + systemInterop := bc.newInteropContext(t, dTrie, b, tx) + vm := systemInterop.SpawnVM() + vm.SetPriceGetter(systemInterop.GetPrice) + vm.LoadToken = contract.LoadToken(systemInterop) + return systemInterop, nil +} + // Various witness verification errors. var ( ErrWitnessHashMismatch = errors.New("witness hash mismatch") diff --git a/pkg/core/blockchainer/blockchainer.go b/pkg/core/blockchainer/blockchainer.go index f5179245f..6e7a1fcac 100644 --- a/pkg/core/blockchainer/blockchainer.go +++ b/pkg/core/blockchainer/blockchainer.go @@ -60,6 +60,7 @@ type Blockchainer interface { GetStateModule() StateRoot GetStorageItem(id int32, key []byte) state.StorageItem GetTestVM(t trigger.Type, tx *transaction.Transaction, b *block.Block) *interop.Context + GetTestHistoricVM(t trigger.Type, tx *transaction.Transaction, b *block.Block) (*interop.Context, error) GetTransaction(util.Uint256) (*transaction.Transaction, uint32, error) SetOracle(service services.Oracle) mempool.Feer // fee interface diff --git a/pkg/core/blockchainer/state_root.go b/pkg/core/blockchainer/state_root.go index 0328c3a63..3b11f6c63 100644 --- a/pkg/core/blockchainer/state_root.go +++ b/pkg/core/blockchainer/state_root.go @@ -15,4 +15,5 @@ type StateRoot interface { GetState(root util.Uint256, key []byte) ([]byte, error) GetStateProof(root util.Uint256, key []byte) ([][]byte, error) GetStateRoot(height uint32) (*state.MPTRoot, error) + GetLatestStateHeight(root util.Uint256) (uint32, error) } diff --git a/pkg/core/stateroot/module.go b/pkg/core/stateroot/module.go index b09c5a953..fc43e7a50 100644 --- a/pkg/core/stateroot/module.go +++ b/pkg/core/stateroot/module.go @@ -1,6 +1,7 @@ package stateroot import ( + "bytes" "encoding/binary" "errors" "fmt" @@ -101,6 +102,34 @@ func (s *Module) GetStateRoot(height uint32) (*state.MPTRoot, error) { return s.getStateRoot(makeStateRootKey(height)) } +// GetLatestStateHeight returns the latest blockchain height by the given stateroot. +func (s *Module) GetLatestStateHeight(root util.Uint256) (uint32, error) { + rootBytes := root.BytesBE() + rootStartOffset := 1 + 4 // stateroot version (1 byte) + stateroot index (4 bytes) + rootEndOffset := rootStartOffset + util.Uint256Size + var ( + h uint32 + found bool + rootKey = makeStateRootKey(s.localHeight.Load()) + ) + s.Store.Seek(storage.SeekRange{ + Prefix: []byte{rootKey[0]}, // DataMPTAux + Start: rootKey[1:], // Start is a value that should be appended to the Prefix + Backwards: true, + }, func(k, v []byte) bool { + if len(k) == 5 && bytes.Equal(v[rootStartOffset:rootEndOffset], rootBytes) { + h = binary.BigEndian.Uint32(k[1:]) // cut prefix DataMPTAux + found = true + return false + } + return true + }) + if found { + return h, nil + } + return h, storage.ErrKeyNotFound +} + // CurrentLocalStateRoot returns hash of the local state root. func (s *Module) CurrentLocalStateRoot() util.Uint256 { return s.currentLocal.Load().(util.Uint256) diff --git a/pkg/core/stateroot_test.go b/pkg/core/stateroot_test.go index 3a958def5..86df6c8b5 100644 --- a/pkg/core/stateroot_test.go +++ b/pkg/core/stateroot_test.go @@ -296,3 +296,20 @@ func checkVoteBroadcasted(t *testing.T, bc *core.Blockchain, p *payload.Extensib require.True(t, len(pubs) > int(valIndex)) require.True(t, pubs[valIndex].VerifyHashable(vote.Signature, uint32(netmode.UnitTestNet), r)) } + +func TestStateroot_GetLatestStateHeight(t *testing.T) { + bc, validators, committee := chain.NewMultiWithCustomConfig(t, func(c *config.ProtocolConfiguration) { + c.P2PSigExtensions = true + }) + e := neotest.NewExecutor(t, bc, validators, committee) + initBasicChain(t, e) + + m := bc.GetStateModule() + for i := uint32(0); i < bc.BlockHeight(); i++ { + r, err := m.GetStateRoot(i) + require.NoError(t, err) + h, err := bc.GetStateModule().GetLatestStateHeight(r.Root) + require.NoError(t, err, i) + require.Equal(t, i, h) + } +} diff --git a/pkg/rpc/client/rpc.go b/pkg/rpc/client/rpc.go index d643d4414..02648d1fb 100644 --- a/pkg/rpc/client/rpc.go +++ b/pkg/rpc/client/rpc.go @@ -588,6 +588,33 @@ func (c *Client) InvokeScript(script []byte, signers []transaction.Signer) (*res return c.invokeSomething("invokescript", p, signers) } +// InvokeScriptAtHeight returns the result of the given script after running it +// true the VM using the provided chain state retrieved from the specified chain +// height. +// NOTE: This is a test invoke and will not affect the blockchain. +func (c *Client) InvokeScriptAtHeight(height uint32, script []byte, signers []transaction.Signer) (*result.Invoke, error) { + var p = request.NewRawParams(height, script) + return c.invokeSomething("invokescripthistoric", p, signers) +} + +// InvokeScriptAtBlock returns the result of the given script after running it +// true the VM using the provided chain state retrieved from the specified block +// hash. +// NOTE: This is a test invoke and will not affect the blockchain. +func (c *Client) InvokeScriptAtBlock(blockHash util.Uint256, script []byte, signers []transaction.Signer) (*result.Invoke, error) { + var p = request.NewRawParams(blockHash.StringLE(), script) + return c.invokeSomething("invokescripthistoric", p, signers) +} + +// InvokeScriptWithState returns the result of the given script after running it +// true the VM using the provided chain state retrieved from the specified +// stateroot hash. +// NOTE: This is a test invoke and will not affect the blockchain. +func (c *Client) InvokeScriptWithState(stateroot util.Uint256, script []byte, signers []transaction.Signer) (*result.Invoke, error) { + var p = request.NewRawParams(stateroot.StringLE(), script) + return c.invokeSomething("invokescripthistoric", p, signers) +} + // InvokeFunction returns the results after calling the smart contract scripthash // with the given operation and parameters. // NOTE: this is test invoke and will not affect the blockchain. @@ -596,6 +623,33 @@ func (c *Client) InvokeFunction(contract util.Uint160, operation string, params return c.invokeSomething("invokefunction", p, signers) } +// InvokeFunctionAtHeight returns the results after calling the smart contract +// with the given operation and parameters at the given blockchain state +// specified by the blockchain height. +// NOTE: this is test invoke and will not affect the blockchain. +func (c *Client) InvokeFunctionAtHeight(height uint32, contract util.Uint160, operation string, params []smartcontract.Parameter, signers []transaction.Signer) (*result.Invoke, error) { + var p = request.NewRawParams(height, contract.StringLE(), operation, params) + return c.invokeSomething("invokefunctionhistoric", p, signers) +} + +// InvokeFunctionAtBlock returns the results after calling the smart contract +// with the given operation and parameters at given the blockchain state +// specified by the block hash. +// NOTE: this is test invoke and will not affect the blockchain. +func (c *Client) InvokeFunctionAtBlock(blockHash util.Uint256, contract util.Uint160, operation string, params []smartcontract.Parameter, signers []transaction.Signer) (*result.Invoke, error) { + var p = request.NewRawParams(blockHash.StringLE(), contract.StringLE(), operation, params) + return c.invokeSomething("invokefunctionhistoric", p, signers) +} + +// InvokeFunctionWithState returns the results after calling the smart contract +// with the given operation and parameters at the given blockchain state defined +// by the specified stateroot hash. +// NOTE: this is test invoke and will not affect the blockchain. +func (c *Client) InvokeFunctionWithState(stateroot util.Uint256, contract util.Uint160, operation string, params []smartcontract.Parameter, signers []transaction.Signer) (*result.Invoke, error) { + var p = request.NewRawParams(stateroot.StringLE(), contract.StringLE(), operation, params) + return c.invokeSomething("invokefunctionhistoric", p, signers) +} + // InvokeContractVerify returns the results after calling `verify` method of the smart contract // with the given parameters under verification trigger type. // NOTE: this is test invoke and will not affect the blockchain. @@ -604,6 +658,33 @@ func (c *Client) InvokeContractVerify(contract util.Uint160, params []smartcontr return c.invokeSomething("invokecontractverify", p, signers, witnesses...) } +// InvokeContractVerifyAtHeight returns the results after calling `verify` method +// of the smart contract with the given parameters under verification trigger type +// at the blockchain state specified by the blockchain height. +// NOTE: this is test invoke and will not affect the blockchain. +func (c *Client) InvokeContractVerifyAtHeight(height uint32, contract util.Uint160, params []smartcontract.Parameter, signers []transaction.Signer, witnesses ...transaction.Witness) (*result.Invoke, error) { + var p = request.NewRawParams(height, contract.StringLE(), params) + return c.invokeSomething("invokecontractverifyhistoric", p, signers, witnesses...) +} + +// InvokeContractVerifyAtBlock returns the results after calling `verify` method +// of the smart contract with the given parameters under verification trigger type +// at the blockchain state specified by the block hash. +// NOTE: this is test invoke and will not affect the blockchain. +func (c *Client) InvokeContractVerifyAtBlock(blockHash util.Uint256, contract util.Uint160, params []smartcontract.Parameter, signers []transaction.Signer, witnesses ...transaction.Witness) (*result.Invoke, error) { + var p = request.NewRawParams(blockHash.StringLE(), contract.StringLE(), params) + return c.invokeSomething("invokecontractverifyhistoric", p, signers, witnesses...) +} + +// InvokeContractVerifyWithState returns the results after calling `verify` method +// of the smart contract with the given parameters under verification trigger type +// at the blockchain state specified by the stateroot hash. +// NOTE: this is test invoke and will not affect the blockchain. +func (c *Client) InvokeContractVerifyWithState(stateroot util.Uint256, contract util.Uint160, params []smartcontract.Parameter, signers []transaction.Signer, witnesses ...transaction.Witness) (*result.Invoke, error) { + var p = request.NewRawParams(stateroot.StringLE(), contract.StringLE(), params) + return c.invokeSomething("invokecontractverifyhistoric", p, signers, witnesses...) +} + // invokeSomething is an inner wrapper for Invoke* functions. func (c *Client) invokeSomething(method string, p request.RawParams, signers []transaction.Signer, witnesses ...transaction.Witness) (*result.Invoke, error) { var resp = new(result.Invoke) diff --git a/pkg/rpc/server/client_test.go b/pkg/rpc/server/client_test.go index dc1ae0c5e..ca2ac2b63 100644 --- a/pkg/rpc/server/client_test.go +++ b/pkg/rpc/server/client_test.go @@ -766,6 +766,56 @@ func TestInvokeVerify(t *testing.T) { require.True(t, res.Stack[0].Value().(bool)) }) + t.Run("positive, historic, by height, with signer", func(t *testing.T) { + h := chain.BlockHeight() - 1 + res, err := c.InvokeContractVerifyAtHeight(h, contract, smartcontract.Params{}, []transaction.Signer{{Account: testchain.PrivateKeyByID(0).PublicKey().GetScriptHash()}}) + require.NoError(t, err) + require.Equal(t, "HALT", res.State) + require.Equal(t, 1, len(res.Stack)) + require.True(t, res.Stack[0].Value().(bool)) + }) + + t.Run("positive, historic, by block, with signer", func(t *testing.T) { + res, err := c.InvokeContractVerifyAtBlock(chain.GetHeaderHash(int(chain.BlockHeight())-1), contract, smartcontract.Params{}, []transaction.Signer{{Account: testchain.PrivateKeyByID(0).PublicKey().GetScriptHash()}}) + require.NoError(t, err) + require.Equal(t, "HALT", res.State) + require.Equal(t, 1, len(res.Stack)) + require.True(t, res.Stack[0].Value().(bool)) + }) + + t.Run("positive, historic, by stateroot, with signer", func(t *testing.T) { + h := chain.BlockHeight() - 1 + sr, err := chain.GetStateModule().GetStateRoot(h) + require.NoError(t, err) + res, err := c.InvokeContractVerifyWithState(sr.Root, contract, smartcontract.Params{}, []transaction.Signer{{Account: testchain.PrivateKeyByID(0).PublicKey().GetScriptHash()}}) + require.NoError(t, err) + require.Equal(t, "HALT", res.State) + require.Equal(t, 1, len(res.Stack)) + require.True(t, res.Stack[0].Value().(bool)) + }) + + t.Run("bad, historic, by hash: contract not found", func(t *testing.T) { + var h uint32 = 1 + _, err = c.InvokeContractVerifyAtHeight(h, contract, smartcontract.Params{}, []transaction.Signer{{Account: testchain.PrivateKeyByID(0).PublicKey().GetScriptHash()}}) + require.Error(t, err) + // TODO: check that error is `ErrUnknownVerificationContract` + }) + + t.Run("bad, historic, by block: contract not found", func(t *testing.T) { + _, err = c.InvokeContractVerifyAtBlock(chain.GetHeaderHash(1), contract, smartcontract.Params{}, []transaction.Signer{{Account: testchain.PrivateKeyByID(0).PublicKey().GetScriptHash()}}) + require.Error(t, err) + // TODO: check that error is `ErrUnknownVerificationContract` + }) + + t.Run("bad, historic, by stateroot: contract not found", func(t *testing.T) { + var h uint32 = 1 + sr, err := chain.GetStateModule().GetStateRoot(h) + require.NoError(t, err) + _, err = c.InvokeContractVerifyWithState(sr.Root, contract, smartcontract.Params{}, []transaction.Signer{{Account: testchain.PrivateKeyByID(0).PublicKey().GetScriptHash()}}) + require.Error(t, err) + // TODO: check that error is `ErrUnknownVerificationContract` + }) + t.Run("positive, with signer and witness", func(t *testing.T) { res, err := c.InvokeContractVerify(contract, smartcontract.Params{}, []transaction.Signer{{Account: testchain.PrivateKeyByID(0).PublicKey().GetScriptHash()}}, transaction.Witness{InvocationScript: []byte{byte(opcode.PUSH1), byte(opcode.RET)}}) require.NoError(t, err) diff --git a/pkg/rpc/server/server.go b/pkg/rpc/server/server.go index d08758173..590a44662 100644 --- a/pkg/rpc/server/server.go +++ b/pkg/rpc/server/server.go @@ -24,6 +24,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/core/block" "github.com/nspcc-dev/neo-go/pkg/core/blockchainer" "github.com/nspcc-dev/neo-go/pkg/core/fee" + "github.com/nspcc-dev/neo-go/pkg/core/interop" "github.com/nspcc-dev/neo-go/pkg/core/interop/iterator" "github.com/nspcc-dev/neo-go/pkg/core/mempoolevent" "github.com/nspcc-dev/neo-go/pkg/core/mpt" @@ -108,46 +109,49 @@ const ( ) var rpcHandlers = map[string]func(*Server, request.Params) (interface{}, *response.Error){ - "calculatenetworkfee": (*Server).calculateNetworkFee, - "findstates": (*Server).findStates, - "getapplicationlog": (*Server).getApplicationLog, - "getbestblockhash": (*Server).getBestBlockHash, - "getblock": (*Server).getBlock, - "getblockcount": (*Server).getBlockCount, - "getblockhash": (*Server).getBlockHash, - "getblockheader": (*Server).getBlockHeader, - "getblockheadercount": (*Server).getBlockHeaderCount, - "getblocksysfee": (*Server).getBlockSysFee, - "getcommittee": (*Server).getCommittee, - "getconnectioncount": (*Server).getConnectionCount, - "getcontractstate": (*Server).getContractState, - "getnativecontracts": (*Server).getNativeContracts, - "getnep11balances": (*Server).getNEP11Balances, - "getnep11properties": (*Server).getNEP11Properties, - "getnep11transfers": (*Server).getNEP11Transfers, - "getnep17balances": (*Server).getNEP17Balances, - "getnep17transfers": (*Server).getNEP17Transfers, - "getpeers": (*Server).getPeers, - "getproof": (*Server).getProof, - "getrawmempool": (*Server).getRawMempool, - "getrawtransaction": (*Server).getrawtransaction, - "getstate": (*Server).getState, - "getstateheight": (*Server).getStateHeight, - "getstateroot": (*Server).getStateRoot, - "getstorage": (*Server).getStorage, - "gettransactionheight": (*Server).getTransactionHeight, - "getunclaimedgas": (*Server).getUnclaimedGas, - "getnextblockvalidators": (*Server).getNextBlockValidators, - "getversion": (*Server).getVersion, - "invokefunction": (*Server).invokeFunction, - "invokescript": (*Server).invokescript, - "invokecontractverify": (*Server).invokeContractVerify, - "sendrawtransaction": (*Server).sendrawtransaction, - "submitblock": (*Server).submitBlock, - "submitnotaryrequest": (*Server).submitNotaryRequest, - "submitoracleresponse": (*Server).submitOracleResponse, - "validateaddress": (*Server).validateAddress, - "verifyproof": (*Server).verifyProof, + "calculatenetworkfee": (*Server).calculateNetworkFee, + "findstates": (*Server).findStates, + "getapplicationlog": (*Server).getApplicationLog, + "getbestblockhash": (*Server).getBestBlockHash, + "getblock": (*Server).getBlock, + "getblockcount": (*Server).getBlockCount, + "getblockhash": (*Server).getBlockHash, + "getblockheader": (*Server).getBlockHeader, + "getblockheadercount": (*Server).getBlockHeaderCount, + "getblocksysfee": (*Server).getBlockSysFee, + "getcommittee": (*Server).getCommittee, + "getconnectioncount": (*Server).getConnectionCount, + "getcontractstate": (*Server).getContractState, + "getnativecontracts": (*Server).getNativeContracts, + "getnep11balances": (*Server).getNEP11Balances, + "getnep11properties": (*Server).getNEP11Properties, + "getnep11transfers": (*Server).getNEP11Transfers, + "getnep17balances": (*Server).getNEP17Balances, + "getnep17transfers": (*Server).getNEP17Transfers, + "getpeers": (*Server).getPeers, + "getproof": (*Server).getProof, + "getrawmempool": (*Server).getRawMempool, + "getrawtransaction": (*Server).getrawtransaction, + "getstate": (*Server).getState, + "getstateheight": (*Server).getStateHeight, + "getstateroot": (*Server).getStateRoot, + "getstorage": (*Server).getStorage, + "gettransactionheight": (*Server).getTransactionHeight, + "getunclaimedgas": (*Server).getUnclaimedGas, + "getnextblockvalidators": (*Server).getNextBlockValidators, + "getversion": (*Server).getVersion, + "invokefunction": (*Server).invokeFunction, + "invokefunctionhistoric": (*Server).invokeFunctionHistoric, + "invokescript": (*Server).invokescript, + "invokescripthistoric": (*Server).invokescripthistoric, + "invokecontractverify": (*Server).invokeContractVerify, + "invokecontractverifyhistoric": (*Server).invokeContractVerifyHistoric, + "sendrawtransaction": (*Server).sendrawtransaction, + "submitblock": (*Server).submitBlock, + "submitnotaryrequest": (*Server).submitNotaryRequest, + "submitoracleresponse": (*Server).submitOracleResponse, + "validateaddress": (*Server).validateAddress, + "verifyproof": (*Server).verifyProof, } var rpcWsHandlers = map[string]func(*Server, request.Params, *subscriber) (interface{}, *response.Error){ @@ -866,7 +870,7 @@ func (s *Server) invokeReadOnly(bw *io.BufBinWriter, h util.Uint160, method stri } script := bw.Bytes() tx := &transaction.Transaction{Script: script} - b, err := s.getFakeNextBlock() + b, err := s.getFakeNextBlock(s.chain.BlockHeight() + 1) if err != nil { return nil, nil, err } @@ -1571,16 +1575,40 @@ func (s *Server) getCommittee(_ request.Params) (interface{}, *response.Error) { // invokeFunction implements the `invokeFunction` RPC call. func (s *Server) invokeFunction(reqParams request.Params) (interface{}, *response.Error) { + tx, verbose, respErr := s.getInvokeFunctionParams(reqParams) + if respErr != nil { + return nil, respErr + } + return s.runScriptInVM(trigger.Application, tx.Script, util.Uint160{}, tx, nil, verbose) +} + +// invokeFunctionHistoric implements the `invokeFunctionHistoric` RPC call. +func (s *Server) invokeFunctionHistoric(reqParams request.Params) (interface{}, *response.Error) { + b, respErr := s.getHistoricParams(reqParams) + if respErr != nil { + return nil, respErr + } if len(reqParams) < 2 { return nil, response.ErrInvalidParams } + tx, verbose, respErr := s.getInvokeFunctionParams(reqParams[1:]) + if respErr != nil { + return nil, respErr + } + return s.runScriptInVM(trigger.Application, tx.Script, util.Uint160{}, tx, b, verbose) +} + +func (s *Server) getInvokeFunctionParams(reqParams request.Params) (*transaction.Transaction, bool, *response.Error) { + if len(reqParams) < 2 { + return nil, false, response.ErrInvalidParams + } scriptHash, responseErr := s.contractScriptHashFromParam(reqParams.Value(0)) if responseErr != nil { - return nil, responseErr + return nil, false, responseErr } method, err := reqParams[1].GetString() if err != nil { - return nil, response.ErrInvalidParams + return nil, false, response.ErrInvalidParams } var params *request.Param if len(reqParams) > 2 { @@ -1590,7 +1618,7 @@ func (s *Server) invokeFunction(reqParams request.Params) (interface{}, *respons if len(reqParams) > 3 { signers, _, err := reqParams[3].GetSignersWithWitnesses() if err != nil { - return nil, response.ErrInvalidParams + return nil, false, response.ErrInvalidParams } tx.Signers = signers } @@ -1598,7 +1626,7 @@ func (s *Server) invokeFunction(reqParams request.Params) (interface{}, *respons if len(reqParams) > 4 { verbose, err = reqParams[4].GetBoolean() if err != nil { - return nil, response.ErrInvalidParams + return nil, false, response.ErrInvalidParams } } if len(tx.Signers) == 0 { @@ -1606,28 +1634,48 @@ func (s *Server) invokeFunction(reqParams request.Params) (interface{}, *respons } script, err := request.CreateFunctionInvocationScript(scriptHash, method, params) if err != nil { - return nil, response.NewInternalServerError("can't create invocation script", err) + return nil, false, response.NewInternalServerError("can't create invocation script", err) } tx.Script = script - return s.runScriptInVM(trigger.Application, script, util.Uint160{}, tx, verbose) + return tx, verbose, nil } // invokescript implements the `invokescript` RPC call. func (s *Server) invokescript(reqParams request.Params) (interface{}, *response.Error) { - if len(reqParams) < 1 { + tx, verbose, respErr := s.getInvokeScriptParams(reqParams) + if respErr != nil { + return nil, respErr + } + return s.runScriptInVM(trigger.Application, tx.Script, util.Uint160{}, tx, nil, verbose) +} + +// invokescripthistoric implements the `invokescripthistoric` RPC call. +func (s *Server) invokescripthistoric(reqParams request.Params) (interface{}, *response.Error) { + b, respErr := s.getHistoricParams(reqParams) + if respErr != nil { + return nil, respErr + } + if len(reqParams) < 2 { return nil, response.ErrInvalidParams } + tx, verbose, respErr := s.getInvokeScriptParams(reqParams[1:]) + if respErr != nil { + return nil, respErr + } + return s.runScriptInVM(trigger.Application, tx.Script, util.Uint160{}, tx, b, verbose) +} - script, err := reqParams[0].GetBytesBase64() +func (s *Server) getInvokeScriptParams(reqParams request.Params) (*transaction.Transaction, bool, *response.Error) { + script, err := reqParams.Value(0).GetBytesBase64() if err != nil { - return nil, response.ErrInvalidParams + return nil, false, response.ErrInvalidParams } tx := &transaction.Transaction{} if len(reqParams) > 1 { signers, witnesses, err := reqParams[1].GetSignersWithWitnesses() if err != nil { - return nil, response.ErrInvalidParams + return nil, false, response.ErrInvalidParams } tx.Signers = signers tx.Scripts = witnesses @@ -1636,33 +1684,57 @@ func (s *Server) invokescript(reqParams request.Params) (interface{}, *response. if len(reqParams) > 2 { verbose, err = reqParams[2].GetBoolean() if err != nil { - return nil, response.ErrInvalidParams + return nil, false, response.ErrInvalidParams } } if len(tx.Signers) == 0 { tx.Signers = []transaction.Signer{{Account: util.Uint160{}, Scopes: transaction.None}} } tx.Script = script - return s.runScriptInVM(trigger.Application, script, util.Uint160{}, tx, verbose) + return tx, verbose, nil } // invokeContractVerify implements the `invokecontractverify` RPC call. func (s *Server) invokeContractVerify(reqParams request.Params) (interface{}, *response.Error) { + scriptHash, tx, invocationScript, respErr := s.getInvokeContractVerifyParams(reqParams) + if respErr != nil { + return nil, respErr + } + return s.runScriptInVM(trigger.Verification, invocationScript, scriptHash, tx, nil, false) +} + +// invokeContractVerifyHistoric implements the `invokecontractverifyhistoric` RPC call. +func (s *Server) invokeContractVerifyHistoric(reqParams request.Params) (interface{}, *response.Error) { + b, respErr := s.getHistoricParams(reqParams) + if respErr != nil { + return nil, respErr + } + if len(reqParams) < 2 { + return nil, response.ErrInvalidParams + } + scriptHash, tx, invocationScript, respErr := s.getInvokeContractVerifyParams(reqParams[1:]) + if respErr != nil { + return nil, respErr + } + return s.runScriptInVM(trigger.Verification, invocationScript, scriptHash, tx, b, false) +} + +func (s *Server) getInvokeContractVerifyParams(reqParams request.Params) (util.Uint160, *transaction.Transaction, []byte, *response.Error) { scriptHash, responseErr := s.contractScriptHashFromParam(reqParams.Value(0)) if responseErr != nil { - return nil, responseErr + return util.Uint160{}, nil, nil, responseErr } bw := io.NewBufBinWriter() if len(reqParams) > 1 { args, err := reqParams[1].GetArray() // second `invokecontractverify` parameter is an array of arguments for `verify` method if err != nil { - return nil, response.WrapErrorWithData(response.ErrInvalidParams, err) + return util.Uint160{}, nil, nil, response.WrapErrorWithData(response.ErrInvalidParams, err) } if len(args) > 0 { err := request.ExpandArrayIntoScript(bw.BinWriter, args) if err != nil { - return nil, response.NewRPCError("can't create witness invocation script", err.Error(), err) + return util.Uint160{}, nil, nil, response.NewRPCError("can't create witness invocation script", err.Error(), err) } } } @@ -1672,7 +1744,7 @@ func (s *Server) invokeContractVerify(reqParams request.Params) (interface{}, *r if len(reqParams) > 2 { signers, witnesses, err := reqParams[2].GetSignersWithWitnesses() if err != nil { - return nil, response.ErrInvalidParams + return util.Uint160{}, nil, nil, response.ErrInvalidParams } tx.Signers = signers tx.Scripts = witnesses @@ -1680,16 +1752,51 @@ func (s *Server) invokeContractVerify(reqParams request.Params) (interface{}, *r tx.Signers = []transaction.Signer{{Account: scriptHash}} tx.Scripts = []transaction.Witness{{InvocationScript: invocationScript, VerificationScript: []byte{}}} } - return s.runScriptInVM(trigger.Verification, invocationScript, scriptHash, tx, false) + return scriptHash, tx, invocationScript, nil } -func (s *Server) getFakeNextBlock() (*block.Block, error) { +// getHistoricParams checks that historic calls are supported and returns fake block +// with the specified index to perform the historic call. It also checks that +// specified stateroot is stored at the specified height for further request +// handling consistency. +func (s *Server) getHistoricParams(reqParams request.Params) (*block.Block, *response.Error) { + if s.chain.GetConfig().KeepOnlyLatestState { + return nil, response.NewInvalidRequestError("only latest state is supported", errKeepOnlyLatestState) + } + if len(reqParams) < 1 { + return nil, response.ErrInvalidParams + } + height, respErr := s.blockHeightFromParam(reqParams.Value(0)) + if respErr != nil { + hash, err := reqParams.Value(0).GetUint256() + if err != nil { + return nil, response.NewInvalidParamsError("invalid block hash or index or stateroot hash", err) + } + b, err := s.chain.GetBlock(hash) + if err != nil { + stateH, err := s.chain.GetStateModule().GetLatestStateHeight(hash) + if err != nil { + return nil, response.NewInvalidParamsError(fmt.Sprintf("unknown block or stateroot: %s", err), err) + } + height = int(stateH) + } else { + height = int(b.Index) + } + } + b, err := s.getFakeNextBlock(uint32(height)) + if err != nil { + return nil, response.NewInternalServerError(fmt.Sprintf("can't create fake block for height %d: %s", height, err.Error()), err) + } + return b, nil +} + +func (s *Server) getFakeNextBlock(nextBlockHeight uint32) (*block.Block, error) { // When transferring funds, script execution does no auto GAS claim, // because it depends on persisting tx height. // This is why we provide block here. b := block.New(s.stateRootEnabled) - b.Index = s.chain.BlockHeight() + 1 - hdr, err := s.chain.GetHeader(s.chain.GetHeaderHash(int(s.chain.BlockHeight()))) + b.Index = nextBlockHeight + hdr, err := s.chain.GetHeader(s.chain.GetHeaderHash(int(nextBlockHeight - 1))) if err != nil { return nil, err } @@ -1702,12 +1809,23 @@ func (s *Server) getFakeNextBlock() (*block.Block, error) { // witness invocation script in case of `verification` trigger (it pushes `verify` // arguments on stack before verification). In case of contract verification // contractScriptHash should be specified. -func (s *Server) runScriptInVM(t trigger.Type, script []byte, contractScriptHash util.Uint160, tx *transaction.Transaction, verbose bool) (*result.Invoke, *response.Error) { - b, err := s.getFakeNextBlock() - if err != nil { - return nil, response.NewInternalServerError("can't create fake block", err) +func (s *Server) runScriptInVM(t trigger.Type, script []byte, contractScriptHash util.Uint160, tx *transaction.Transaction, b *block.Block, verbose bool) (*result.Invoke, *response.Error) { + var ( + err error + ic *interop.Context + ) + if b == nil { + b, err = s.getFakeNextBlock(s.chain.BlockHeight() + 1) + if err != nil { + return nil, response.NewInternalServerError("can't create fake block", err) + } + ic = s.chain.GetTestVM(t, tx, b) + } else { + ic, err = s.chain.GetTestHistoricVM(t, tx, b) + if err != nil { + return nil, response.NewInternalServerError("failed to create historic VM", err) + } } - ic := s.chain.GetTestVM(t, tx, b) if verbose { ic.VM.EnableInvocationTree() } @@ -1720,7 +1838,7 @@ func (s *Server) runScriptInVM(t trigger.Type, script []byte, contractScriptHash ic.VM.GasLimit = gasPolicy } - err := s.chain.InitVerificationContext(ic, contractScriptHash, &transaction.Witness{InvocationScript: script, VerificationScript: []byte{}}) + err = s.chain.InitVerificationContext(ic, contractScriptHash, &transaction.Witness{InvocationScript: script, VerificationScript: []byte{}}) if err != nil { return nil, response.NewInternalServerError("can't prepare verification VM", err) } diff --git a/pkg/rpc/server/server_test.go b/pkg/rpc/server/server_test.go index 868e19eb4..b7bfc46fc 100644 --- a/pkg/rpc/server/server_test.go +++ b/pkg/rpc/server/server_test.go @@ -75,6 +75,7 @@ const ( nfsoContractHash = "5f9ebd6b001b54c7bc70f96e0412fcf415dfe09f" nfsoToken1ID = "7e244ffd6aa85fb1579d2ed22e9b761ab62e3486" invokescriptContractAVM = "VwIADBQBDAMOBQYMDQIODw0DDgcJAAAAAErZMCQE2zBwaEH4J+yMqiYEEUAMFA0PAwIJAAIBAwcDBAUCAQAOBgwJStkwJATbMHFpQfgn7IyqJgQSQBNA" + block20StateRootLE = "19ec3c3d01afe5274e8bb4a393c97da708c5608c5b0ad116c16108b6a04fb08e" ) var ( @@ -999,6 +1000,134 @@ var rpcTestCases = map[string][]rpcTestCase{ fail: true, }, }, + "invokefunctionhistoric": { + { + name: "positive, by index", + params: `[20, "50befd26fdf6e4d957c11e078b24ebce6291456f", "test", []]`, + result: func(e *executor) interface{} { return &result.Invoke{} }, + check: func(t *testing.T, e *executor, inv interface{}) { + res, ok := inv.(*result.Invoke) + require.True(t, ok) + assert.NotNil(t, res.Script) + assert.NotEqual(t, "", res.State) + assert.NotEqual(t, 0, res.GasConsumed) + }, + }, + { + name: "positive, by stateroot", + params: `["` + block20StateRootLE + `", "50befd26fdf6e4d957c11e078b24ebce6291456f", "test", []]`, + result: func(e *executor) interface{} { return &result.Invoke{} }, + check: func(t *testing.T, e *executor, inv interface{}) { + res, ok := inv.(*result.Invoke) + require.True(t, ok) + assert.NotNil(t, res.Script) + assert.NotEqual(t, "", res.State) + assert.NotEqual(t, 0, res.GasConsumed) + }, + }, + { + name: "positive, with notifications", + params: `[20, "` + nnsContractHash + `", "transfer", [{"type":"Hash160", "value":"0x0bcd2978634d961c24f5aea0802297ff128724d6"},{"type":"String", "value":"neo.com"},{"type":"Any", "value":null}],["0xb248508f4ef7088e10c48f14d04be3272ca29eee"]]`, + result: func(e *executor) interface{} { + script := []byte{0x0b, 0x0c, 0x07, 0x6e, 0x65, 0x6f, 0x2e, 0x63, 0x6f, 0x6d, 0x0c, 0x14, 0xd6, 0x24, 0x87, 0x12, 0xff, 0x97, 0x22, 0x80, 0xa0, 0xae, 0xf5, 0x24, 0x1c, 0x96, 0x4d, 0x63, 0x78, 0x29, 0xcd, 0xb, 0x13, 0xc0, 0x1f, 0xc, 0x8, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x65, 0x72, 0xc, 0x14, 0x1f, 0xe2, 0x37, 0x5c, 0xdc, 0xdb, 0xb2, 0x80, 0x40, 0x78, 0x65, 0x35, 0xd5, 0xef, 0xe4, 0x3, 0x39, 0x56, 0x92, 0xee, 0x41, 0x62, 0x7d, 0x5b, 0x52} + return &result.Invoke{ + State: "HALT", + GasConsumed: 32167260, + Script: script, + Stack: []stackitem.Item{stackitem.Make(true)}, + Notifications: []state.NotificationEvent{{ + ScriptHash: nnsHash, + Name: "Transfer", + Item: stackitem.NewArray([]stackitem.Item{ + stackitem.Make([]byte{0xee, 0x9e, 0xa2, 0x2c, 0x27, 0xe3, 0x4b, 0xd0, 0x14, 0x8f, 0xc4, 0x10, 0x8e, 0x08, 0xf7, 0x4e, 0x8f, 0x50, 0x48, 0xb2}), + stackitem.Make([]byte{0xd6, 0x24, 0x87, 0x12, 0xff, 0x97, 0x22, 0x80, 0xa0, 0xae, 0xf5, 0x24, 0x1c, 0x96, 0x4d, 0x63, 0x78, 0x29, 0xcd, 0x0b}), + stackitem.Make(1), + stackitem.Make("neo.com"), + }), + }}, + } + }, + }, + { + name: "positive, verbose", + params: `[20, "` + nnsContractHash + `", "resolve", [{"type":"String", "value":"neo.com"},{"type":"Integer","value":1}], [], true]`, + result: func(e *executor) interface{} { + script := []byte{0x11, 0xc, 0x7, 0x6e, 0x65, 0x6f, 0x2e, 0x63, 0x6f, 0x6d, 0x12, 0xc0, 0x1f, 0xc, 0x7, 0x72, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0xc, 0x14, 0x1f, 0xe2, 0x37, 0x5c, 0xdc, 0xdb, 0xb2, 0x80, 0x40, 0x78, 0x65, 0x35, 0xd5, 0xef, 0xe4, 0x3, 0x39, 0x56, 0x92, 0xee, 0x41, 0x62, 0x7d, 0x5b, 0x52} + stdHash, _ := e.chain.GetNativeContractScriptHash(nativenames.StdLib) + cryptoHash, _ := e.chain.GetNativeContractScriptHash(nativenames.CryptoLib) + return &result.Invoke{ + State: "HALT", + GasConsumed: 15928320, + Script: script, + Stack: []stackitem.Item{stackitem.Make("1.2.3.4")}, + Notifications: []state.NotificationEvent{}, + Diagnostics: &result.InvokeDiag{ + Changes: []storage.Operation{}, + Invocations: []*vm.InvocationTree{{ + Current: hash.Hash160(script), + Calls: []*vm.InvocationTree{ + { + Current: nnsHash, + Calls: []*vm.InvocationTree{ + { + Current: stdHash, + }, + { + Current: cryptoHash, + }, + { + Current: stdHash, + }, + { + Current: cryptoHash, + }, + { + Current: cryptoHash, + }, + }, + }, + }, + }}, + }, + } + }, + }, + { + name: "no params", + params: `[]`, + fail: true, + }, + { + name: "no args", + params: `[20]`, + fail: true, + }, + { + name: "not a string", + params: `[20, 42, "test", []]`, + fail: true, + }, + { + name: "not a scripthash", + params: `[20,"qwerty", "test", []]`, + fail: true, + }, + { + name: "bad params", + params: `[20,"50befd26fdf6e4d957c11e078b24ebce6291456f", "test", [{"type": "Integer", "value": "qwerty"}]]`, + fail: true, + }, + { + name: "bad height", + params: `[100500,"50befd26fdf6e4d957c11e078b24ebce6291456f", "test", [{"type": "Integer", "value": 1}]]`, + fail: true, + }, + { + name: "bad stateroot", + params: `["` + util.Uint256{1, 2, 3}.StringLE() + `","50befd26fdf6e4d957c11e078b24ebce6291456f", "test", [{"type": "Integer", "value": 1}]]`, + fail: true, + }, + }, "invokescript": { { name: "positive", @@ -1098,6 +1227,132 @@ var rpcTestCases = map[string][]rpcTestCase{ fail: true, }, }, + "invokescripthistoric": { + { + name: "positive, by index", + params: `[20,"UcVrDUhlbGxvLCB3b3JsZCFoD05lby5SdW50aW1lLkxvZ2FsdWY="]`, + result: func(e *executor) interface{} { return &result.Invoke{} }, + check: func(t *testing.T, e *executor, inv interface{}) { + res, ok := inv.(*result.Invoke) + require.True(t, ok) + assert.NotEqual(t, "", res.Script) + assert.NotEqual(t, "", res.State) + assert.NotEqual(t, 0, res.GasConsumed) + }, + }, + { + name: "positive, by stateroot", + params: `["` + block20StateRootLE + `","UcVrDUhlbGxvLCB3b3JsZCFoD05lby5SdW50aW1lLkxvZ2FsdWY="]`, + result: func(e *executor) interface{} { return &result.Invoke{} }, + check: func(t *testing.T, e *executor, inv interface{}) { + res, ok := inv.(*result.Invoke) + require.True(t, ok) + assert.NotEqual(t, "", res.Script) + assert.NotEqual(t, "", res.State) + assert.NotEqual(t, 0, res.GasConsumed) + }, + }, + { + name: "positive,verbose", + params: `[20, "UcVrDUhlbGxvLCB3b3JsZCFoD05lby5SdW50aW1lLkxvZ2FsdWY=",[],true]`, + result: func(e *executor) interface{} { + script := []byte{0x51, 0xc5, 0x6b, 0xd, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x2c, 0x20, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x21, 0x68, 0xf, 0x4e, 0x65, 0x6f, 0x2e, 0x52, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x4c, 0x6f, 0x67, 0x61, 0x6c, 0x75, 0x66} + return &result.Invoke{ + State: "FAULT", + GasConsumed: 60, + Script: script, + Stack: []stackitem.Item{}, + FaultException: "at instruction 0 (ROT): too big index", + Notifications: []state.NotificationEvent{}, + Diagnostics: &result.InvokeDiag{ + Changes: []storage.Operation{}, + Invocations: []*vm.InvocationTree{{ + Current: hash.Hash160(script), + }}, + }, + } + }, + }, + { + name: "positive, good witness", + // script is base64-encoded `invokescript_contract.avm` representation, hashes are hex-encoded LE bytes of hashes used in the contract with `0x` prefix + params: fmt.Sprintf(`[20,"%s",["0x0000000009070e030d0f0e020d0c06050e030c01","0x090c060e00010205040307030102000902030f0d"]]`, invokescriptContractAVM), + result: func(e *executor) interface{} { return &result.Invoke{} }, + check: func(t *testing.T, e *executor, inv interface{}) { + res, ok := inv.(*result.Invoke) + require.True(t, ok) + assert.Equal(t, "HALT", res.State) + require.Equal(t, 1, len(res.Stack)) + require.Equal(t, big.NewInt(3), res.Stack[0].Value()) + }, + }, + { + name: "positive, bad witness of second hash", + params: fmt.Sprintf(`[20,"%s",["0x0000000009070e030d0f0e020d0c06050e030c01"]]`, invokescriptContractAVM), + result: func(e *executor) interface{} { return &result.Invoke{} }, + check: func(t *testing.T, e *executor, inv interface{}) { + res, ok := inv.(*result.Invoke) + require.True(t, ok) + assert.Equal(t, "HALT", res.State) + require.Equal(t, 1, len(res.Stack)) + require.Equal(t, big.NewInt(2), res.Stack[0].Value()) + }, + }, + { + name: "positive, no good hashes", + params: fmt.Sprintf(`[20,"%s"]`, invokescriptContractAVM), + result: func(e *executor) interface{} { return &result.Invoke{} }, + check: func(t *testing.T, e *executor, inv interface{}) { + res, ok := inv.(*result.Invoke) + require.True(t, ok) + assert.Equal(t, "HALT", res.State) + require.Equal(t, 1, len(res.Stack)) + require.Equal(t, big.NewInt(1), res.Stack[0].Value()) + }, + }, + { + name: "positive, bad hashes witness", + params: fmt.Sprintf(`[20,"%s",["0x0000000009070e030d0f0e020d0c06050e030c02"]]`, invokescriptContractAVM), + result: func(e *executor) interface{} { return &result.Invoke{} }, + check: func(t *testing.T, e *executor, inv interface{}) { + res, ok := inv.(*result.Invoke) + require.True(t, ok) + assert.Equal(t, "HALT", res.State) + assert.Equal(t, 1, len(res.Stack)) + assert.Equal(t, big.NewInt(1), res.Stack[0].Value()) + }, + }, + { + name: "no params", + params: `[]`, + fail: true, + }, + { + name: "no script", + params: `[20]`, + fail: true, + }, + { + name: "not a string", + params: `[20,42]`, + fail: true, + }, + { + name: "bas string", + params: `[20, "qwerty"]`, + fail: true, + }, + { + name: "bas height", + params: `[100500,"qwerty"]`, + fail: true, + }, + { + name: "bas stateroot", + params: `["` + util.Uint256{1, 2, 3}.StringLE() + `","UcVrDUhlbGxvLCB3b3JsZCFoD05lby5SdW50aW1lLkxvZ2FsdWY="]`, + fail: true, + }, + }, "invokecontractverify": { { name: "positive", @@ -1203,6 +1458,129 @@ var rpcTestCases = map[string][]rpcTestCase{ fail: true, }, }, + "invokecontractverifyhistoric": { + { + name: "positive, by index", + params: fmt.Sprintf(`[20,"%s", [], [{"account":"%s"}]]`, verifyContractHash, testchain.PrivateKeyByID(0).PublicKey().GetScriptHash().StringLE()), + result: func(e *executor) interface{} { return &result.Invoke{} }, + check: func(t *testing.T, e *executor, inv interface{}) { + res, ok := inv.(*result.Invoke) + require.True(t, ok) + assert.Nil(t, res.Script) // empty witness invocation script (pushes args of `verify` on stack, but this `verify` don't have args) + assert.Equal(t, "HALT", res.State) + assert.NotEqual(t, 0, res.GasConsumed) + assert.Equal(t, true, res.Stack[0].Value().(bool), fmt.Sprintf("check address in verification_contract.go: expected %s", testchain.PrivateKeyByID(0).Address())) + }, + }, + { + name: "positive, by stateroot", + params: fmt.Sprintf(`["`+block20StateRootLE+`","%s", [], [{"account":"%s"}]]`, verifyContractHash, testchain.PrivateKeyByID(0).PublicKey().GetScriptHash().StringLE()), + result: func(e *executor) interface{} { return &result.Invoke{} }, + check: func(t *testing.T, e *executor, inv interface{}) { + res, ok := inv.(*result.Invoke) + require.True(t, ok) + assert.Nil(t, res.Script) // empty witness invocation script (pushes args of `verify` on stack, but this `verify` don't have args) + assert.Equal(t, "HALT", res.State) + assert.NotEqual(t, 0, res.GasConsumed) + assert.Equal(t, true, res.Stack[0].Value().(bool), fmt.Sprintf("check address in verification_contract.go: expected %s", testchain.PrivateKeyByID(0).Address())) + }, + }, + { + name: "positive, no signers", + params: fmt.Sprintf(`[20,"%s", []]`, verifyContractHash), + result: func(e *executor) interface{} { return &result.Invoke{} }, + check: func(t *testing.T, e *executor, inv interface{}) { + res, ok := inv.(*result.Invoke) + require.True(t, ok) + assert.Nil(t, res.Script) + assert.Equal(t, "HALT", res.State, res.FaultException) + assert.NotEqual(t, 0, res.GasConsumed) + assert.Equal(t, false, res.Stack[0].Value().(bool)) + }, + }, + { + name: "positive, no arguments", + params: fmt.Sprintf(`[20,"%s"]`, verifyContractHash), + result: func(e *executor) interface{} { return &result.Invoke{} }, + check: func(t *testing.T, e *executor, inv interface{}) { + res, ok := inv.(*result.Invoke) + require.True(t, ok) + assert.Nil(t, res.Script) + assert.Equal(t, "HALT", res.State, res.FaultException) + assert.NotEqual(t, 0, res.GasConsumed) + assert.Equal(t, false, res.Stack[0].Value().(bool)) + }, + }, + { + name: "positive, with signers and scripts", + params: fmt.Sprintf(`[20,"%s", [], [{"account":"%s", "invocation":"MQo=", "verification": ""}]]`, verifyContractHash, testchain.PrivateKeyByID(0).PublicKey().GetScriptHash().StringLE()), + result: func(e *executor) interface{} { return &result.Invoke{} }, + check: func(t *testing.T, e *executor, inv interface{}) { + res, ok := inv.(*result.Invoke) + require.True(t, ok) + assert.Nil(t, res.Script) + assert.Equal(t, "HALT", res.State) + assert.NotEqual(t, 0, res.GasConsumed) + assert.Equal(t, true, res.Stack[0].Value().(bool)) + }, + }, + { + name: "positive, with arguments, result=true", + params: fmt.Sprintf(`[20,"%s", [{"type": "String", "value": "good_string"}, {"type": "Integer", "value": "4"}, {"type":"Boolean", "value": false}]]`, verifyWithArgsContractHash), + result: func(e *executor) interface{} { return &result.Invoke{} }, + check: func(t *testing.T, e *executor, inv interface{}) { + res, ok := inv.(*result.Invoke) + require.True(t, ok) + expectedInvScript := io.NewBufBinWriter() + emit.Int(expectedInvScript.BinWriter, 0) + emit.Int(expectedInvScript.BinWriter, int64(4)) + emit.String(expectedInvScript.BinWriter, "good_string") + require.NoError(t, expectedInvScript.Err) + assert.Equal(t, expectedInvScript.Bytes(), res.Script) // witness invocation script (pushes args of `verify` on stack) + assert.Equal(t, "HALT", res.State, res.FaultException) + assert.NotEqual(t, 0, res.GasConsumed) + assert.Equal(t, true, res.Stack[0].Value().(bool)) + }, + }, + { + name: "positive, with arguments, result=false", + params: fmt.Sprintf(`[20, "%s", [{"type": "String", "value": "invalid_string"}, {"type": "Integer", "value": "4"}, {"type":"Boolean", "value": false}]]`, verifyWithArgsContractHash), + result: func(e *executor) interface{} { return &result.Invoke{} }, + check: func(t *testing.T, e *executor, inv interface{}) { + res, ok := inv.(*result.Invoke) + require.True(t, ok) + expectedInvScript := io.NewBufBinWriter() + emit.Int(expectedInvScript.BinWriter, 0) + emit.Int(expectedInvScript.BinWriter, int64(4)) + emit.String(expectedInvScript.BinWriter, "invalid_string") + require.NoError(t, expectedInvScript.Err) + assert.Equal(t, expectedInvScript.Bytes(), res.Script) + assert.Equal(t, "HALT", res.State, res.FaultException) + assert.NotEqual(t, 0, res.GasConsumed) + assert.Equal(t, false, res.Stack[0].Value().(bool)) + }, + }, + { + name: "unknown contract", + params: fmt.Sprintf(`[20, "%s", []]`, util.Uint160{}.String()), + fail: true, + }, + { + name: "no params", + params: `[]`, + fail: true, + }, + { + name: "no args", + params: `[20]`, + fail: true, + }, + { + name: "not a string", + params: `[20,42, []]`, + fail: true, + }, + }, "sendrawtransaction": { { name: "positive",