mirror of
https://github.com/nspcc-dev/neo-go.git
synced 2024-12-05 03:58:23 +00:00
503442a60d
All storage items can still be retrived via zero-length prefix.
595 lines
17 KiB
Go
595 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 {
|
|
// We only accept compressed keys here as per C# implementation.
|
|
if len(hashOrKey) != 33 {
|
|
return errors.New("bad parameter length")
|
|
}
|
|
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, nil)
|
|
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)
|
|
}
|
|
_, err := ic.dao.GetContractState(cs.ScriptHash())
|
|
if err != nil {
|
|
return fmt.Errorf("non-existent contract")
|
|
}
|
|
_, err = ic.lowerDao.GetContractState(cs.ScriptHash())
|
|
if 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
|
|
}
|