From a7457d08a18f0ba2bd5feb71bfa3a5fe4c4bdbe3 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Wed, 18 Dec 2019 19:49:56 +0300 Subject: [PATCH] vm/core: add ID support for SYSCALL, redo interop registration This solves two problems: * adds support for shortened SYSCALL form that uses IDs (similar to #434, but for NEO 2.0, supporting both forms), which is important for compatibility with C# node and mainnet chain that uses it from some height * reworks interop plugging to use callbacks rather than appending to the map, these map mangling functions are clearly visible in the VM profiling statistics and we want spawning a VM to be fast, so it makes sense optimizing it. This change moves most of the work to the init() phase making VM setup cheaper. Caveats: * InteropNameToID accepts `[]byte` because that's the thing we have in SYSCALL processing and that's the most often usecase for it, it leads to some conversions in other places but that's acceptable because those are either tests or init() * three getInterop functions are: `getDefaultVMInterop`, `getSystemInterop` and `getNeoInterop` Our 100K (1.4M->1.5M) block import time improves by ~4% with this change. --- pkg/core/blockchain.go | 4 +- pkg/core/interops.go | 417 ++++++++++++++++++++++------------------ pkg/vm/interop.go | 57 ++++++ pkg/vm/json_test.go | 23 ++- pkg/vm/tests/vm_test.go | 25 ++- pkg/vm/vm.go | 64 +++--- pkg/vm/vm_test.go | 41 +++- 7 files changed, 384 insertions(+), 247 deletions(-) diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index 20b6ca120..18f589832 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -1342,8 +1342,8 @@ func (bc *Blockchain) spawnVMWithInterops(interopCtx *interopContext) *vm.VM { } return cs.Script }) - vm.RegisterInteropFuncs(interopCtx.getSystemInteropMap()) - vm.RegisterInteropFuncs(interopCtx.getNeoInteropMap()) + vm.RegisterInteropGetter(interopCtx.getSystemInterop) + vm.RegisterInteropGetter(interopCtx.getNeoInterop) return vm } diff --git a/pkg/core/interops.go b/pkg/core/interops.go index f249e0299..d8413774d 100644 --- a/pkg/core/interops.go +++ b/pkg/core/interops.go @@ -8,6 +8,8 @@ package core */ import ( + "sort" + "github.com/CityOfZion/neo-go/pkg/core/state" "github.com/CityOfZion/neo-go/pkg/core/storage" "github.com/CityOfZion/neo-go/pkg/core/transaction" @@ -29,193 +31,240 @@ func newInteropContext(trigger byte, bc Blockchainer, s storage.Store, block *Bl return &interopContext{bc, trigger, block, tx, dao, nes} } +// interopedFunction binds function name, id with the function itself and price, +// it's supposed to be inited once for all interopContexts, so it doesn't use +// vm.InteropFuncPrice directly. +type interopedFunction struct { + ID uint32 + Name string + Func func(*interopContext, *vm.VM) error + Price int +} + +// getSystemInterop returns matching interop function from the System namespace +// for a given id in the current context. +func (ic *interopContext) getSystemInterop(id uint32) *vm.InteropFuncPrice { + return ic.getInteropFromSlice(id, systemInterops) +} + +// getNeoInterop returns matching interop function from the Neo and AntShares +// namespaces for a given id in the current context. +func (ic *interopContext) getNeoInterop(id uint32) *vm.InteropFuncPrice { + return ic.getInteropFromSlice(id, neoInterops) +} + +// getInteropFromSlice returns matching interop function from the given slice of +// interop functions in the current context. +func (ic *interopContext) getInteropFromSlice(id uint32, slice []interopedFunction) *vm.InteropFuncPrice { + n := sort.Search(len(slice), func(i int) bool { + return slice[i].ID >= id + }) + if n < len(slice) && slice[n].ID == id { + // Currying, yay! + return &vm.InteropFuncPrice{Func: func(v *vm.VM) error { + return slice[n].Func(ic, v) + }, Price: slice[n].Price} + } + return nil +} + // All lists are sorted, keep 'em this way, please. - -// getSystemInteropMap returns interop mappings for System namespace. -func (ic *interopContext) getSystemInteropMap() map[string]vm.InteropFuncPrice { - return map[string]vm.InteropFuncPrice{ - "System.Block.GetTransaction": {Func: ic.blockGetTransaction, Price: 1}, - "System.Block.GetTransactionCount": {Func: ic.blockGetTransactionCount, Price: 1}, - "System.Block.GetTransactions": {Func: ic.blockGetTransactions, Price: 1}, - "System.Blockchain.GetBlock": {Func: ic.bcGetBlock, Price: 200}, - "System.Blockchain.GetContract": {Func: ic.bcGetContract, Price: 100}, - "System.Blockchain.GetHeader": {Func: ic.bcGetHeader, Price: 100}, - "System.Blockchain.GetHeight": {Func: ic.bcGetHeight, Price: 1}, - "System.Blockchain.GetTransaction": {Func: ic.bcGetTransaction, Price: 200}, - "System.Blockchain.GetTransactionHeight": {Func: ic.bcGetTransactionHeight, Price: 100}, - "System.Contract.Destroy": {Func: ic.contractDestroy, Price: 1}, - "System.Contract.GetStorageContext": {Func: ic.contractGetStorageContext, Price: 1}, - "System.ExecutionEngine.GetCallingScriptHash": {Func: ic.engineGetCallingScriptHash, Price: 1}, - "System.ExecutionEngine.GetEntryScriptHash": {Func: ic.engineGetEntryScriptHash, Price: 1}, - "System.ExecutionEngine.GetExecutingScriptHash": {Func: ic.engineGetExecutingScriptHash, Price: 1}, - "System.ExecutionEngine.GetScriptContainer": {Func: ic.engineGetScriptContainer, Price: 1}, - "System.Header.GetHash": {Func: ic.headerGetHash, Price: 1}, - "System.Header.GetIndex": {Func: ic.headerGetIndex, Price: 1}, - "System.Header.GetPrevHash": {Func: ic.headerGetPrevHash, Price: 1}, - "System.Header.GetTimestamp": {Func: ic.headerGetTimestamp, Price: 1}, - "System.Runtime.CheckWitness": {Func: ic.runtimeCheckWitness, Price: 200}, - "System.Runtime.GetTime": {Func: ic.runtimeGetTime, Price: 1}, - "System.Runtime.GetTrigger": {Func: ic.runtimeGetTrigger, Price: 1}, - "System.Runtime.Log": {Func: ic.runtimeLog, Price: 1}, - "System.Runtime.Notify": {Func: ic.runtimeNotify, Price: 1}, - "System.Runtime.Platform": {Func: ic.runtimePlatform, Price: 1}, - "System.Storage.Delete": {Func: ic.storageDelete, Price: 100}, - "System.Storage.Get": {Func: ic.storageGet, Price: 100}, - "System.Storage.GetContext": {Func: ic.storageGetContext, Price: 1}, - "System.Storage.GetReadOnlyContext": {Func: ic.storageGetReadOnlyContext, Price: 1}, - "System.Storage.Put": {Func: ic.storagePut, Price: 0}, // These don't have static price in C# code. - "System.Storage.PutEx": {Func: ic.storagePutEx, Price: 0}, - "System.StorageContext.AsReadOnly": {Func: ic.storageContextAsReadOnly, Price: 1}, - "System.Transaction.GetHash": {Func: ic.txGetHash, Price: 1}, - "System.Runtime.Deserialize": {Func: ic.runtimeDeserialize, Price: 1}, - "System.Runtime.Serialize": {Func: ic.runtimeSerialize, Price: 1}, - } +var systemInterops = []interopedFunction{ + {Name: "System.Block.GetTransaction", Func: (*interopContext).blockGetTransaction, Price: 1}, + {Name: "System.Block.GetTransactionCount", Func: (*interopContext).blockGetTransactionCount, Price: 1}, + {Name: "System.Block.GetTransactions", Func: (*interopContext).blockGetTransactions, Price: 1}, + {Name: "System.Blockchain.GetBlock", Func: (*interopContext).bcGetBlock, Price: 200}, + {Name: "System.Blockchain.GetContract", Func: (*interopContext).bcGetContract, Price: 100}, + {Name: "System.Blockchain.GetHeader", Func: (*interopContext).bcGetHeader, Price: 100}, + {Name: "System.Blockchain.GetHeight", Func: (*interopContext).bcGetHeight, Price: 1}, + {Name: "System.Blockchain.GetTransaction", Func: (*interopContext).bcGetTransaction, Price: 200}, + {Name: "System.Blockchain.GetTransactionHeight", Func: (*interopContext).bcGetTransactionHeight, Price: 100}, + {Name: "System.Contract.Destroy", Func: (*interopContext).contractDestroy, Price: 1}, + {Name: "System.Contract.GetStorageContext", Func: (*interopContext).contractGetStorageContext, Price: 1}, + {Name: "System.ExecutionEngine.GetCallingScriptHash", Func: (*interopContext).engineGetCallingScriptHash, Price: 1}, + {Name: "System.ExecutionEngine.GetEntryScriptHash", Func: (*interopContext).engineGetEntryScriptHash, Price: 1}, + {Name: "System.ExecutionEngine.GetExecutingScriptHash", Func: (*interopContext).engineGetExecutingScriptHash, Price: 1}, + {Name: "System.ExecutionEngine.GetScriptContainer", Func: (*interopContext).engineGetScriptContainer, Price: 1}, + {Name: "System.Header.GetHash", Func: (*interopContext).headerGetHash, Price: 1}, + {Name: "System.Header.GetIndex", Func: (*interopContext).headerGetIndex, Price: 1}, + {Name: "System.Header.GetPrevHash", Func: (*interopContext).headerGetPrevHash, Price: 1}, + {Name: "System.Header.GetTimestamp", Func: (*interopContext).headerGetTimestamp, Price: 1}, + {Name: "System.Runtime.CheckWitness", Func: (*interopContext).runtimeCheckWitness, Price: 200}, + {Name: "System.Runtime.Deserialize", Func: (*interopContext).runtimeDeserialize, Price: 1}, + {Name: "System.Runtime.GetTime", Func: (*interopContext).runtimeGetTime, Price: 1}, + {Name: "System.Runtime.GetTrigger", Func: (*interopContext).runtimeGetTrigger, Price: 1}, + {Name: "System.Runtime.Log", Func: (*interopContext).runtimeLog, Price: 1}, + {Name: "System.Runtime.Notify", Func: (*interopContext).runtimeNotify, Price: 1}, + {Name: "System.Runtime.Platform", Func: (*interopContext).runtimePlatform, Price: 1}, + {Name: "System.Runtime.Serialize", Func: (*interopContext).runtimeSerialize, Price: 1}, + {Name: "System.Storage.Delete", Func: (*interopContext).storageDelete, Price: 100}, + {Name: "System.Storage.Get", Func: (*interopContext).storageGet, Price: 100}, + {Name: "System.Storage.GetContext", Func: (*interopContext).storageGetContext, Price: 1}, + {Name: "System.Storage.GetReadOnlyContext", Func: (*interopContext).storageGetReadOnlyContext, Price: 1}, + {Name: "System.Storage.Put", Func: (*interopContext).storagePut, Price: 0}, // These don't have static price in C# code. + {Name: "System.Storage.PutEx", Func: (*interopContext).storagePutEx, Price: 0}, + {Name: "System.StorageContext.AsReadOnly", Func: (*interopContext).storageContextAsReadOnly, Price: 1}, + {Name: "System.Transaction.GetHash", Func: (*interopContext).txGetHash, Price: 1}, } -// getSystemInteropMap returns interop mappings for Neo and (legacy) AntShares namespaces. -func (ic *interopContext) getNeoInteropMap() map[string]vm.InteropFuncPrice { - return map[string]vm.InteropFuncPrice{ - "Neo.Account.GetBalance": {Func: ic.accountGetBalance, Price: 1}, - "Neo.Account.GetScriptHash": {Func: ic.accountGetScriptHash, Price: 1}, - "Neo.Account.GetVotes": {Func: ic.accountGetVotes, Price: 1}, - "Neo.Account.IsStandard": {Func: ic.accountIsStandard, Price: 100}, - "Neo.Asset.Create": {Func: ic.assetCreate, Price: 0}, - "Neo.Asset.GetAdmin": {Func: ic.assetGetAdmin, Price: 1}, - "Neo.Asset.GetAmount": {Func: ic.assetGetAmount, Price: 1}, - "Neo.Asset.GetAssetId": {Func: ic.assetGetAssetID, Price: 1}, - "Neo.Asset.GetAssetType": {Func: ic.assetGetAssetType, Price: 1}, - "Neo.Asset.GetAvailable": {Func: ic.assetGetAvailable, Price: 1}, - "Neo.Asset.GetIssuer": {Func: ic.assetGetIssuer, Price: 1}, - "Neo.Asset.GetOwner": {Func: ic.assetGetOwner, Price: 1}, - "Neo.Asset.GetPrecision": {Func: ic.assetGetPrecision, Price: 1}, - "Neo.Asset.Renew": {Func: ic.assetRenew, Price: 0}, - "Neo.Attribute.GetData": {Func: ic.attrGetData, Price: 1}, - "Neo.Attribute.GetUsage": {Func: ic.attrGetUsage, Price: 1}, - "Neo.Block.GetTransaction": {Func: ic.blockGetTransaction, Price: 1}, - "Neo.Block.GetTransactionCount": {Func: ic.blockGetTransactionCount, Price: 1}, - "Neo.Block.GetTransactions": {Func: ic.blockGetTransactions, Price: 1}, - "Neo.Blockchain.GetAccount": {Func: ic.bcGetAccount, Price: 100}, - "Neo.Blockchain.GetAsset": {Func: ic.bcGetAsset, Price: 100}, - "Neo.Blockchain.GetBlock": {Func: ic.bcGetBlock, Price: 200}, - "Neo.Blockchain.GetContract": {Func: ic.bcGetContract, Price: 100}, - "Neo.Blockchain.GetHeader": {Func: ic.bcGetHeader, Price: 100}, - "Neo.Blockchain.GetHeight": {Func: ic.bcGetHeight, Price: 1}, - "Neo.Blockchain.GetTransaction": {Func: ic.bcGetTransaction, Price: 100}, - "Neo.Blockchain.GetTransactionHeight": {Func: ic.bcGetTransactionHeight, Price: 100}, - "Neo.Blockchain.GetValidators": {Func: ic.bcGetValidators, Price: 200}, - "Neo.Contract.Create": {Func: ic.contractCreate, Price: 0}, - "Neo.Contract.Destroy": {Func: ic.contractDestroy, Price: 1}, - "Neo.Contract.GetScript": {Func: ic.contractGetScript, Price: 1}, - "Neo.Contract.GetStorageContext": {Func: ic.contractGetStorageContext, Price: 1}, - "Neo.Contract.IsPayable": {Func: ic.contractIsPayable, Price: 1}, - "Neo.Contract.Migrate": {Func: ic.contractMigrate, Price: 0}, - "Neo.Header.GetConsensusData": {Func: ic.headerGetConsensusData, Price: 1}, - "Neo.Header.GetHash": {Func: ic.headerGetHash, Price: 1}, - "Neo.Header.GetIndex": {Func: ic.headerGetIndex, Price: 1}, - "Neo.Header.GetMerkleRoot": {Func: ic.headerGetMerkleRoot, Price: 1}, - "Neo.Header.GetNextConsensus": {Func: ic.headerGetNextConsensus, Price: 1}, - "Neo.Header.GetPrevHash": {Func: ic.headerGetPrevHash, Price: 1}, - "Neo.Header.GetTimestamp": {Func: ic.headerGetTimestamp, Price: 1}, - "Neo.Header.GetVersion": {Func: ic.headerGetVersion, Price: 1}, - "Neo.Input.GetHash": {Func: ic.inputGetHash, Price: 1}, - "Neo.Input.GetIndex": {Func: ic.inputGetIndex, Price: 1}, - "Neo.Output.GetAssetId": {Func: ic.outputGetAssetID, Price: 1}, - "Neo.Output.GetScriptHash": {Func: ic.outputGetScriptHash, Price: 1}, - "Neo.Output.GetValue": {Func: ic.outputGetValue, Price: 1}, - "Neo.Runtime.CheckWitness": {Func: ic.runtimeCheckWitness, Price: 200}, - "Neo.Runtime.GetTime": {Func: ic.runtimeGetTime, Price: 1}, - "Neo.Runtime.GetTrigger": {Func: ic.runtimeGetTrigger, Price: 1}, - "Neo.Runtime.Log": {Func: ic.runtimeLog, Price: 1}, - "Neo.Runtime.Notify": {Func: ic.runtimeNotify, Price: 1}, - "Neo.Storage.Delete": {Func: ic.storageDelete, Price: 100}, - "Neo.Storage.Get": {Func: ic.storageGet, Price: 100}, - "Neo.Storage.GetContext": {Func: ic.storageGetContext, Price: 1}, - "Neo.Storage.GetReadOnlyContext": {Func: ic.storageGetReadOnlyContext, Price: 1}, - "Neo.Storage.Put": {Func: ic.storagePut, Price: 0}, - "Neo.StorageContext.AsReadOnly": {Func: ic.storageContextAsReadOnly, Price: 1}, - "Neo.Transaction.GetAttributes": {Func: ic.txGetAttributes, Price: 1}, - "Neo.Transaction.GetHash": {Func: ic.txGetHash, Price: 1}, - "Neo.Transaction.GetInputs": {Func: ic.txGetInputs, Price: 1}, - "Neo.Transaction.GetOutputs": {Func: ic.txGetOutputs, Price: 1}, - "Neo.Transaction.GetReferences": {Func: ic.txGetReferences, Price: 200}, - "Neo.Transaction.GetType": {Func: ic.txGetType, Price: 1}, - "Neo.Transaction.GetUnspentCoins": {Func: ic.txGetUnspentCoins, Price: 200}, - "Neo.Transaction.GetWitnesses": {Func: ic.txGetWitnesses, Price: 200}, - // "Neo.Enumerator.Concat": {Func: ic.enumeratorConcat, Price: 1}, - // "Neo.Enumerator.Create": {Func: ic.enumeratorCreate, Price: 1}, - // "Neo.Enumerator.Next": {Func: ic.enumeratorNext, Price: 1}, - // "Neo.Enumerator.Value": {Func: ic.enumeratorValue, Price: 1}, - // "Neo.InvocationTransaction.GetScript": {ic.invocationTx_GetScript, 1}, - // "Neo.Iterator.Concat": {Func: ic.iteratorConcat, Price: 1}, - // "Neo.Iterator.Create": {Func: ic.iteratorCreate, Price: 1}, - // "Neo.Iterator.Key": {Func: ic.iteratorKey, Price: 1}, - // "Neo.Iterator.Keys": {Func: ic.iteratorKeys, Price: 1}, - // "Neo.Iterator.Values": {Func: ic.iteratorValues, Price: 1}, - "Neo.Runtime.Deserialize": {Func: ic.runtimeDeserialize, Price: 1}, - "Neo.Runtime.Serialize": {Func: ic.runtimeSerialize, Price: 1}, - // "Neo.Storage.Find": {Func: ic.storageFind, Price: 1}, - // "Neo.Witness.GetVerificationScript": {Func: ic.witnessGetVerificationScript, Price: 100}, +var neoInterops = []interopedFunction{ + {Name: "Neo.Account.GetBalance", Func: (*interopContext).accountGetBalance, Price: 1}, + {Name: "Neo.Account.GetScriptHash", Func: (*interopContext).accountGetScriptHash, Price: 1}, + {Name: "Neo.Account.GetVotes", Func: (*interopContext).accountGetVotes, Price: 1}, + {Name: "Neo.Account.IsStandard", Func: (*interopContext).accountIsStandard, Price: 100}, + {Name: "Neo.Asset.Create", Func: (*interopContext).assetCreate, Price: 0}, + {Name: "Neo.Asset.GetAdmin", Func: (*interopContext).assetGetAdmin, Price: 1}, + {Name: "Neo.Asset.GetAmount", Func: (*interopContext).assetGetAmount, Price: 1}, + {Name: "Neo.Asset.GetAssetId", Func: (*interopContext).assetGetAssetID, Price: 1}, + {Name: "Neo.Asset.GetAssetType", Func: (*interopContext).assetGetAssetType, Price: 1}, + {Name: "Neo.Asset.GetAvailable", Func: (*interopContext).assetGetAvailable, Price: 1}, + {Name: "Neo.Asset.GetIssuer", Func: (*interopContext).assetGetIssuer, Price: 1}, + {Name: "Neo.Asset.GetOwner", Func: (*interopContext).assetGetOwner, Price: 1}, + {Name: "Neo.Asset.GetPrecision", Func: (*interopContext).assetGetPrecision, Price: 1}, + {Name: "Neo.Asset.Renew", Func: (*interopContext).assetRenew, Price: 0}, + {Name: "Neo.Attribute.GetData", Func: (*interopContext).attrGetData, Price: 1}, + {Name: "Neo.Attribute.GetUsage", Func: (*interopContext).attrGetUsage, Price: 1}, + {Name: "Neo.Block.GetTransaction", Func: (*interopContext).blockGetTransaction, Price: 1}, + {Name: "Neo.Block.GetTransactionCount", Func: (*interopContext).blockGetTransactionCount, Price: 1}, + {Name: "Neo.Block.GetTransactions", Func: (*interopContext).blockGetTransactions, Price: 1}, + {Name: "Neo.Blockchain.GetAccount", Func: (*interopContext).bcGetAccount, Price: 100}, + {Name: "Neo.Blockchain.GetAsset", Func: (*interopContext).bcGetAsset, Price: 100}, + {Name: "Neo.Blockchain.GetBlock", Func: (*interopContext).bcGetBlock, Price: 200}, + {Name: "Neo.Blockchain.GetContract", Func: (*interopContext).bcGetContract, Price: 100}, + {Name: "Neo.Blockchain.GetHeader", Func: (*interopContext).bcGetHeader, Price: 100}, + {Name: "Neo.Blockchain.GetHeight", Func: (*interopContext).bcGetHeight, Price: 1}, + {Name: "Neo.Blockchain.GetTransaction", Func: (*interopContext).bcGetTransaction, Price: 100}, + {Name: "Neo.Blockchain.GetTransactionHeight", Func: (*interopContext).bcGetTransactionHeight, Price: 100}, + {Name: "Neo.Blockchain.GetValidators", Func: (*interopContext).bcGetValidators, Price: 200}, + {Name: "Neo.Contract.Create", Func: (*interopContext).contractCreate, Price: 0}, + {Name: "Neo.Contract.Destroy", Func: (*interopContext).contractDestroy, Price: 1}, + {Name: "Neo.Contract.GetScript", Func: (*interopContext).contractGetScript, Price: 1}, + {Name: "Neo.Contract.GetStorageContext", Func: (*interopContext).contractGetStorageContext, Price: 1}, + {Name: "Neo.Contract.IsPayable", Func: (*interopContext).contractIsPayable, Price: 1}, + {Name: "Neo.Contract.Migrate", Func: (*interopContext).contractMigrate, Price: 0}, + {Name: "Neo.Header.GetConsensusData", Func: (*interopContext).headerGetConsensusData, Price: 1}, + {Name: "Neo.Header.GetHash", Func: (*interopContext).headerGetHash, Price: 1}, + {Name: "Neo.Header.GetIndex", Func: (*interopContext).headerGetIndex, Price: 1}, + {Name: "Neo.Header.GetMerkleRoot", Func: (*interopContext).headerGetMerkleRoot, Price: 1}, + {Name: "Neo.Header.GetNextConsensus", Func: (*interopContext).headerGetNextConsensus, Price: 1}, + {Name: "Neo.Header.GetPrevHash", Func: (*interopContext).headerGetPrevHash, Price: 1}, + {Name: "Neo.Header.GetTimestamp", Func: (*interopContext).headerGetTimestamp, Price: 1}, + {Name: "Neo.Header.GetVersion", Func: (*interopContext).headerGetVersion, Price: 1}, + {Name: "Neo.Input.GetHash", Func: (*interopContext).inputGetHash, Price: 1}, + {Name: "Neo.Input.GetIndex", Func: (*interopContext).inputGetIndex, Price: 1}, + {Name: "Neo.Output.GetAssetId", Func: (*interopContext).outputGetAssetID, Price: 1}, + {Name: "Neo.Output.GetScriptHash", Func: (*interopContext).outputGetScriptHash, Price: 1}, + {Name: "Neo.Output.GetValue", Func: (*interopContext).outputGetValue, Price: 1}, + {Name: "Neo.Runtime.CheckWitness", Func: (*interopContext).runtimeCheckWitness, Price: 200}, + {Name: "Neo.Runtime.GetTime", Func: (*interopContext).runtimeGetTime, Price: 1}, + {Name: "Neo.Runtime.GetTrigger", Func: (*interopContext).runtimeGetTrigger, Price: 1}, + {Name: "Neo.Runtime.Log", Func: (*interopContext).runtimeLog, Price: 1}, + {Name: "Neo.Runtime.Notify", Func: (*interopContext).runtimeNotify, Price: 1}, + {Name: "Neo.Storage.Delete", Func: (*interopContext).storageDelete, Price: 100}, + {Name: "Neo.Storage.Get", Func: (*interopContext).storageGet, Price: 100}, + {Name: "Neo.Storage.GetContext", Func: (*interopContext).storageGetContext, Price: 1}, + {Name: "Neo.Storage.GetReadOnlyContext", Func: (*interopContext).storageGetReadOnlyContext, Price: 1}, + {Name: "Neo.Storage.Put", Func: (*interopContext).storagePut, Price: 0}, + {Name: "Neo.StorageContext.AsReadOnly", Func: (*interopContext).storageContextAsReadOnly, Price: 1}, + {Name: "Neo.Transaction.GetAttributes", Func: (*interopContext).txGetAttributes, Price: 1}, + {Name: "Neo.Transaction.GetHash", Func: (*interopContext).txGetHash, Price: 1}, + {Name: "Neo.Transaction.GetInputs", Func: (*interopContext).txGetInputs, Price: 1}, + {Name: "Neo.Transaction.GetOutputs", Func: (*interopContext).txGetOutputs, Price: 1}, + {Name: "Neo.Transaction.GetReferences", Func: (*interopContext).txGetReferences, Price: 200}, + {Name: "Neo.Transaction.GetType", Func: (*interopContext).txGetType, Price: 1}, + {Name: "Neo.Transaction.GetUnspentCoins", Func: (*interopContext).txGetUnspentCoins, Price: 200}, + {Name: "Neo.Transaction.GetWitnesses", Func: (*interopContext).txGetWitnesses, Price: 200}, + // {Name: "Neo.Enumerator.Concat", Func: (*interopContext).enumeratorConcat, Price: 1}, + // {Name: "Neo.Enumerator.Create", Func: (*interopContext).enumeratorCreate, Price: 1}, + // {Name: "Neo.Enumerator.Next", Func: (*interopContext).enumeratorNext, Price: 1}, + // {Name: "Neo.Enumerator.Value", Func: (*interopContext).enumeratorValue, Price: 1}, + // {Name: "Neo.InvocationTransaction.GetScript", {ic.invocationTx_GetScript, 1}, + // {Name: "Neo.Iterator.Concat", Func: (*interopContext).iteratorConcat, Price: 1}, + // {Name: "Neo.Iterator.Create", Func: (*interopContext).iteratorCreate, Price: 1}, + // {Name: "Neo.Iterator.Key", Func: (*interopContext).iteratorKey, Price: 1}, + // {Name: "Neo.Iterator.Keys", Func: (*interopContext).iteratorKeys, Price: 1}, + // {Name: "Neo.Iterator.Values", Func: (*interopContext).iteratorValues, Price: 1}, + {Name: "Neo.Runtime.Deserialize", Func: (*interopContext).runtimeDeserialize, Price: 1}, + {Name: "Neo.Runtime.Serialize", Func: (*interopContext).runtimeSerialize, Price: 1}, + // {Name: "Neo.Storage.Find", Func: (*interopContext).storageFind, Price: 1}, + // {Name: "Neo.Witness.GetVerificationScript", Func: (*interopContext).witnessGetVerificationScript, Price: 100}, - // Aliases. - // "Neo.Iterator.Next": {Func: ic.enumeratorNext, Price: 1}, - // "Neo.Iterator.Value": {Func: ic.enumeratorValue, Price: 1}, + // Aliases. + // {Name: "Neo.Iterator.Next", Func: (*interopContext).enumeratorNext, Price: 1}, + // {Name: "Neo.Iterator.Value", Func: (*interopContext).enumeratorValue, Price: 1}, - // Old compatibility APIs. - "AntShares.Account.GetBalance": {Func: ic.accountGetBalance, Price: 1}, - "AntShares.Account.GetScriptHash": {Func: ic.accountGetScriptHash, Price: 1}, - "AntShares.Account.GetVotes": {Func: ic.accountGetVotes, Price: 1}, - "AntShares.Asset.Create": {Func: ic.assetCreate, Price: 0}, - "AntShares.Asset.GetAdmin": {Func: ic.assetGetAdmin, Price: 1}, - "AntShares.Asset.GetAmount": {Func: ic.assetGetAmount, Price: 1}, - "AntShares.Asset.GetAssetId": {Func: ic.assetGetAssetID, Price: 1}, - "AntShares.Asset.GetAssetType": {Func: ic.assetGetAssetType, Price: 1}, - "AntShares.Asset.GetAvailable": {Func: ic.assetGetAvailable, Price: 1}, - "AntShares.Asset.GetIssuer": {Func: ic.assetGetIssuer, Price: 1}, - "AntShares.Asset.GetOwner": {Func: ic.assetGetOwner, Price: 1}, - "AntShares.Asset.GetPrecision": {Func: ic.assetGetPrecision, Price: 1}, - "AntShares.Asset.Renew": {Func: ic.assetRenew, Price: 0}, - "AntShares.Attribute.GetData": {Func: ic.attrGetData, Price: 1}, - "AntShares.Attribute.GetUsage": {Func: ic.attrGetUsage, Price: 1}, - "AntShares.Block.GetTransaction": {Func: ic.blockGetTransaction, Price: 1}, - "AntShares.Block.GetTransactionCount": {Func: ic.blockGetTransactionCount, Price: 1}, - "AntShares.Block.GetTransactions": {Func: ic.blockGetTransactions, Price: 1}, - "AntShares.Blockchain.GetAccount": {Func: ic.bcGetAccount, Price: 100}, - "AntShares.Blockchain.GetAsset": {Func: ic.bcGetAsset, Price: 100}, - "AntShares.Blockchain.GetBlock": {Func: ic.bcGetBlock, Price: 200}, - "AntShares.Blockchain.GetContract": {Func: ic.bcGetContract, Price: 100}, - "AntShares.Blockchain.GetHeader": {Func: ic.bcGetHeader, Price: 100}, - "AntShares.Blockchain.GetHeight": {Func: ic.bcGetHeight, Price: 1}, - "AntShares.Blockchain.GetTransaction": {Func: ic.bcGetTransaction, Price: 100}, - "AntShares.Blockchain.GetValidators": {Func: ic.bcGetValidators, Price: 200}, - "AntShares.Contract.Create": {Func: ic.contractCreate, Price: 0}, - "AntShares.Contract.Destroy": {Func: ic.contractDestroy, Price: 1}, - "AntShares.Contract.GetScript": {Func: ic.contractGetScript, Price: 1}, - "AntShares.Contract.GetStorageContext": {Func: ic.contractGetStorageContext, Price: 1}, - "AntShares.Contract.Migrate": {Func: ic.contractMigrate, Price: 0}, - "AntShares.Header.GetConsensusData": {Func: ic.headerGetConsensusData, Price: 1}, - "AntShares.Header.GetHash": {Func: ic.headerGetHash, Price: 1}, - "AntShares.Header.GetMerkleRoot": {Func: ic.headerGetMerkleRoot, Price: 1}, - "AntShares.Header.GetNextConsensus": {Func: ic.headerGetNextConsensus, Price: 1}, - "AntShares.Header.GetPrevHash": {Func: ic.headerGetPrevHash, Price: 1}, - "AntShares.Header.GetTimestamp": {Func: ic.headerGetTimestamp, Price: 1}, - "AntShares.Header.GetVersion": {Func: ic.headerGetVersion, Price: 1}, - "AntShares.Input.GetHash": {Func: ic.inputGetHash, Price: 1}, - "AntShares.Input.GetIndex": {Func: ic.inputGetIndex, Price: 1}, - "AntShares.Output.GetAssetId": {Func: ic.outputGetAssetID, Price: 1}, - "AntShares.Output.GetScriptHash": {Func: ic.outputGetScriptHash, Price: 1}, - "AntShares.Output.GetValue": {Func: ic.outputGetValue, Price: 1}, - "AntShares.Runtime.CheckWitness": {Func: ic.runtimeCheckWitness, Price: 200}, - "AntShares.Runtime.Log": {Func: ic.runtimeLog, Price: 1}, - "AntShares.Runtime.Notify": {Func: ic.runtimeNotify, Price: 1}, - "AntShares.Storage.Delete": {Func: ic.storageDelete, Price: 100}, - "AntShares.Storage.Get": {Func: ic.storageGet, Price: 100}, - "AntShares.Storage.GetContext": {Func: ic.storageGetContext, Price: 1}, - "AntShares.Storage.Put": {Func: ic.storagePut, Price: 0}, - "AntShares.Transaction.GetAttributes": {Func: ic.txGetAttributes, Price: 1}, - "AntShares.Transaction.GetHash": {Func: ic.txGetHash, Price: 1}, - "AntShares.Transaction.GetInputs": {Func: ic.txGetInputs, Price: 1}, - "AntShares.Transaction.GetOutputs": {Func: ic.txGetOutputs, Price: 1}, - "AntShares.Transaction.GetReferences": {Func: ic.txGetReferences, Price: 200}, - "AntShares.Transaction.GetType": {Func: ic.txGetType, Price: 1}, - } + // Old compatibility APIs. + {Name: "AntShares.Account.GetBalance", Func: (*interopContext).accountGetBalance, Price: 1}, + {Name: "AntShares.Account.GetScriptHash", Func: (*interopContext).accountGetScriptHash, Price: 1}, + {Name: "AntShares.Account.GetVotes", Func: (*interopContext).accountGetVotes, Price: 1}, + {Name: "AntShares.Asset.Create", Func: (*interopContext).assetCreate, Price: 0}, + {Name: "AntShares.Asset.GetAdmin", Func: (*interopContext).assetGetAdmin, Price: 1}, + {Name: "AntShares.Asset.GetAmount", Func: (*interopContext).assetGetAmount, Price: 1}, + {Name: "AntShares.Asset.GetAssetId", Func: (*interopContext).assetGetAssetID, Price: 1}, + {Name: "AntShares.Asset.GetAssetType", Func: (*interopContext).assetGetAssetType, Price: 1}, + {Name: "AntShares.Asset.GetAvailable", Func: (*interopContext).assetGetAvailable, Price: 1}, + {Name: "AntShares.Asset.GetIssuer", Func: (*interopContext).assetGetIssuer, Price: 1}, + {Name: "AntShares.Asset.GetOwner", Func: (*interopContext).assetGetOwner, Price: 1}, + {Name: "AntShares.Asset.GetPrecision", Func: (*interopContext).assetGetPrecision, Price: 1}, + {Name: "AntShares.Asset.Renew", Func: (*interopContext).assetRenew, Price: 0}, + {Name: "AntShares.Attribute.GetData", Func: (*interopContext).attrGetData, Price: 1}, + {Name: "AntShares.Attribute.GetUsage", Func: (*interopContext).attrGetUsage, Price: 1}, + {Name: "AntShares.Block.GetTransaction", Func: (*interopContext).blockGetTransaction, Price: 1}, + {Name: "AntShares.Block.GetTransactionCount", Func: (*interopContext).blockGetTransactionCount, Price: 1}, + {Name: "AntShares.Block.GetTransactions", Func: (*interopContext).blockGetTransactions, Price: 1}, + {Name: "AntShares.Blockchain.GetAccount", Func: (*interopContext).bcGetAccount, Price: 100}, + {Name: "AntShares.Blockchain.GetAsset", Func: (*interopContext).bcGetAsset, Price: 100}, + {Name: "AntShares.Blockchain.GetBlock", Func: (*interopContext).bcGetBlock, Price: 200}, + {Name: "AntShares.Blockchain.GetContract", Func: (*interopContext).bcGetContract, Price: 100}, + {Name: "AntShares.Blockchain.GetHeader", Func: (*interopContext).bcGetHeader, Price: 100}, + {Name: "AntShares.Blockchain.GetHeight", Func: (*interopContext).bcGetHeight, Price: 1}, + {Name: "AntShares.Blockchain.GetTransaction", Func: (*interopContext).bcGetTransaction, Price: 100}, + {Name: "AntShares.Blockchain.GetValidators", Func: (*interopContext).bcGetValidators, Price: 200}, + {Name: "AntShares.Contract.Create", Func: (*interopContext).contractCreate, Price: 0}, + {Name: "AntShares.Contract.Destroy", Func: (*interopContext).contractDestroy, Price: 1}, + {Name: "AntShares.Contract.GetScript", Func: (*interopContext).contractGetScript, Price: 1}, + {Name: "AntShares.Contract.GetStorageContext", Func: (*interopContext).contractGetStorageContext, Price: 1}, + {Name: "AntShares.Contract.Migrate", Func: (*interopContext).contractMigrate, Price: 0}, + {Name: "AntShares.Header.GetConsensusData", Func: (*interopContext).headerGetConsensusData, Price: 1}, + {Name: "AntShares.Header.GetHash", Func: (*interopContext).headerGetHash, Price: 1}, + {Name: "AntShares.Header.GetMerkleRoot", Func: (*interopContext).headerGetMerkleRoot, Price: 1}, + {Name: "AntShares.Header.GetNextConsensus", Func: (*interopContext).headerGetNextConsensus, Price: 1}, + {Name: "AntShares.Header.GetPrevHash", Func: (*interopContext).headerGetPrevHash, Price: 1}, + {Name: "AntShares.Header.GetTimestamp", Func: (*interopContext).headerGetTimestamp, Price: 1}, + {Name: "AntShares.Header.GetVersion", Func: (*interopContext).headerGetVersion, Price: 1}, + {Name: "AntShares.Input.GetHash", Func: (*interopContext).inputGetHash, Price: 1}, + {Name: "AntShares.Input.GetIndex", Func: (*interopContext).inputGetIndex, Price: 1}, + {Name: "AntShares.Output.GetAssetId", Func: (*interopContext).outputGetAssetID, Price: 1}, + {Name: "AntShares.Output.GetScriptHash", Func: (*interopContext).outputGetScriptHash, Price: 1}, + {Name: "AntShares.Output.GetValue", Func: (*interopContext).outputGetValue, Price: 1}, + {Name: "AntShares.Runtime.CheckWitness", Func: (*interopContext).runtimeCheckWitness, Price: 200}, + {Name: "AntShares.Runtime.Log", Func: (*interopContext).runtimeLog, Price: 1}, + {Name: "AntShares.Runtime.Notify", Func: (*interopContext).runtimeNotify, Price: 1}, + {Name: "AntShares.Storage.Delete", Func: (*interopContext).storageDelete, Price: 100}, + {Name: "AntShares.Storage.Get", Func: (*interopContext).storageGet, Price: 100}, + {Name: "AntShares.Storage.GetContext", Func: (*interopContext).storageGetContext, Price: 1}, + {Name: "AntShares.Storage.Put", Func: (*interopContext).storagePut, Price: 0}, + {Name: "AntShares.Transaction.GetAttributes", Func: (*interopContext).txGetAttributes, Price: 1}, + {Name: "AntShares.Transaction.GetHash", Func: (*interopContext).txGetHash, Price: 1}, + {Name: "AntShares.Transaction.GetInputs", Func: (*interopContext).txGetInputs, Price: 1}, + {Name: "AntShares.Transaction.GetOutputs", Func: (*interopContext).txGetOutputs, Price: 1}, + {Name: "AntShares.Transaction.GetReferences", Func: (*interopContext).txGetReferences, Price: 200}, + {Name: "AntShares.Transaction.GetType", Func: (*interopContext).txGetType, Price: 1}, +} + +// initIDinInteropsSlice initializes IDs from names in one given +// interopedFunction slice and then sorts it. +func initIDinInteropsSlice(iops []interopedFunction) { + for i := range iops { + iops[i].ID = vm.InteropNameToID([]byte(iops[i].Name)) + } + sort.Slice(iops, func(i, j int) bool { + return iops[i].ID < iops[j].ID + }) +} + +// init initializes IDs in the global interop slices. +func init() { + initIDinInteropsSlice(systemInterops) + initIDinInteropsSlice(neoInterops) } diff --git a/pkg/vm/interop.go b/pkg/vm/interop.go index c96477f6f..facfcb601 100644 --- a/pkg/vm/interop.go +++ b/pkg/vm/interop.go @@ -1,13 +1,63 @@ package vm import ( + "crypto/sha256" + "encoding/binary" "errors" "fmt" + "sort" ) // InteropFunc allows to hook into the VM. type InteropFunc func(vm *VM) error +// InteropFuncPrice represents an interop function with a price. +type InteropFuncPrice struct { + Func InteropFunc + Price int +} + +// interopIDFuncPrice adds an ID to the InteropFuncPrice. +type interopIDFuncPrice struct { + ID uint32 + InteropFuncPrice +} + +// InteropGetterFunc is a function that returns an interop function-price +// structure by the given interop ID. +type InteropGetterFunc func(uint32) *InteropFuncPrice + +var defaultVMInterops = []interopIDFuncPrice{ + {InteropNameToID([]byte("Neo.Runtime.Log")), + InteropFuncPrice{runtimeLog, 1}}, + {InteropNameToID([]byte("Neo.Runtime.Notify")), + InteropFuncPrice{runtimeNotify, 1}}, + {InteropNameToID([]byte("Neo.Runtime.Serialize")), + InteropFuncPrice{RuntimeSerialize, 1}}, + {InteropNameToID([]byte("System.Runtime.Serialize")), + InteropFuncPrice{RuntimeSerialize, 1}}, + {InteropNameToID([]byte("Neo.Runtime.Deserialize")), + InteropFuncPrice{RuntimeDeserialize, 1}}, + {InteropNameToID([]byte("System.Runtime.Deserialize")), + InteropFuncPrice{RuntimeDeserialize, 1}}, +} + +func getDefaultVMInterop(id uint32) *InteropFuncPrice { + n := sort.Search(len(defaultVMInterops), func(i int) bool { + return defaultVMInterops[i].ID >= id + }) + if n < len(defaultVMInterops) && defaultVMInterops[n].ID == id { + return &defaultVMInterops[n].InteropFuncPrice + } + return nil +} + +// InteropNameToID returns an identificator of the method based on its name. +func InteropNameToID(name []byte) uint32 { + h := sha256.Sum256(name) + return binary.LittleEndian.Uint32(h[:4]) +} + // runtimeLog handles the syscall "Neo.Runtime.Log" for printing and logging stuff. func runtimeLog(vm *VM) error { item := vm.Estack().Pop() @@ -50,3 +100,10 @@ func RuntimeDeserialize(vm *VM) error { return nil } + +// init sorts the global defaultVMInterops value. +func init() { + sort.Slice(defaultVMInterops, func(i, j int) bool { + return defaultVMInterops[i].ID < defaultVMInterops[j].ID + }) +} diff --git a/pkg/vm/json_test.go b/pkg/vm/json_test.go index 2cc00de2b..6ee5568ae 100644 --- a/pkg/vm/json_test.go +++ b/pkg/vm/json_test.go @@ -108,6 +108,18 @@ func TestUT(t *testing.T) { require.Equal(t, true, testsRan, "neo-vm tests should be available (check submodules)") } +func getTestingInterop(id uint32) *InteropFuncPrice { + // FIXME in NEO 3.0 it is []byte{0x77, 0x77, 0x77, 0x77} https://github.com/nspcc-dev/neo-go/issues/477 + if id == InteropNameToID([]byte("Test.ExecutionEngine.GetScriptContainer")) || + id == InteropNameToID([]byte("System.ExecutionEngine.GetScriptContainer")) { + return &InteropFuncPrice{InteropFunc(func(v *VM) error { + v.estack.Push(&Element{value: (*InteropItem)(nil)}) + return nil + }), 0} + } + return nil +} + func testFile(t *testing.T, filename string) { data, err := ioutil.ReadFile(filename) if err != nil { @@ -134,16 +146,7 @@ func testFile(t *testing.T, filename string) { // FIXME remove when NEO 3.0 https://github.com/nspcc-dev/neo-go/issues/477 vm.getScript = getScript(test.ScriptTable) - - // FIXME in NEO 3.0 it is []byte{0x77, 0x77, 0x77, 0x77} https://github.com/nspcc-dev/neo-go/issues/477 - vm.RegisterInteropFunc("Test.ExecutionEngine.GetScriptContainer", InteropFunc(func(v *VM) error { - v.estack.Push(&Element{value: (*InteropItem)(nil)}) - return nil - }), 0) - vm.RegisterInteropFunc("System.ExecutionEngine.GetScriptContainer", InteropFunc(func(v *VM) error { - v.estack.Push(&Element{value: (*InteropItem)(nil)}) - return nil - }), 0) + vm.RegisterInteropGetter(getTestingInterop) for i := range test.Steps { execStep(t, vm, test.Steps[i]) diff --git a/pkg/vm/tests/vm_test.go b/pkg/vm/tests/vm_test.go index 011103934..fa4955187 100644 --- a/pkg/vm/tests/vm_test.go +++ b/pkg/vm/tests/vm_test.go @@ -48,9 +48,7 @@ func vmAndCompile(t *testing.T, src string) *vm.VM { vm := vm.New() storePlugin := newStoragePlugin() - vm.RegisterInteropFunc("Neo.Storage.Get", storePlugin.Get, 1) - vm.RegisterInteropFunc("Neo.Storage.Put", storePlugin.Put, 1) - vm.RegisterInteropFunc("Neo.Storage.GetContext", storePlugin.GetContext, 1) + vm.RegisterInteropGetter(storePlugin.getInterop) b, err := compiler.Compile(strings.NewReader(src)) if err != nil { @@ -61,13 +59,28 @@ func vmAndCompile(t *testing.T, src string) *vm.VM { } type storagePlugin struct { - mem map[string][]byte + mem map[string][]byte + interops map[uint32]vm.InteropFunc } func newStoragePlugin() *storagePlugin { - return &storagePlugin{ - mem: make(map[string][]byte), + s := &storagePlugin{ + mem: make(map[string][]byte), + interops: make(map[uint32]vm.InteropFunc), } + s.interops[vm.InteropNameToID([]byte("Neo.Storage.Get"))] = s.Get + s.interops[vm.InteropNameToID([]byte("Neo.Storage.Put"))] = s.Put + s.interops[vm.InteropNameToID([]byte("Neo.Storage.GetContext"))] = s.GetContext + return s + +} + +func (s *storagePlugin) getInterop(id uint32) *vm.InteropFuncPrice { + f := s.interops[id] + if f != nil { + return &vm.InteropFuncPrice{Func: f, Price: 1} + } + return nil } func (s *storagePlugin) Delete(vm *vm.VM) error { diff --git a/pkg/vm/vm.go b/pkg/vm/vm.go index 93f190c10..204846f49 100644 --- a/pkg/vm/vm.go +++ b/pkg/vm/vm.go @@ -60,8 +60,8 @@ const ( type VM struct { state State - // registered interop hooks. - interop map[string]InteropFuncPrice + // callbacks to get interops. + getInterop []InteropGetterFunc // callback to get scripts. getScript func(util.Uint160) []byte @@ -80,19 +80,13 @@ type VM struct { keys map[string]*keys.PublicKey } -// InteropFuncPrice represents an interop function with a price. -type InteropFuncPrice struct { - Func InteropFunc - Price int -} - // New returns a new VM object ready to load .avm bytecode scripts. func New() *VM { vm := &VM{ - interop: make(map[string]InteropFuncPrice), - getScript: nil, - state: haltState, - istack: NewStack("invocation"), + getInterop: make([]InteropGetterFunc, 0, 3), // 3 functions is typical for our default usage. + getScript: nil, + state: haltState, + istack: NewStack("invocation"), itemCount: make(map[StackItem]int), keys: make(map[string]*keys.PublicKey), @@ -101,14 +95,7 @@ func New() *VM { vm.estack = vm.newItemStack("evaluation") vm.astack = vm.newItemStack("alt") - // Register native interop hooks. - vm.RegisterInteropFunc("Neo.Runtime.Log", runtimeLog, 1) - vm.RegisterInteropFunc("Neo.Runtime.Notify", runtimeNotify, 1) - vm.RegisterInteropFunc("Neo.Runtime.Serialize", RuntimeSerialize, 1) - vm.RegisterInteropFunc("System.Runtime.Serialize", RuntimeSerialize, 1) - vm.RegisterInteropFunc("Neo.Runtime.Deserialize", RuntimeDeserialize, 1) - vm.RegisterInteropFunc("System.Runtime.Deserialize", RuntimeDeserialize, 1) - + vm.RegisterInteropGetter(getDefaultVMInterop) return vm } @@ -120,18 +107,11 @@ func (v *VM) newItemStack(n string) *Stack { return s } -// RegisterInteropFunc registers the given InteropFunc to the VM. -func (v *VM) RegisterInteropFunc(name string, f InteropFunc, price int) { - v.interop[name] = InteropFuncPrice{f, price} -} - -// RegisterInteropFuncs registers all interop functions passed in a map in -// the VM. Effectively it's a batched version of RegisterInteropFunc. -func (v *VM) RegisterInteropFuncs(interops map[string]InteropFuncPrice) { - // We allow reregistration here. - for name := range interops { - v.interop[name] = interops[name] - } +// RegisterInteropGetter registers the given InteropGetterFunc into VM. There +// can be many interop getters and they're probed in LIFO order wrt their +// registration time. +func (v *VM) RegisterInteropGetter(f InteropGetterFunc) { + v.getInterop = append(v.getInterop, f) } // Estack returns the evaluation stack so interop hooks can utilize this. @@ -1095,9 +1075,23 @@ func (v *VM) execute(ctx *Context, op opcode.Opcode, parameter []byte) (err erro } case opcode.SYSCALL: - ifunc, ok := v.interop[string(parameter)] - if !ok { - panic(fmt.Sprintf("interop hook (%q) not registered", parameter)) + var ifunc *InteropFuncPrice + var interopID uint32 + + if len(parameter) == 4 { + interopID = binary.LittleEndian.Uint32(parameter) + } else { + interopID = InteropNameToID(parameter) + } + // LIFO interpretation of callbacks. + for i := len(v.getInterop) - 1; i >= 0; i-- { + ifunc = v.getInterop[i](interopID) + if ifunc != nil { + break + } + } + if ifunc == nil { + panic(fmt.Sprintf("interop hook (%q/0x%x) not registered", parameter, interopID)) } if err := ifunc.Func(v); err != nil { panic(fmt.Sprintf("failed to invoke syscall: %s", err)) diff --git a/pkg/vm/vm_test.go b/pkg/vm/vm_test.go index 9d6d86452..5825fbc83 100644 --- a/pkg/vm/vm_test.go +++ b/pkg/vm/vm_test.go @@ -16,12 +16,19 @@ import ( "github.com/stretchr/testify/require" ) +func fooInteropGetter(id uint32) *InteropFuncPrice { + if id == InteropNameToID([]byte("foo")) { + return &InteropFuncPrice{func(evm *VM) error { + evm.Estack().PushVal(1) + return nil + }, 1} + } + return nil +} + func TestInteropHook(t *testing.T) { v := New() - v.RegisterInteropFunc("foo", func(evm *VM) error { - evm.Estack().PushVal(1) - return nil - }, 1) + v.RegisterInteropGetter(fooInteropGetter) buf := new(bytes.Buffer) EmitSyscall(buf, "foo") @@ -32,13 +39,27 @@ func TestInteropHook(t *testing.T) { assert.Equal(t, big.NewInt(1), v.estack.Pop().value.Value()) } -func TestRegisterInterop(t *testing.T) { +func TestInteropHookViaID(t *testing.T) { v := New() - currRegistered := len(v.interop) - v.RegisterInteropFunc("foo", func(evm *VM) error { return nil }, 1) - assert.Equal(t, currRegistered+1, len(v.interop)) - _, ok := v.interop["foo"] - assert.Equal(t, true, ok) + v.RegisterInteropGetter(fooInteropGetter) + + buf := new(bytes.Buffer) + fooid := InteropNameToID([]byte("foo")) + var id = make([]byte, 4) + binary.LittleEndian.PutUint32(id, fooid) + _ = EmitSyscall(buf, string(id)) + _ = EmitOpcode(buf, opcode.RET) + v.Load(buf.Bytes()) + runVM(t, v) + assert.Equal(t, 1, v.estack.Len()) + assert.Equal(t, big.NewInt(1), v.estack.Pop().value.Value()) +} + +func TestRegisterInteropGetter(t *testing.T) { + v := New() + currRegistered := len(v.getInterop) + v.RegisterInteropGetter(fooInteropGetter) + assert.Equal(t, currRegistered+1, len(v.getInterop)) } func TestBytesToPublicKey(t *testing.T) {