diff --git a/pkg/compiler/interop_test.go b/pkg/compiler/interop_test.go index 120e29709..d737a3058 100644 --- a/pkg/compiler/interop_test.go +++ b/pkg/compiler/interop_test.go @@ -202,7 +202,7 @@ func TestAppCall(t *testing.T) { fc := fakechain.NewFakeChain() ic := interop.NewContext(trigger.Application, fc, dao.NewSimple(storage.NewMemoryStore(), false, false), - interop.DefaultBaseExecFee, native.DefaultStoragePrice, contractGetter, nil, nil, nil, zaptest.NewLogger(t)) + interop.DefaultBaseExecFee, native.DefaultStoragePrice, contractGetter, nil, nil, nil, nil, zaptest.NewLogger(t)) t.Run("valid script", func(t *testing.T) { src := getAppCallScript(fmt.Sprintf("%#v", ih.BytesBE())) diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index 1f881156b..694d99b33 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -1095,7 +1095,7 @@ func (bc *Blockchain) storeBlock(block *block.Block, txpool *mempool.Pool) error close(aerdone) }() _ = cache.GetItemCtx() // Prime serialization context cache (it'll be reused by upper layer DAOs). - aer, err := bc.runPersist(bc.contracts.GetPersistScript(), block, cache, trigger.OnPersist) + aer, v, err := bc.runPersist(bc.contracts.GetPersistScript(), block, cache, trigger.OnPersist, nil) if err != nil { // Release goroutines, don't care about errors, we already have one. close(aerchan) @@ -1107,10 +1107,8 @@ func (bc *Blockchain) storeBlock(block *block.Block, txpool *mempool.Pool) error for _, tx := range block.Transactions { systemInterop := bc.newInteropContext(trigger.Application, cache, block, tx) - v := systemInterop.SpawnVM() + systemInterop.ReuseVM(v) v.LoadScriptWithFlags(tx.Script, callflag.All) - v.SetPriceGetter(systemInterop.GetPrice) - v.LoadToken = contract.LoadToken(systemInterop) v.GasLimit = tx.SystemFee err := systemInterop.Exec() @@ -1145,7 +1143,7 @@ func (bc *Blockchain) storeBlock(block *block.Block, txpool *mempool.Pool) error aerchan <- aer } - aer, err = bc.runPersist(bc.contracts.GetPostPersistScript(), block, cache, trigger.PostPersist) + aer, _, err = bc.runPersist(bc.contracts.GetPostPersistScript(), block, cache, trigger.PostPersist, v) if err != nil { // Release goroutines, don't care about errors, we already have one. close(aerchan) @@ -1280,15 +1278,18 @@ func (bc *Blockchain) IsExtensibleAllowed(u util.Uint160) bool { return n < len(us) } -func (bc *Blockchain) runPersist(script []byte, block *block.Block, cache *dao.Simple, trig trigger.Type) (*state.AppExecResult, error) { +func (bc *Blockchain) runPersist(script []byte, block *block.Block, cache *dao.Simple, trig trigger.Type, v *vm.VM) (*state.AppExecResult, *vm.VM, error) { systemInterop := bc.newInteropContext(trig, cache, block, nil) - v := systemInterop.SpawnVM() + if v == nil { + v = systemInterop.SpawnVM() + } else { + systemInterop.ReuseVM(v) + } v.LoadScriptWithFlags(script, callflag.All) - v.SetPriceGetter(systemInterop.GetPrice) if err := systemInterop.Exec(); err != nil { - return nil, fmt.Errorf("VM has failed: %w", err) + return nil, v, fmt.Errorf("VM has failed: %w", err) } else if _, err := systemInterop.DAO.Persist(); err != nil { - return nil, fmt.Errorf("can't save changes: %w", err) + return nil, v, fmt.Errorf("can't save changes: %w", err) } return &state.AppExecResult{ Container: block.Hash(), // application logs can be retrieved by block hash @@ -1299,7 +1300,7 @@ func (bc *Blockchain) runPersist(script []byte, block *block.Block, cache *dao.S Stack: v.Estack().ToArray(), Events: systemInterop.Notifications, }, - }, nil + }, v, nil } func (bc *Blockchain) handleNotification(note *state.NotificationEvent, d *dao.Simple, @@ -1365,6 +1366,7 @@ func (bc *Blockchain) processTokenTransfer(cache *dao.Simple, transCache map[uti if !isNEP11 { nep17xfer = &state.NEP17Transfer{ Asset: id, + Amount: *amount, From: from, To: to, Block: b.Index, @@ -1376,6 +1378,7 @@ func (bc *Blockchain) processTokenTransfer(cache *dao.Simple, transCache map[uti nep11xfer := &state.NEP11Transfer{ NEP17Transfer: state.NEP17Transfer{ Asset: id, + Amount: *amount, From: from, To: to, Block: b.Index, @@ -1388,13 +1391,14 @@ func (bc *Blockchain) processTokenTransfer(cache *dao.Simple, transCache map[uti nep17xfer = &nep11xfer.NEP17Transfer } if !from.Equals(util.Uint160{}) { - _ = nep17xfer.Amount.Neg(amount) // We already have the Int. - if appendTokenTransfer(cache, transCache, from, transfer, id, b.Index, b.Timestamp, isNEP11) != nil { + _ = nep17xfer.Amount.Neg(&nep17xfer.Amount) + err := appendTokenTransfer(cache, transCache, from, transfer, id, b.Index, b.Timestamp, isNEP11) + _ = nep17xfer.Amount.Neg(&nep17xfer.Amount) + if err != nil { return } } if !to.Equals(util.Uint160{}) { - _ = nep17xfer.Amount.Set(amount) // We already have the Int. _ = appendTokenTransfer(cache, transCache, to, transfer, id, b.Index, b.Timestamp, isNEP11) // Nothing useful we can do. } } @@ -1451,7 +1455,7 @@ func appendTokenTransfer(cache *dao.Simple, transCache map[util.Uint160]transfer *nextBatch++ *currTimestamp = bTimestamp // Put makes a copy of it anyway. - log.Raw = log.Raw[:0] + log.Reset() } transCache[addr] = transferData return nil @@ -2164,9 +2168,7 @@ func (bc *Blockchain) GetEnrollments() ([]state.Validator, error) { // GetTestVM returns an interop context with VM set up for a test run. func (bc *Blockchain) GetTestVM(t trigger.Type, tx *transaction.Transaction, b *block.Block) *interop.Context { systemInterop := bc.newInteropContext(t, bc.dao, b, tx) - vm := systemInterop.SpawnVM() - vm.SetPriceGetter(systemInterop.GetPrice) - vm.LoadToken = contract.LoadToken(systemInterop) + _ = systemInterop.SpawnVM() // All the other code suppose that the VM is ready. return systemInterop } @@ -2199,9 +2201,7 @@ func (bc *Blockchain) GetTestHistoricVM(t trigger.Type, tx *transaction.Transact return nil, fmt.Errorf("failed to initialize native cache backed by historic DAO: %w", err) } systemInterop := bc.newInteropContext(t, dTrie, b, tx) - vm := systemInterop.SpawnVM() - vm.SetPriceGetter(systemInterop.GetPrice) - vm.LoadToken = contract.LoadToken(systemInterop) + _ = systemInterop.SpawnVM() // All the other code suppose that the VM is ready. return systemInterop, nil } @@ -2276,8 +2276,6 @@ func (bc *Blockchain) verifyHashAgainstScript(hash util.Uint160, witness *transa } vm := interopCtx.SpawnVM() - vm.SetPriceGetter(interopCtx.GetPrice) - vm.LoadToken = contract.LoadToken(interopCtx) vm.GasLimit = gas if err := bc.InitVerificationContext(interopCtx, hash, witness); err != nil { return 0, err @@ -2373,7 +2371,7 @@ func (bc *Blockchain) newInteropContext(trigger trigger.Type, d *dao.Simple, blo // changes that were not yet persisted to Blockchain's dao. baseStorageFee = bc.contracts.Policy.GetStoragePriceInternal(d) } - ic := interop.NewContext(trigger, bc, d, baseExecFee, baseStorageFee, bc.contracts.Management.GetContract, bc.contracts.Contracts, block, tx, bc.log) + ic := interop.NewContext(trigger, bc, d, baseExecFee, baseStorageFee, bc.contracts.Management.GetContract, bc.contracts.Contracts, contract.LoadToken, block, tx, bc.log) ic.Functions = systemInterops switch { case tx != nil: diff --git a/pkg/core/interop/context.go b/pkg/core/interop/context.go index 0c7419284..f3b805132 100644 --- a/pkg/core/interop/context.go +++ b/pkg/core/interop/context.go @@ -64,6 +64,7 @@ type Context struct { getContract func(*dao.Simple, util.Uint160) (*state.Contract, error) baseExecFee int64 baseStorageFee int64 + loadToken func(ic *Context, id int32) error GetRandomCounter uint32 signers []transaction.Signer } @@ -71,6 +72,7 @@ type Context struct { // NewContext returns new interop context. func NewContext(trigger trigger.Type, bc Ledger, d *dao.Simple, baseExecFee, baseStorageFee int64, getContract func(*dao.Simple, util.Uint160) (*state.Contract, error), natives []Contract, + loadTokenFunc func(ic *Context, id int32) error, block *block.Block, tx *transaction.Transaction, log *zap.Logger) *Context { dao := d.GetPrivate() cfg := bc.GetConfig() @@ -88,6 +90,7 @@ func NewContext(trigger trigger.Type, bc Ledger, d *dao.Simple, baseExecFee, bas getContract: getContract, baseExecFee: baseExecFee, baseStorageFee: baseStorageFee, + loadToken: loadTokenFunc, } } @@ -298,6 +301,12 @@ func (ic *Context) BaseStorageFee() int64 { return ic.baseStorageFee } +// LoadToken wraps externally provided load-token loading function providing it with context, +// this function can then be easily used by VM. +func (ic *Context) LoadToken(id int32) error { + return ic.loadToken(ic, id) +} + // SyscallHandler handles syscall with id. func (ic *Context) SyscallHandler(_ *vm.VM, id uint32) error { f := ic.GetFunction(id) @@ -317,10 +326,22 @@ func (ic *Context) SyscallHandler(_ *vm.VM, id uint32) error { // SpawnVM spawns a new VM with the specified gas limit and set context.VM field. func (ic *Context) SpawnVM() *vm.VM { v := vm.NewWithTrigger(ic.Trigger) + ic.initVM(v) + return v +} + +func (ic *Context) initVM(v *vm.VM) { + v.LoadToken = ic.LoadToken v.GasLimit = -1 v.SyscallHandler = ic.SyscallHandler + v.SetPriceGetter(ic.GetPrice) ic.VM = v - return v +} + +// ReuseVM resets given VM and allows to reuse it in the current context. +func (ic *Context) ReuseVM(v *vm.VM) { + v.Reset(ic.Trigger) + ic.initVM(v) } // RegisterCancelFunc adds the given function to the list of functions to be called after the VM diff --git a/pkg/core/interop/contract/call.go b/pkg/core/interop/contract/call.go index a322bcc7c..26a23aaa1 100644 --- a/pkg/core/interop/contract/call.go +++ b/pkg/core/interop/contract/call.go @@ -22,26 +22,24 @@ type policyChecker interface { } // LoadToken calls method specified by the token id. -func LoadToken(ic *interop.Context) func(id int32) error { - return func(id int32) error { - ctx := ic.VM.Context() - if !ctx.GetCallFlags().Has(callflag.ReadStates | callflag.AllowCall) { - return errors.New("invalid call flags") - } - tok := ctx.NEF.Tokens[id] - if int(tok.ParamCount) > ctx.Estack().Len() { - return errors.New("stack is too small") - } - args := make([]stackitem.Item, tok.ParamCount) - for i := range args { - args[i] = ic.VM.Estack().Pop().Item() - } - cs, err := ic.GetContract(tok.Hash) - if err != nil { - return fmt.Errorf("token contract %s not found: %w", tok.Hash.StringLE(), err) - } - return callInternal(ic, cs, tok.Method, tok.CallFlag, tok.HasReturn, args, false) +func LoadToken(ic *interop.Context, id int32) error { + ctx := ic.VM.Context() + if !ctx.GetCallFlags().Has(callflag.ReadStates | callflag.AllowCall) { + return errors.New("invalid call flags") } + tok := ctx.NEF.Tokens[id] + if int(tok.ParamCount) > ctx.Estack().Len() { + return errors.New("stack is too small") + } + args := make([]stackitem.Item, tok.ParamCount) + for i := range args { + args[i] = ic.VM.Estack().Pop().Item() + } + cs, err := ic.GetContract(tok.Hash) + if err != nil { + return fmt.Errorf("token contract %s not found: %w", tok.Hash.StringLE(), err) + } + return callInternal(ic, cs, tok.Method, tok.CallFlag, tok.HasReturn, args, false) } // Call calls a contract with flags. diff --git a/pkg/core/interop/crypto/ecdsa_test.go b/pkg/core/interop/crypto/ecdsa_test.go index 9cfb370e1..fc6f70bfd 100644 --- a/pkg/core/interop/crypto/ecdsa_test.go +++ b/pkg/core/interop/crypto/ecdsa_test.go @@ -73,7 +73,7 @@ func initCheckMultisigVMNoArgs(container *transaction.Transaction) *vm.VM { trigger.Verification, fakechain.NewFakeChain(), dao.NewSimple(storage.NewMemoryStore(), false, false), - interop.DefaultBaseExecFee, native.DefaultStoragePrice, nil, nil, nil, + interop.DefaultBaseExecFee, native.DefaultStoragePrice, nil, nil, nil, nil, container, nil) ic.Container = container diff --git a/pkg/core/state/tokens.go b/pkg/core/state/tokens.go index c81d4e32d..81739ccdf 100644 --- a/pkg/core/state/tokens.go +++ b/pkg/core/state/tokens.go @@ -16,6 +16,8 @@ const TokenTransferBatchSize = 128 // TokenTransferLog is a serialized log of token transfers. type TokenTransferLog struct { Raw []byte + buf *bytes.Buffer + iow *io.BinWriter } // NEP17Transfer represents a single NEP-17 Transfer event. @@ -111,18 +113,30 @@ func (lg *TokenTransferLog) Append(tr io.Serializable) error { lg.Raw = append(lg.Raw, 0) } - b := bytes.NewBuffer(lg.Raw) - w := io.NewBinWriterFromIO(b) - - tr.EncodeBinary(w) - if w.Err != nil { - return w.Err + if lg.buf == nil { + lg.buf = bytes.NewBuffer(lg.Raw) } - lg.Raw = b.Bytes() + if lg.iow == nil { + lg.iow = io.NewBinWriterFromIO(lg.buf) + } + + tr.EncodeBinary(lg.iow) + if lg.iow.Err != nil { + return lg.iow.Err + } + lg.Raw = lg.buf.Bytes() lg.Raw[0]++ return nil } +// Reset resets the state of the log, clearing all entries, but keeping existing +// buffer for future writes. +func (lg *TokenTransferLog) Reset() { + lg.Raw = lg.Raw[:0] + lg.buf = nil + lg.iow = nil +} + // ForEachNEP11 iterates over a transfer log returning on the first error. func (lg *TokenTransferLog) ForEachNEP11(f func(*NEP11Transfer) (bool, error)) (bool, error) { if lg == nil || len(lg.Raw) == 0 { diff --git a/pkg/vm/bench_test.go b/pkg/vm/bench_test.go index c0403ea8b..d1ad21f24 100644 --- a/pkg/vm/bench_test.go +++ b/pkg/vm/bench_test.go @@ -48,3 +48,12 @@ func BenchmarkScriptPushPop(t *testing.B) { }) } } + +func BenchmarkIsSignatureContract(t *testing.B) { + b64script := "DCED2eixa9myLTNF1tTN4xvhw+HRYVMuPQzOy5Xs4utYM25BVuezJw==" + script, err := base64.StdEncoding.DecodeString(b64script) + require.NoError(t, err) + for n := 0; n < t.N; n++ { + _ = IsSignatureContract(script) + } +} diff --git a/pkg/vm/contract_checks.go b/pkg/vm/contract_checks.go index 1336765f4..51bb68040 100644 --- a/pkg/vm/contract_checks.go +++ b/pkg/vm/contract_checks.go @@ -118,17 +118,14 @@ func ParseSignatureContract(script []byte) ([]byte, bool) { return nil, false } - ctx := NewContext(script) - instr, param, err := ctx.Next() - if err != nil || instr != opcode.PUSHDATA1 || len(param) != 33 { - return nil, false + // We don't use Context for this simple case, it's more efficient this way. + if script[0] == byte(opcode.PUSHDATA1) && // PUSHDATA1 + script[1] == 33 && // with a public key parameter + script[35] == byte(opcode.SYSCALL) && // and a CheckSig SYSCALL. + binary.LittleEndian.Uint32(script[36:]) == verifyInteropID { + return script[2:35], true } - pub := param - instr, param, err = ctx.Next() - if err != nil || instr != opcode.SYSCALL || binary.LittleEndian.Uint32(param) != verifyInteropID { - return nil, false - } - return pub, true + return nil, false } // IsStandardContract checks whether the passed script is a signature or diff --git a/pkg/vm/vm.go b/pkg/vm/vm.go index d7b744aed..95e03a2c1 100644 --- a/pkg/vm/vm.go +++ b/pkg/vm/vm.go @@ -122,6 +122,24 @@ func (v *VM) SetPriceGetter(f func(opcode.Opcode, []byte) int64) { v.getPrice = f } +// Reset allows to reuse existing VM for subsequent executions making them somewhat +// more efficient. It reuses invocation and evaluation stacks as well as VM structure +// itself. +func (v *VM) Reset(t trigger.Type) { + v.state = NoneState + v.getPrice = nil + v.istack.elems = v.istack.elems[:0] + v.estack.elems = v.estack.elems[:0] + v.uncaughtException = nil + v.refs = 0 + v.gasConsumed = 0 + v.GasLimit = 0 + v.SyscallHandler = nil + v.LoadToken = nil + v.trigger = t + v.invTree = nil +} + // GasConsumed returns the amount of GAS consumed during execution. func (v *VM) GasConsumed() int64 { return v.gasConsumed