core, rpc: support [invokefunction, invokescript, invokecontractverify]historic

This commit is contained in:
Anna Shaleva 2022-04-07 18:13:08 +03:00
parent f8b5972f61
commit 63c26ca270
10 changed files with 793 additions and 67 deletions

View file

@ -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

View file

@ -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")

View file

@ -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

View file

@ -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)
}

View file

@ -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)

View file

@ -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)
}
}

View file

@ -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)

View file

@ -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)

View file

@ -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"
@ -140,8 +141,11 @@ var rpcHandlers = map[string]func(*Server, request.Params) (interface{}, *respon
"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,
@ -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 {
return nil, response.ErrInvalidParams
tx, verbose, respErr := s.getInvokeScriptParams(reqParams)
if respErr != nil {
return nil, respErr
}
return s.runScriptInVM(trigger.Application, tx.Script, util.Uint160{}, tx, nil, verbose)
}
script, err := reqParams[0].GetBytesBase64()
if err != nil {
// 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)
}
func (s *Server) getInvokeScriptParams(reqParams request.Params) (*transaction.Transaction, bool, *response.Error) {
script, err := reqParams.Value(0).GetBytesBase64()
if err != nil {
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()
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)
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)
}
}
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)
}

View file

@ -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",