mirror of
https://github.com/nspcc-dev/neo-go.git
synced 2024-12-13 05:45:02 +00:00
b96fe8173c
The order in which storage.Find items are returns depends on what items were processed in previous transactions of the same block. The easiest way to implement this sort of caching is to cache operations with storage, flushing the only in `Persist()`.
587 lines
17 KiB
Go
587 lines
17 KiB
Go
package core
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"math"
|
|
|
|
"github.com/nspcc-dev/neo-go/pkg/core/block"
|
|
"github.com/nspcc-dev/neo-go/pkg/core/dao"
|
|
"github.com/nspcc-dev/neo-go/pkg/core/state"
|
|
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
|
|
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
|
"github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger"
|
|
"github.com/nspcc-dev/neo-go/pkg/util"
|
|
"github.com/nspcc-dev/neo-go/pkg/vm"
|
|
gherr "github.com/pkg/errors"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
const (
|
|
// MaxStorageKeyLen is the maximum length of a key for storage items.
|
|
MaxStorageKeyLen = 1024
|
|
)
|
|
|
|
// StorageContext contains storing script hash and read/write flag, it's used as
|
|
// a context for storage manipulation functions.
|
|
type StorageContext struct {
|
|
ScriptHash util.Uint160
|
|
ReadOnly bool
|
|
}
|
|
|
|
// getBlockHashFromElement converts given vm.Element to block hash using given
|
|
// Blockchainer if needed. Interop functions accept both block numbers and
|
|
// block hashes as parameters, thus this function is needed.
|
|
func getBlockHashFromElement(bc Blockchainer, element *vm.Element) (util.Uint256, error) {
|
|
var hash util.Uint256
|
|
hashbytes := element.Bytes()
|
|
if len(hashbytes) <= 5 {
|
|
hashint := element.BigInt().Int64()
|
|
if hashint < 0 || hashint > math.MaxUint32 {
|
|
return hash, errors.New("bad block index")
|
|
}
|
|
hash = bc.GetHeaderHash(int(hashint))
|
|
} else {
|
|
return util.Uint256DecodeBytesBE(hashbytes)
|
|
}
|
|
return hash, nil
|
|
}
|
|
|
|
// bcGetBlock returns current block.
|
|
func (ic *interopContext) bcGetBlock(v *vm.VM) error {
|
|
hash, err := getBlockHashFromElement(ic.bc, v.Estack().Pop())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
block, err := ic.bc.GetBlock(hash)
|
|
if err != nil {
|
|
v.Estack().PushVal([]byte{})
|
|
} else {
|
|
v.Estack().PushVal(vm.NewInteropItem(block))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// bcGetContract returns contract.
|
|
func (ic *interopContext) bcGetContract(v *vm.VM) error {
|
|
hashbytes := v.Estack().Pop().Bytes()
|
|
hash, err := util.Uint160DecodeBytesBE(hashbytes)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cs, err := ic.dao.GetContractState(hash)
|
|
if err != nil {
|
|
v.Estack().PushVal([]byte{})
|
|
} else {
|
|
v.Estack().PushVal(vm.NewInteropItem(cs))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// bcGetHeader returns block header.
|
|
func (ic *interopContext) bcGetHeader(v *vm.VM) error {
|
|
hash, err := getBlockHashFromElement(ic.bc, v.Estack().Pop())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
header, err := ic.bc.GetHeader(hash)
|
|
if err != nil {
|
|
v.Estack().PushVal([]byte{})
|
|
} else {
|
|
v.Estack().PushVal(vm.NewInteropItem(header))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// bcGetHeight returns blockchain height.
|
|
func (ic *interopContext) bcGetHeight(v *vm.VM) error {
|
|
v.Estack().PushVal(ic.bc.BlockHeight())
|
|
return nil
|
|
}
|
|
|
|
// getTransactionAndHeight gets parameter from the vm evaluation stack and
|
|
// returns transaction and its height if it's present in the blockchain.
|
|
func getTransactionAndHeight(cd *dao.Cached, v *vm.VM) (*transaction.Transaction, uint32, error) {
|
|
hashbytes := v.Estack().Pop().Bytes()
|
|
hash, err := util.Uint256DecodeBytesBE(hashbytes)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
return cd.GetTransaction(hash)
|
|
}
|
|
|
|
// bcGetTransaction returns transaction.
|
|
func (ic *interopContext) bcGetTransaction(v *vm.VM) error {
|
|
tx, _, err := getTransactionAndHeight(ic.dao, v)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
v.Estack().PushVal(vm.NewInteropItem(tx))
|
|
return nil
|
|
}
|
|
|
|
// bcGetTransactionHeight returns transaction height.
|
|
func (ic *interopContext) bcGetTransactionHeight(v *vm.VM) error {
|
|
_, h, err := getTransactionAndHeight(ic.dao, v)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
v.Estack().PushVal(h)
|
|
return nil
|
|
}
|
|
|
|
// popHeaderFromVM returns pointer to Header or error. It's main feature is
|
|
// proper treatment of Block structure, because C# code implicitly assumes
|
|
// that header APIs can also operate on blocks.
|
|
func popHeaderFromVM(v *vm.VM) (*block.Header, error) {
|
|
iface := v.Estack().Pop().Value()
|
|
header, ok := iface.(*block.Header)
|
|
if !ok {
|
|
block, ok := iface.(*block.Block)
|
|
if !ok {
|
|
return nil, errors.New("value is not a header or block")
|
|
}
|
|
return block.Header(), nil
|
|
}
|
|
return header, nil
|
|
}
|
|
|
|
// headerGetIndex returns block index from the header.
|
|
func (ic *interopContext) headerGetIndex(v *vm.VM) error {
|
|
header, err := popHeaderFromVM(v)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
v.Estack().PushVal(header.Index)
|
|
return nil
|
|
}
|
|
|
|
// headerGetHash returns header hash of the passed header.
|
|
func (ic *interopContext) headerGetHash(v *vm.VM) error {
|
|
header, err := popHeaderFromVM(v)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
v.Estack().PushVal(header.Hash().BytesBE())
|
|
return nil
|
|
}
|
|
|
|
// headerGetPrevHash returns previous header hash of the passed header.
|
|
func (ic *interopContext) headerGetPrevHash(v *vm.VM) error {
|
|
header, err := popHeaderFromVM(v)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
v.Estack().PushVal(header.PrevHash.BytesBE())
|
|
return nil
|
|
}
|
|
|
|
// headerGetTimestamp returns timestamp of the passed header.
|
|
func (ic *interopContext) headerGetTimestamp(v *vm.VM) error {
|
|
header, err := popHeaderFromVM(v)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
v.Estack().PushVal(header.Timestamp)
|
|
return nil
|
|
}
|
|
|
|
// blockGetTransactionCount returns transactions count in the given block.
|
|
func (ic *interopContext) blockGetTransactionCount(v *vm.VM) error {
|
|
blockInterface := v.Estack().Pop().Value()
|
|
block, ok := blockInterface.(*block.Block)
|
|
if !ok {
|
|
return errors.New("value is not a block")
|
|
}
|
|
v.Estack().PushVal(len(block.Transactions))
|
|
return nil
|
|
}
|
|
|
|
// blockGetTransactions returns transactions from the given block.
|
|
func (ic *interopContext) blockGetTransactions(v *vm.VM) error {
|
|
blockInterface := v.Estack().Pop().Value()
|
|
block, ok := blockInterface.(*block.Block)
|
|
if !ok {
|
|
return errors.New("value is not a block")
|
|
}
|
|
if len(block.Transactions) > vm.MaxArraySize {
|
|
return errors.New("too many transactions")
|
|
}
|
|
txes := make([]vm.StackItem, 0, len(block.Transactions))
|
|
for _, tx := range block.Transactions {
|
|
txes = append(txes, vm.NewInteropItem(tx))
|
|
}
|
|
v.Estack().PushVal(txes)
|
|
return nil
|
|
}
|
|
|
|
// blockGetTransaction returns transaction with the given number from the given
|
|
// block.
|
|
func (ic *interopContext) blockGetTransaction(v *vm.VM) error {
|
|
blockInterface := v.Estack().Pop().Value()
|
|
block, ok := blockInterface.(*block.Block)
|
|
if !ok {
|
|
return errors.New("value is not a block")
|
|
}
|
|
index := v.Estack().Pop().BigInt().Int64()
|
|
if index < 0 || index >= int64(len(block.Transactions)) {
|
|
return errors.New("wrong transaction index")
|
|
}
|
|
tx := block.Transactions[index]
|
|
v.Estack().PushVal(vm.NewInteropItem(tx))
|
|
return nil
|
|
}
|
|
|
|
// txGetHash returns transaction's hash.
|
|
func (ic *interopContext) txGetHash(v *vm.VM) error {
|
|
txInterface := v.Estack().Pop().Value()
|
|
tx, ok := txInterface.(*transaction.Transaction)
|
|
if !ok {
|
|
return errors.New("value is not a transaction")
|
|
}
|
|
v.Estack().PushVal(tx.Hash().BytesBE())
|
|
return nil
|
|
}
|
|
|
|
// engineGetScriptContainer returns transaction that contains the script being
|
|
// run.
|
|
func (ic *interopContext) engineGetScriptContainer(v *vm.VM) error {
|
|
v.Estack().PushVal(vm.NewInteropItem(ic.tx))
|
|
return nil
|
|
}
|
|
|
|
// pushContextScriptHash returns script hash of the invocation stack element
|
|
// number n.
|
|
func getContextScriptHash(v *vm.VM, n int) util.Uint160 {
|
|
ctxIface := v.Istack().Peek(n).Value()
|
|
ctx := ctxIface.(*vm.Context)
|
|
return ctx.ScriptHash()
|
|
}
|
|
|
|
// pushContextScriptHash pushes to evaluation stack the script hash of the
|
|
// invocation stack element number n.
|
|
func pushContextScriptHash(v *vm.VM, n int) error {
|
|
h := getContextScriptHash(v, n)
|
|
v.Estack().PushVal(h.BytesBE())
|
|
return nil
|
|
}
|
|
|
|
// engineGetExecutingScriptHash returns executing script hash.
|
|
func (ic *interopContext) engineGetExecutingScriptHash(v *vm.VM) error {
|
|
return pushContextScriptHash(v, 0)
|
|
}
|
|
|
|
// engineGetCallingScriptHash returns calling script hash.
|
|
func (ic *interopContext) engineGetCallingScriptHash(v *vm.VM) error {
|
|
return pushContextScriptHash(v, 1)
|
|
}
|
|
|
|
// engineGetEntryScriptHash returns entry script hash.
|
|
func (ic *interopContext) engineGetEntryScriptHash(v *vm.VM) error {
|
|
return pushContextScriptHash(v, v.Istack().Len()-1)
|
|
}
|
|
|
|
// runtimePlatform returns the name of the platform.
|
|
func (ic *interopContext) runtimePlatform(v *vm.VM) error {
|
|
v.Estack().PushVal([]byte("NEO"))
|
|
return nil
|
|
}
|
|
|
|
// runtimeGetTrigger returns the script trigger.
|
|
func (ic *interopContext) runtimeGetTrigger(v *vm.VM) error {
|
|
v.Estack().PushVal(byte(ic.trigger))
|
|
return nil
|
|
}
|
|
|
|
// checkHashedWitness checks given hash against current list of script hashes
|
|
// for verifying in the interop context.
|
|
func (ic *interopContext) checkHashedWitness(hash util.Uint160) (bool, error) {
|
|
hashes, err := ic.bc.GetScriptHashesForVerifying(ic.tx)
|
|
if err != nil {
|
|
return false, gherr.Wrap(err, "failed to get script hashes")
|
|
}
|
|
for _, v := range hashes {
|
|
if hash.Equals(v) {
|
|
return true, nil
|
|
}
|
|
}
|
|
return false, nil
|
|
}
|
|
|
|
// checkKeyedWitness checks hash of signature check contract with a given public
|
|
// key against current list of script hashes for verifying in the interop context.
|
|
func (ic *interopContext) checkKeyedWitness(key *keys.PublicKey) (bool, error) {
|
|
return ic.checkHashedWitness(key.GetScriptHash())
|
|
}
|
|
|
|
// runtimeCheckWitness checks witnesses.
|
|
func (ic *interopContext) runtimeCheckWitness(v *vm.VM) error {
|
|
var res bool
|
|
var err error
|
|
|
|
hashOrKey := v.Estack().Pop().Bytes()
|
|
hash, err := util.Uint160DecodeBytesBE(hashOrKey)
|
|
if err != nil {
|
|
key := &keys.PublicKey{}
|
|
err = key.DecodeBytes(hashOrKey)
|
|
if err != nil {
|
|
return errors.New("parameter given is neither a key nor a hash")
|
|
}
|
|
res, err = ic.checkKeyedWitness(key)
|
|
} else {
|
|
res, err = ic.checkHashedWitness(hash)
|
|
}
|
|
if err != nil {
|
|
return gherr.Wrap(err, "failed to check")
|
|
}
|
|
v.Estack().PushVal(res)
|
|
return nil
|
|
}
|
|
|
|
// runtimeNotify should pass stack item to the notify plugin to handle it, but
|
|
// in neo-go the only meaningful thing to do here is to log.
|
|
func (ic *interopContext) runtimeNotify(v *vm.VM) error {
|
|
// It can be just about anything.
|
|
e := v.Estack().Pop()
|
|
item := e.Item()
|
|
// But it has to be serializable, otherwise we either have some broken
|
|
// (recursive) structure inside or an interop item that can't be used
|
|
// outside of the interop subsystem anyway. I'd probably fail transactions
|
|
// that emit such broken notifications, but that might break compatibility
|
|
// with testnet/mainnet, so we're replacing these with error messages.
|
|
_, err := vm.SerializeItem(item)
|
|
if err != nil {
|
|
item = vm.NewByteArrayItem([]byte(fmt.Sprintf("bad notification: %v", err)))
|
|
}
|
|
ne := state.NotificationEvent{ScriptHash: getContextScriptHash(v, 0), Item: item}
|
|
ic.notifications = append(ic.notifications, ne)
|
|
return nil
|
|
}
|
|
|
|
// runtimeLog logs the message passed.
|
|
func (ic *interopContext) runtimeLog(v *vm.VM) error {
|
|
msg := fmt.Sprintf("%q", v.Estack().Pop().Bytes())
|
|
ic.log.Info("runtime log",
|
|
zap.Stringer("script", getContextScriptHash(v, 0)),
|
|
zap.String("logs", msg))
|
|
return nil
|
|
}
|
|
|
|
// runtimeGetTime returns timestamp of the block being verified, or the latest
|
|
// one in the blockchain if no block is given to interopContext.
|
|
func (ic *interopContext) runtimeGetTime(v *vm.VM) error {
|
|
var header *block.Header
|
|
if ic.block == nil {
|
|
var err error
|
|
header, err = ic.bc.GetHeader(ic.bc.CurrentBlockHash())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
header = ic.block.Header()
|
|
}
|
|
v.Estack().PushVal(header.Timestamp)
|
|
return nil
|
|
}
|
|
|
|
/*
|
|
// runtimeSerialize serializes given stack item.
|
|
func (ic *interopContext) runtimeSerialize(v *vm.VM) error {
|
|
panic("TODO")
|
|
}
|
|
|
|
// runtimeDeserialize deserializes given stack item.
|
|
func (ic *interopContext) runtimeDeserialize(v *vm.VM) error {
|
|
panic("TODO")
|
|
}
|
|
*/
|
|
func (ic *interopContext) checkStorageContext(stc *StorageContext) error {
|
|
contract, err := ic.dao.GetContractState(stc.ScriptHash)
|
|
if err != nil {
|
|
return errors.New("no contract found")
|
|
}
|
|
if !contract.HasStorage() {
|
|
return fmt.Errorf("contract %s can't use storage", stc.ScriptHash)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// storageDelete deletes stored key-value pair.
|
|
func (ic *interopContext) storageDelete(v *vm.VM) error {
|
|
if ic.trigger != trigger.Application && ic.trigger != trigger.ApplicationR {
|
|
return errors.New("can't delete when the trigger is not application")
|
|
}
|
|
stcInterface := v.Estack().Pop().Value()
|
|
stc, ok := stcInterface.(*StorageContext)
|
|
if !ok {
|
|
return fmt.Errorf("%T is not a StorageContext", stcInterface)
|
|
}
|
|
if stc.ReadOnly {
|
|
return errors.New("StorageContext is read only")
|
|
}
|
|
err := ic.checkStorageContext(stc)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
key := v.Estack().Pop().Bytes()
|
|
si := ic.dao.GetStorageItem(stc.ScriptHash, key)
|
|
if si != nil && si.IsConst {
|
|
return errors.New("storage item is constant")
|
|
}
|
|
return ic.dao.DeleteStorageItem(stc.ScriptHash, key)
|
|
}
|
|
|
|
// storageGet returns stored key-value pair.
|
|
func (ic *interopContext) storageGet(v *vm.VM) error {
|
|
stcInterface := v.Estack().Pop().Value()
|
|
stc, ok := stcInterface.(*StorageContext)
|
|
if !ok {
|
|
return fmt.Errorf("%T is not a StorageContext", stcInterface)
|
|
}
|
|
err := ic.checkStorageContext(stc)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
key := v.Estack().Pop().Bytes()
|
|
si := ic.dao.GetStorageItem(stc.ScriptHash, key)
|
|
if si != nil && si.Value != nil {
|
|
v.Estack().PushVal(si.Value)
|
|
} else {
|
|
v.Estack().PushVal([]byte{})
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// storageGetContext returns storage context (scripthash).
|
|
func (ic *interopContext) storageGetContext(v *vm.VM) error {
|
|
sc := &StorageContext{
|
|
ScriptHash: getContextScriptHash(v, 0),
|
|
ReadOnly: false,
|
|
}
|
|
v.Estack().PushVal(vm.NewInteropItem(sc))
|
|
return nil
|
|
}
|
|
|
|
// storageGetReadOnlyContext returns read-only context (scripthash).
|
|
func (ic *interopContext) storageGetReadOnlyContext(v *vm.VM) error {
|
|
sc := &StorageContext{
|
|
ScriptHash: getContextScriptHash(v, 0),
|
|
ReadOnly: true,
|
|
}
|
|
v.Estack().PushVal(vm.NewInteropItem(sc))
|
|
return nil
|
|
}
|
|
|
|
func (ic *interopContext) putWithContextAndFlags(stc *StorageContext, key []byte, value []byte, isConst bool) error {
|
|
if ic.trigger != trigger.Application && ic.trigger != trigger.ApplicationR {
|
|
return errors.New("can't delete when the trigger is not application")
|
|
}
|
|
if len(key) > MaxStorageKeyLen {
|
|
return errors.New("key is too big")
|
|
}
|
|
if stc.ReadOnly {
|
|
return errors.New("StorageContext is read only")
|
|
}
|
|
err := ic.checkStorageContext(stc)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
si := ic.dao.GetStorageItem(stc.ScriptHash, key)
|
|
if si == nil {
|
|
si = &state.StorageItem{}
|
|
}
|
|
if si.IsConst {
|
|
return errors.New("storage item exists and is read-only")
|
|
}
|
|
si.Value = value
|
|
si.IsConst = isConst
|
|
return ic.dao.PutStorageItem(stc.ScriptHash, key, si)
|
|
}
|
|
|
|
// storagePutInternal is a unified implementation of storagePut and storagePutEx.
|
|
func (ic *interopContext) storagePutInternal(v *vm.VM, getFlag bool) error {
|
|
stcInterface := v.Estack().Pop().Value()
|
|
stc, ok := stcInterface.(*StorageContext)
|
|
if !ok {
|
|
return fmt.Errorf("%T is not a StorageContext", stcInterface)
|
|
}
|
|
key := v.Estack().Pop().Bytes()
|
|
value := v.Estack().Pop().Bytes()
|
|
var flag int
|
|
if getFlag {
|
|
flag = int(v.Estack().Pop().BigInt().Int64())
|
|
}
|
|
return ic.putWithContextAndFlags(stc, key, value, flag == 1)
|
|
}
|
|
|
|
// storagePut puts key-value pair into the storage.
|
|
func (ic *interopContext) storagePut(v *vm.VM) error {
|
|
return ic.storagePutInternal(v, false)
|
|
}
|
|
|
|
// storagePutEx puts key-value pair with given flags into the storage.
|
|
func (ic *interopContext) storagePutEx(v *vm.VM) error {
|
|
return ic.storagePutInternal(v, true)
|
|
}
|
|
|
|
// storageContextAsReadOnly sets given context to read-only mode.
|
|
func (ic *interopContext) storageContextAsReadOnly(v *vm.VM) error {
|
|
stcInterface := v.Estack().Pop().Value()
|
|
stc, ok := stcInterface.(*StorageContext)
|
|
if !ok {
|
|
return fmt.Errorf("%T is not a StorageContext", stcInterface)
|
|
}
|
|
if !stc.ReadOnly {
|
|
stx := &StorageContext{
|
|
ScriptHash: stc.ScriptHash,
|
|
ReadOnly: true,
|
|
}
|
|
stc = stx
|
|
}
|
|
v.Estack().PushVal(vm.NewInteropItem(stc))
|
|
return nil
|
|
}
|
|
|
|
// contractDestroy destroys a contract.
|
|
func (ic *interopContext) contractDestroy(v *vm.VM) error {
|
|
if ic.trigger != trigger.Application {
|
|
return errors.New("can't destroy contract when not triggered by application")
|
|
}
|
|
hash := getContextScriptHash(v, 0)
|
|
cs, err := ic.dao.GetContractState(hash)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
err = ic.dao.DeleteContractState(hash)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if cs.HasStorage() {
|
|
siMap, err := ic.dao.GetStorageItems(hash)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for i := range siMap {
|
|
_ = ic.dao.DeleteStorageItem(hash, siMap[i].Key)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// contractGetStorageContext retrieves StorageContext of a contract.
|
|
func (ic *interopContext) contractGetStorageContext(v *vm.VM) error {
|
|
csInterface := v.Estack().Pop().Value()
|
|
cs, ok := csInterface.(*state.Contract)
|
|
if !ok {
|
|
return fmt.Errorf("%T is not a contract state", cs)
|
|
}
|
|
contractState, err := ic.dao.GetContractState(cs.ScriptHash())
|
|
if contractState == nil || err != nil {
|
|
return fmt.Errorf("contract was not created in this transaction")
|
|
}
|
|
stc := &StorageContext{
|
|
ScriptHash: cs.ScriptHash(),
|
|
}
|
|
v.Estack().PushVal(vm.NewInteropItem(stc))
|
|
return nil
|
|
}
|