diff --git a/docs/rpc.md b/docs/rpc.md index ad22e75cb..4fb8c8a86 100644 --- a/docs/rpc.md +++ b/docs/rpc.md @@ -168,6 +168,28 @@ block. It can be removed in future versions, but at the moment you can use it to see how much GAS is burned with particular block (because system fees are burned). +#### `invokecontractverifyhistoric`, `invokefunctionhistoric` and `invokescripthistoric` calls + +These methods provide the ability of *historical* calls and accept block hash or +block index or stateroot hash as the first parameter and the list of parameters +that is the same as of `invokecontractverify`, `invokefunction` and +`invokescript` correspondingly. The historical call assumes that the contracts' +storage state has all its values got from MPT with the specified stateroot and +the transaction will be invoked using interop context with block of the specified +height. This allows to perform test invocation using the specified past chain +state. These methods may be useful for debugging purposes. + +Behavior note: any historical RPC call need the historical chain state to be +presented in the node storage, thus if the node keeps only latest MPT state +the historical call can not be handled properly.The historical calls only +guaranteed to correctly work on archival node that stores all MPT data. If a +node keeps the number of latest states and has the GC on (this setting +corresponds to the `RemoveUntraceableBlocks` set to `true`), then the behaviour +of historical RPC call is undefined. GC can always kick some data out of the +storage while the historical call is executing, thus keep in mind that the call +can be processed with `RemoveUntraceableBlocks` only with limitations on +available data. + #### `submitnotaryrequest` call This method can be used on P2P Notary enabled networks to submit new notary diff --git a/pkg/compiler/interop_test.go b/pkg/compiler/interop_test.go index 3243f4a1d..120e29709 100644 --- a/pkg/compiler/interop_test.go +++ b/pkg/compiler/interop_test.go @@ -201,7 +201,8 @@ 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)) + ic := interop.NewContext(trigger.Application, fc, dao.NewSimple(storage.NewMemoryStore(), false, false), + interop.DefaultBaseExecFee, native.DefaultStoragePrice, contractGetter, 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/basic_chain_test.go b/pkg/core/basic_chain_test.go index 480db1eb8..d460685bb 100644 --- a/pkg/core/basic_chain_test.go +++ b/pkg/core/basic_chain_test.go @@ -76,7 +76,7 @@ func TestCreateBasicChain(t *testing.T) { func initBasicChain(t *testing.T, e *neotest.Executor) { if !e.Chain.GetConfig().P2PSigExtensions { - t.Fatal("P2PSitExtensions should be enabled to init basic chain") + t.Fatal("P2PSigExtensions should be enabled to init basic chain") } const neoAmount = 99999000 diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index efbf57d0d..3776b4887 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -433,14 +433,9 @@ func (bc *Blockchain) init() error { return fmt.Errorf("can't init MPT at height %d: %w", bHeight, err) } - err = bc.contracts.NEO.InitializeCache(bc, bc.dao) + err = bc.initializeNativeCache(bc.blockHeight, bc.dao) if err != nil { - return fmt.Errorf("can't init cache for NEO native contract: %w", err) - } - - err = bc.contracts.Management.InitializeCache(bc.dao) - if err != nil { - return fmt.Errorf("can't init cache for Management native contract: %w", err) + return fmt.Errorf("can't init natives cache: %w", err) } // Check autogenerated native contracts' manifests and NEFs against the stored ones. @@ -575,15 +570,10 @@ func (bc *Blockchain) jumpToStateInternal(p uint32, stage stateJumpStage) error Root: block.PrevStateRoot, }) - err = bc.contracts.NEO.InitializeCache(bc, bc.dao) + err = bc.initializeNativeCache(block.Index, bc.dao) if err != nil { - return fmt.Errorf("can't init cache for NEO native contract: %w", err) + return fmt.Errorf("failed to initialize natives cache: %w", err) } - err = bc.contracts.Management.InitializeCache(bc.dao) - if err != nil { - return fmt.Errorf("can't init cache for Management native contract: %w", err) - } - bc.contracts.Designate.InitializeCache() if err := bc.updateExtensibleWhitelist(p); err != nil { return fmt.Errorf("failed to update extensible whitelist: %w", err) @@ -595,6 +585,33 @@ func (bc *Blockchain) jumpToStateInternal(p uint32, stage stateJumpStage) error return nil } +func (bc *Blockchain) initializeNativeCache(blockHeight uint32, d *dao.Simple) error { + err := bc.contracts.NEO.InitializeCache(blockHeight, d) + if err != nil { + return fmt.Errorf("can't init cache for NEO native contract: %w", err) + } + err = bc.contracts.Management.InitializeCache(d) + if err != nil { + return fmt.Errorf("can't init cache for Management native contract: %w", err) + } + err = bc.contracts.Designate.InitializeCache(d) + if err != nil { + return fmt.Errorf("can't init cache for Designation native contract: %w", err) + } + bc.contracts.Oracle.InitializeCache(d) + if bc.P2PSigExtensionsEnabled() { + err = bc.contracts.Notary.InitializeCache(d) + if err != nil { + return fmt.Errorf("can't init cache for Notary native contract: %w", err) + } + } + err = bc.contracts.Policy.InitializeCache(d) + if err != nil { + return fmt.Errorf("can't init cache for Policy native contract: %w", err) + } + return nil +} + // Run runs chain loop, it needs to be run as goroutine and executing it is // critical for correct Blockchain operation. func (bc *Blockchain) Run() { @@ -1220,14 +1237,14 @@ func (bc *Blockchain) updateExtensibleWhitelist(height uint32) error { return nil } - newList := []util.Uint160{bc.contracts.NEO.GetCommitteeAddress()} - nextVals := bc.contracts.NEO.GetNextBlockValidatorsInternal() + newList := []util.Uint160{bc.contracts.NEO.GetCommitteeAddress(bc.dao)} + nextVals := bc.contracts.NEO.GetNextBlockValidatorsInternal(bc.dao) script, err := smartcontract.CreateDefaultMultiSigRedeemScript(nextVals) if err != nil { return err } newList = append(newList, hash.Hash160(script)) - bc.updateExtensibleList(&newList, bc.contracts.NEO.GetNextBlockValidatorsInternal()) + bc.updateExtensibleList(&newList, bc.contracts.NEO.GetNextBlockValidatorsInternal(bc.dao)) if len(stateVals) > 0 { h, err := bc.contracts.Designate.GetLastDesignatedHash(bc.dao, noderoles.StateValidator) @@ -1451,12 +1468,12 @@ func (bc *Blockchain) ForEachNEP11Transfer(acc util.Uint160, newestTimestamp uin // GetNEP17Contracts returns the list of deployed NEP-17 contracts. func (bc *Blockchain) GetNEP17Contracts() []util.Uint160 { - return bc.contracts.Management.GetNEP17Contracts() + return bc.contracts.Management.GetNEP17Contracts(bc.dao) } // GetNEP11Contracts returns the list of deployed NEP-11 contracts. func (bc *Blockchain) GetNEP11Contracts() []util.Uint160 { - return bc.contracts.Management.GetNEP11Contracts() + return bc.contracts.Management.GetNEP11Contracts(bc.dao) } // GetTokenLastUpdated returns a set of contract ids with the corresponding last updated @@ -1823,7 +1840,7 @@ func (bc *Blockchain) ApplyPolicyToTxSet(txes []*transaction.Transaction) []*tra curVC := bc.config.GetNumOfCNs(bc.BlockHeight() + 1) if oldVC == nil || oldVC != curVC { m := smartcontract.GetDefaultHonestNodeCount(curVC) - verification, _ := smartcontract.CreateDefaultMultiSigRedeemScript(bc.contracts.NEO.GetNextBlockValidatorsInternal()) + verification, _ := smartcontract.CreateDefaultMultiSigRedeemScript(bc.contracts.NEO.GetNextBlockValidatorsInternal(bc.dao)) defaultWitness = transaction.Witness{ InvocationScript: make([]byte, 66*m), VerificationScript: verification, @@ -1939,7 +1956,7 @@ func (bc *Blockchain) verifyAndPoolTx(t *transaction.Transaction, pool *mempool. if err != nil { return err } - if err := bc.verifyTxAttributes(t, isPartialTx); err != nil { + if err := bc.verifyTxAttributes(bc.dao, t, isPartialTx); err != nil { return err } err = pool.Add(t, feer, data...) @@ -1963,11 +1980,11 @@ func (bc *Blockchain) verifyAndPoolTx(t *transaction.Transaction, pool *mempool. return nil } -func (bc *Blockchain) verifyTxAttributes(tx *transaction.Transaction, isPartialTx bool) error { +func (bc *Blockchain) verifyTxAttributes(d *dao.Simple, tx *transaction.Transaction, isPartialTx bool) error { for i := range tx.Attributes { switch attrType := tx.Attributes[i].Type; attrType { case transaction.HighPriority: - h := bc.contracts.NEO.GetCommitteeAddress() + h := bc.contracts.NEO.GetCommitteeAddress(d) if !tx.HasSigner(h) { return fmt.Errorf("%w: high priority tx is not signed by committee", ErrInvalidAttribute) } @@ -2061,7 +2078,7 @@ func (bc *Blockchain) IsTxStillRelevant(t *transaction.Transaction, txpool *memp } else if txpool.HasConflicts(t, bc) { return false } - if err := bc.verifyTxAttributes(t, isPartialTx); err != nil { + if err := bc.verifyTxAttributes(bc.dao, t, isPartialTx); err != nil { return false } for i := range t.Scripts { @@ -2119,19 +2136,19 @@ func (bc *Blockchain) PoolTxWithData(t *transaction.Transaction, data interface{ // GetCommittee returns the sorted list of public keys of nodes in committee. func (bc *Blockchain) GetCommittee() (keys.PublicKeys, error) { - pubs := bc.contracts.NEO.GetCommitteeMembers() + pubs := bc.contracts.NEO.GetCommitteeMembers(bc.dao) sort.Sort(pubs) return pubs, nil } // GetValidators returns current validators. func (bc *Blockchain) GetValidators() ([]*keys.PublicKey, error) { - return bc.contracts.NEO.ComputeNextBlockValidators(bc, bc.dao) + return bc.contracts.NEO.ComputeNextBlockValidators(bc.blockHeight, bc.dao) } // GetNextBlockValidators returns next block validators. func (bc *Blockchain) GetNextBlockValidators() ([]*keys.PublicKey, error) { - return bc.contracts.NEO.GetNextBlockValidatorsInternal(), nil + return bc.contracts.NEO.GetNextBlockValidatorsInternal(bc.dao), nil } // GetEnrollments returns all registered validators. @@ -2148,6 +2165,41 @@ func (bc *Blockchain) GetTestVM(t trigger.Type, tx *transaction.Transaction, b * return systemInterop } +// GetTestHistoricVM returns an interop context with VM set up for a test run. +func (bc *Blockchain) GetTestHistoricVM(t trigger.Type, tx *transaction.Transaction, b *block.Block) (*interop.Context, error) { + if bc.config.KeepOnlyLatestState { + return nil, errors.New("only latest state is supported") + } + if b == nil { + return nil, errors.New("block is mandatory to produce test historic VM") + } + var mode = mpt.ModeAll + if bc.config.RemoveUntraceableBlocks { + if b.Index < bc.BlockHeight()-bc.config.MaxTraceableBlocks { + return nil, fmt.Errorf("state for height %d is outdated and removed from the storage", b.Index) + } + mode |= mpt.ModeGCFlag + } + sr, err := bc.stateRoot.GetStateRoot(b.Index) + if err != nil { + return nil, fmt.Errorf("failed to retrieve stateroot for height %d: %w", b.Index, err) + } + s := mpt.NewTrieStore(sr.Root, mode, storage.NewPrivateMemCachedStore(bc.dao.Store)) + dTrie := dao.NewSimple(s, bc.config.StateRootInHeader, bc.config.P2PSigExtensions) + dTrie.Version = bc.dao.Version + // Initialize native cache before passing DAO to interop context constructor, because + // the constructor will call BaseExecFee/StoragePrice policy methods on the passed DAO. + err = bc.initializeNativeCache(b.Index, dTrie) + if err != nil { + 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) + return systemInterop, nil +} + // Various witness verification errors. var ( ErrWitnessHashMismatch = errors.New("witness hash mismatch") diff --git a/pkg/core/blockchain_core_test.go b/pkg/core/blockchain_core_test.go index fbfe43213..a0ccae047 100644 --- a/pkg/core/blockchain_core_test.go +++ b/pkg/core/blockchain_core_test.go @@ -11,7 +11,6 @@ import ( "github.com/nspcc-dev/neo-go/internal/testchain" "github.com/nspcc-dev/neo-go/pkg/config" "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/storage" "github.com/nspcc-dev/neo-go/pkg/core/transaction" @@ -329,7 +328,7 @@ func TestBlockchain_BaseExecFeeBaseStoragePrice_Compat(t *testing.T) { bc := newTestChain(t) check := func(t *testing.T) { - ic := bc.newInteropContext(trigger.Application, dao.NewSimple(storage.NewMemoryStore(), bc.config.StateRootInHeader, bc.config.P2PSigExtensions), bc.topBlock.Load().(*block.Block), nil) + ic := bc.newInteropContext(trigger.Application, bc.dao, bc.topBlock.Load().(*block.Block), nil) require.Equal(t, bc.GetBaseExecFee(), ic.BaseExecFee()) require.Equal(t, bc.GetStoragePrice(), ic.BaseStorageFee()) } diff --git a/pkg/core/blockchainer/blockchainer.go b/pkg/core/blockchainer/blockchainer.go index f5179245f..6e7a1fcac 100644 --- a/pkg/core/blockchainer/blockchainer.go +++ b/pkg/core/blockchainer/blockchainer.go @@ -60,6 +60,7 @@ type Blockchainer interface { GetStateModule() StateRoot GetStorageItem(id int32, key []byte) state.StorageItem GetTestVM(t trigger.Type, tx *transaction.Transaction, b *block.Block) *interop.Context + GetTestHistoricVM(t trigger.Type, tx *transaction.Transaction, b *block.Block) (*interop.Context, error) GetTransaction(util.Uint256) (*transaction.Transaction, uint32, error) SetOracle(service services.Oracle) mempool.Feer // fee interface diff --git a/pkg/core/blockchainer/state_root.go b/pkg/core/blockchainer/state_root.go index 0328c3a63..3b11f6c63 100644 --- a/pkg/core/blockchainer/state_root.go +++ b/pkg/core/blockchainer/state_root.go @@ -15,4 +15,5 @@ type StateRoot interface { GetState(root util.Uint256, key []byte) ([]byte, error) GetStateProof(root util.Uint256, key []byte) ([][]byte, error) GetStateRoot(height uint32) (*state.MPTRoot, error) + GetLatestStateHeight(root util.Uint256) (uint32, error) } diff --git a/pkg/core/dao/dao.go b/pkg/core/dao/dao.go index b178d2aaf..af268e679 100644 --- a/pkg/core/dao/dao.go +++ b/pkg/core/dao/dao.go @@ -7,6 +7,7 @@ import ( "errors" "fmt" iocore "io" + "sync" "github.com/nspcc-dev/neo-go/pkg/core/block" "github.com/nspcc-dev/neo-go/pkg/core/state" @@ -34,11 +35,28 @@ var ( type Simple struct { Version Version Store *storage.MemCachedStore + + nativeCacheLock sync.RWMutex + nativeCache map[int32]NativeContractCache + // nativeCachePS is the backend store that provides functionality to store + // and retrieve multi-tier native contract cache. The lowest Simple has its + // nativeCachePS set to nil. + nativeCachePS *Simple + private bool keyBuf []byte dataBuf *io.BufBinWriter } +// NativeContractCache is an interface representing cache for a native contract. +// Cache can be copied to create a wrapper around current DAO layer. Wrapped cache +// can be persisted to the underlying DAO native cache. +type NativeContractCache interface { + // Copy returns a copy of native cache item that can safely be changed within + // the subsequent DAO operations. + Copy() NativeContractCache +} + // NewSimple creates new simple dao using provided backend store. func NewSimple(backend storage.Store, stateRootInHeader bool, p2pSigExtensions bool) *Simple { st := storage.NewMemCachedStore(backend) @@ -52,7 +70,8 @@ func newSimple(st *storage.MemCachedStore, stateRootInHeader bool, p2pSigExtensi StateRootInHeader: stateRootInHeader, P2PSigExtensions: p2pSigExtensions, }, - Store: st, + Store: st, + nativeCache: make(map[int32]NativeContractCache), } } @@ -66,16 +85,27 @@ func (dao *Simple) GetBatch() *storage.MemBatch { func (dao *Simple) GetWrapped() *Simple { d := NewSimple(dao.Store, dao.Version.StateRootInHeader, dao.Version.P2PSigExtensions) d.Version = dao.Version + d.nativeCachePS = dao return d } // GetPrivate returns new DAO instance with another layer of private // MemCachedStore around the current DAO Store. func (dao *Simple) GetPrivate() *Simple { - d := &Simple{} - *d = *dao // Inherit everything... + d := &Simple{ + Version: dao.Version, + keyBuf: dao.keyBuf, + dataBuf: dao.dataBuf, + } // Inherit everything... d.Store = storage.NewPrivateMemCachedStore(dao.Store) // except storage, wrap another layer. d.private = true + d.nativeCachePS = dao + // Do not inherit cache from nativeCachePS; instead should create clear map: + // GetRWCache and GetROCache will retrieve cache from the underlying + // nativeCache if requested. The lowest underlying DAO MUST have its native + // cache initialized before access it, otherwise GetROCache and GetRWCache + // won't work properly. + d.nativeCache = make(map[int32]NativeContractCache) return d } @@ -809,6 +839,17 @@ func (dao *Simple) getDataBuf() *io.BufBinWriter { // Persist flushes all the changes made into the (supposedly) persistent // underlying store. It doesn't block accesses to DAO from other threads. func (dao *Simple) Persist() (int, error) { + if dao.nativeCachePS != nil { + if !dao.private { + dao.nativeCacheLock.Lock() + defer dao.nativeCacheLock.Unlock() + } + if !dao.nativeCachePS.private { + dao.nativeCachePS.nativeCacheLock.Lock() + defer dao.nativeCachePS.nativeCacheLock.Unlock() + } + dao.persistNativeCache() + } return dao.Store.Persist() } @@ -816,5 +857,77 @@ func (dao *Simple) Persist() (int, error) { // underlying store. It's a synchronous version of Persist that doesn't allow // other threads to work with DAO while flushing the Store. func (dao *Simple) PersistSync() (int, error) { + if dao.nativeCachePS != nil { + dao.nativeCacheLock.Lock() + dao.nativeCachePS.nativeCacheLock.Lock() + defer func() { + dao.nativeCachePS.nativeCacheLock.Unlock() + dao.nativeCacheLock.Unlock() + }() + dao.persistNativeCache() + } return dao.Store.PersistSync() } + +// persistNativeCache is internal unprotected method for native cache persisting. +// It does NO checks for nativeCachePS is not nil. +func (dao *Simple) persistNativeCache() { + lower := dao.nativeCachePS + for id, nativeCache := range dao.nativeCache { + lower.nativeCache[id] = nativeCache + } + dao.nativeCache = nil +} + +// GetROCache returns native contact cache. The cache CAN NOT be modified by +// the caller. It's the caller's duty to keep it unmodified. +func (dao *Simple) GetROCache(id int32) NativeContractCache { + if !dao.private { + dao.nativeCacheLock.RLock() + defer dao.nativeCacheLock.RUnlock() + } + + return dao.getCache(id, true) +} + +// GetRWCache returns native contact cache. The cache CAN BE safely modified +// by the caller. +func (dao *Simple) GetRWCache(id int32) NativeContractCache { + if !dao.private { + dao.nativeCacheLock.Lock() + defer dao.nativeCacheLock.Unlock() + } + + return dao.getCache(id, false) +} + +// getCache is an internal unlocked representation of GetROCache and GetRWCache. +func (dao *Simple) getCache(k int32, ro bool) NativeContractCache { + if itm, ok := dao.nativeCache[k]; ok { + // Don't need to create itm copy, because its value was already copied + // the first time it was retrieved from loser ps. + return itm + } + + if dao.nativeCachePS != nil { + if ro { + return dao.nativeCachePS.GetROCache(k) + } + v := dao.nativeCachePS.GetRWCache(k) + if v != nil { + // Create a copy here in order not to modify the existing cache. + cp := v.Copy() + dao.nativeCache[k] = cp + return cp + } + } + return nil +} + +// SetCache adds native contract cache to the cache map. +func (dao *Simple) SetCache(id int32, v NativeContractCache) { + dao.nativeCacheLock.Lock() + defer dao.nativeCacheLock.Unlock() + + dao.nativeCache[id] = v +} diff --git a/pkg/core/interop/context.go b/pkg/core/interop/context.go index 9217e8d58..351052918 100644 --- a/pkg/core/interop/context.go +++ b/pkg/core/interop/context.go @@ -13,6 +13,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/core/dao" "github.com/nspcc-dev/neo-go/pkg/core/interop/interopnames" "github.com/nspcc-dev/neo-go/pkg/core/state" + "github.com/nspcc-dev/neo-go/pkg/core/storage" "github.com/nspcc-dev/neo-go/pkg/core/transaction" "github.com/nspcc-dev/neo-go/pkg/crypto/hash" "github.com/nspcc-dev/neo-go/pkg/io" @@ -339,3 +340,31 @@ func (ic *Context) Exec() error { defer ic.Finalize() return ic.VM.Run() } + +// BlockHeight returns current block height got from Context's block if it's set. +func (ic *Context) BlockHeight() uint32 { + if ic.Block != nil { + return ic.Block.Index - 1 // Persisting block is not yet stored. + } + return ic.Chain.BlockHeight() +} + +// CurrentBlockHash returns current block hash got from Context's block if it's set. +func (ic *Context) CurrentBlockHash() util.Uint256 { + if ic.Block != nil { + return ic.Chain.GetHeaderHash(int(ic.Block.Index - 1)) // Persisting block is not yet stored. + } + return ic.Chain.CurrentBlockHash() +} + +// GetBlock returns block if it exists and available at the current Context's height. +func (ic *Context) GetBlock(hash util.Uint256) (*block.Block, error) { + block, err := ic.Chain.GetBlock(hash) + if err != nil { + return nil, err + } + if block.Index > ic.BlockHeight() { + return nil, storage.ErrKeyNotFound + } + return block, nil +} diff --git a/pkg/core/interop/contract/call.go b/pkg/core/interop/contract/call.go index 5c3b6eb0a..2afab663d 100644 --- a/pkg/core/interop/contract/call.go +++ b/pkg/core/interop/contract/call.go @@ -18,7 +18,7 @@ import ( ) type policyChecker interface { - IsBlockedInternal(*dao.Simple, util.Uint160) bool + IsBlocked(*dao.Simple, util.Uint160) bool } // LoadToken calls method specified by token id. @@ -97,7 +97,7 @@ func callExFromNative(ic *interop.Context, caller util.Uint160, cs *state.Contra for _, nc := range ic.Natives { if nc.Metadata().Name == nativenames.Policy { var pch = nc.(policyChecker) - if pch.IsBlockedInternal(ic.DAO, cs.Hash) { + if pch.IsBlocked(ic.DAO, cs.Hash) { return fmt.Errorf("contract %s is blocked", cs.Hash.StringLE()) } break diff --git a/pkg/core/interop_system_core_test.go b/pkg/core/interop_system_core_test.go index 5224c0e4c..2c7b34307 100644 --- a/pkg/core/interop_system_core_test.go +++ b/pkg/core/interop_system_core_test.go @@ -12,7 +12,6 @@ import ( "github.com/nspcc-dev/neo-go/pkg/config" "github.com/nspcc-dev/neo-go/pkg/config/netmode" "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/interop" "github.com/nspcc-dev/neo-go/pkg/core/interop/contract" "github.com/nspcc-dev/neo-go/pkg/core/interop/iterator" @@ -530,10 +529,10 @@ func TestStorageFind(t *testing.T) { // Helper functions to create VM, InteropContext, TX, Account, Contract. -func createVM(t *testing.T) (*vm.VM, *interop.Context, *Blockchain) { +func createVM(t testing.TB) (*vm.VM, *interop.Context, *Blockchain) { chain := newTestChain(t) context := chain.newInteropContext(trigger.Application, - dao.NewSimple(storage.NewMemoryStore(), chain.config.StateRootInHeader, chain.config.P2PSigExtensions), nil, nil) + chain.dao.GetWrapped(), nil, nil) v := context.SpawnVM() return v, context, chain } @@ -552,10 +551,7 @@ func createVMAndContractState(t testing.TB) (*vm.VM, *state.Contract, *interop.C }, } - chain := newTestChain(t) - d := dao.NewSimple(storage.NewMemoryStore(), chain.config.StateRootInHeader, chain.config.P2PSigExtensions) - context := chain.newInteropContext(trigger.Application, d, nil, nil) - v := context.SpawnVM() + v, context, chain := createVM(t) return v, contractState, context, chain } diff --git a/pkg/core/interops_test.go b/pkg/core/interops_test.go index ef4dde7bf..fb31c0e25 100644 --- a/pkg/core/interops_test.go +++ b/pkg/core/interops_test.go @@ -5,9 +5,7 @@ import ( "runtime" "testing" - "github.com/nspcc-dev/neo-go/pkg/core/dao" "github.com/nspcc-dev/neo-go/pkg/core/interop" - "github.com/nspcc-dev/neo-go/pkg/core/storage" "github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger" "github.com/nspcc-dev/neo-go/pkg/vm" "github.com/stretchr/testify/require" @@ -17,8 +15,7 @@ func testNonInterop(t *testing.T, value interface{}, f func(*interop.Context) er v := vm.New() v.Estack().PushVal(value) chain := newTestChain(t) - d := dao.NewSimple(storage.NewMemoryStore(), chain.config.StateRootInHeader, chain.config.P2PSigExtensions) - context := chain.newInteropContext(trigger.Application, d, nil, nil) + context := chain.newInteropContext(trigger.Application, chain.dao, nil, nil) context.VM = v require.Error(t, f(context)) } diff --git a/pkg/core/mpt/billet.go b/pkg/core/mpt/billet.go index af7edf2be..40ee35de0 100644 --- a/pkg/core/mpt/billet.go +++ b/pkg/core/mpt/billet.go @@ -205,7 +205,7 @@ func (b *Billet) incrementRefAndStore(h util.Uint256, bs []byte) { // returned from `process` function. It also replaces all HashNodes to their // "unhashed" counterparts until the stop condition is satisfied. func (b *Billet) Traverse(process func(pathToNode []byte, node Node, nodeBytes []byte) bool, ignoreStorageErr bool) error { - r, err := b.traverse(b.root, []byte{}, []byte{}, process, ignoreStorageErr) + r, err := b.traverse(b.root, []byte{}, []byte{}, process, ignoreStorageErr, false) if err != nil && !errors.Is(err, errStop) { return err } @@ -213,7 +213,7 @@ func (b *Billet) Traverse(process func(pathToNode []byte, node Node, nodeBytes [ return nil } -func (b *Billet) traverse(curr Node, path, from []byte, process func(pathToNode []byte, node Node, nodeBytes []byte) bool, ignoreStorageErr bool) (Node, error) { +func (b *Billet) traverse(curr Node, path, from []byte, process func(pathToNode []byte, node Node, nodeBytes []byte) bool, ignoreStorageErr bool, backwards bool) (Node, error) { if _, ok := curr.(EmptyNode); ok { // We're not interested in EmptyNodes, and they do not affect the // traversal process, thus remain them untouched. @@ -227,7 +227,7 @@ func (b *Billet) traverse(curr Node, path, from []byte, process func(pathToNode } return nil, err } - return b.traverse(r, path, from, process, ignoreStorageErr) + return b.traverse(r, path, from, process, ignoreStorageErr, backwards) } if len(from) == 0 { bytes := slice.Copy(curr.Bytes()) @@ -242,22 +242,36 @@ func (b *Billet) traverse(curr Node, path, from []byte, process func(pathToNode var ( startIndex byte endIndex byte = childrenCount + cmp = func(i int) bool { + return i < int(endIndex) + } + step = 1 ) + if backwards { + startIndex, endIndex = lastChild, startIndex + cmp = func(i int) bool { + return i >= int(endIndex) + } + step = -1 + } if len(from) != 0 { endIndex = lastChild + if backwards { + endIndex = 0 + } startIndex, from = splitPath(from) } - for i := startIndex; i < endIndex; i++ { + for i := int(startIndex); cmp(i); i += step { var newPath []byte if i == lastChild { newPath = path } else { - newPath = append(path, i) + newPath = append(path, byte(i)) } - if i != startIndex { + if byte(i) != startIndex { from = []byte{} } - r, err := b.traverse(n.Children[i], newPath, from, process, ignoreStorageErr) + r, err := b.traverse(n.Children[i], newPath, from, process, ignoreStorageErr, backwards) if err != nil { if !errors.Is(err, errStop) { return nil, err @@ -276,7 +290,7 @@ func (b *Billet) traverse(curr Node, path, from []byte, process func(pathToNode } else { return b.tryCollapseExtension(n), nil } - r, err := b.traverse(n.next, append(path, n.key...), from, process, ignoreStorageErr) + r, err := b.traverse(n.next, append(path, n.key...), from, process, ignoreStorageErr, backwards) if err != nil && !errors.Is(err, errStop) { return nil, err } diff --git a/pkg/core/mpt/trie.go b/pkg/core/mpt/trie.go index 608a39cfb..90352af3d 100644 --- a/pkg/core/mpt/trie.go +++ b/pkg/core/mpt/trie.go @@ -625,7 +625,7 @@ func (t *Trie) Find(prefix, from []byte, max int) ([]storage.KeyValue, error) { } return count >= max } - _, err = b.traverse(start, path, fromP, process, false) + _, err = b.traverse(start, path, fromP, process, false, false) if err != nil && !errors.Is(err, errStop) { return nil, err } diff --git a/pkg/core/mpt/trie_store.go b/pkg/core/mpt/trie_store.go new file mode 100644 index 000000000..b07cc99df --- /dev/null +++ b/pkg/core/mpt/trie_store.go @@ -0,0 +1,126 @@ +package mpt + +import ( + "bytes" + "errors" + "fmt" + + "github.com/nspcc-dev/neo-go/pkg/core/storage" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/util/slice" +) + +// TrieStore is an MPT-based storage implementation for storing and retrieving +// historic blockchain data. TrieStore is supposed to be used within transaction +// script invocations only, thus only contract storage related operations are +// supported. All storage-related operations are being performed using historical +// storage data retrieved from MPT state. TrieStore is read-only and does not +// support put-related operations, thus, it should always be wrapped into +// MemCachedStore for proper puts handling. TrieStore never changes the provided +// backend store. +type TrieStore struct { + trie *Trie +} + +// ErrForbiddenTrieStoreOperation is returned when operation is not supposed to +// be performed over MPT-based Store. +var ErrForbiddenTrieStoreOperation = errors.New("operation is not allowed to be performed over TrieStore") + +// NewTrieStore returns a new ready to use MPT-backed storage. +func NewTrieStore(root util.Uint256, mode TrieMode, backed storage.Store) *TrieStore { + cache, ok := backed.(*storage.MemCachedStore) + if !ok { + cache = storage.NewMemCachedStore(backed) + } + tr := NewTrie(NewHashNode(root), mode, cache) + return &TrieStore{ + trie: tr, + } +} + +// Get implements the Store interface. +func (m *TrieStore) Get(key []byte) ([]byte, error) { + if len(key) == 0 { + return nil, fmt.Errorf("%w: Get is supported only for contract storage items", ErrForbiddenTrieStoreOperation) + } + switch storage.KeyPrefix(key[0]) { + case storage.STStorage, storage.STTempStorage: + res, err := m.trie.Get(key[1:]) + if err != nil && errors.Is(err, ErrNotFound) { + // Mimic the real storage behaviour. + return nil, storage.ErrKeyNotFound + } + return res, err + default: + return nil, fmt.Errorf("%w: Get is supported only for contract storage items", ErrForbiddenTrieStoreOperation) + } +} + +// PutChangeSet implements the Store interface. +func (m *TrieStore) PutChangeSet(puts map[string][]byte, stor map[string][]byte) error { + // Only Get and Seek should be supported, as TrieStore is read-only and is always + // should be wrapped by MemCachedStore to properly support put operations (if any). + return fmt.Errorf("%w: PutChangeSet is not supported", ErrForbiddenTrieStoreOperation) +} + +// Seek implements the Store interface. +func (m *TrieStore) Seek(rng storage.SeekRange, f func(k, v []byte) bool) { + prefix := storage.KeyPrefix(rng.Prefix[0]) + if prefix != storage.STStorage && prefix != storage.STTempStorage { // Prefix is always non-empty. + panic(fmt.Errorf("%w: Seek is supported only for contract storage items", ErrForbiddenTrieStoreOperation)) + } + prefixP := toNibbles(rng.Prefix[1:]) + fromP := []byte{} + if len(rng.Start) > 0 { + fromP = toNibbles(rng.Start) + } + _, start, path, err := m.trie.getWithPath(m.trie.root, prefixP, false) + if err != nil { + // Failed to determine the start node => no matching items. + return + } + path = path[len(prefixP):] + + if len(fromP) > 0 { + if len(path) <= len(fromP) && bytes.HasPrefix(fromP, path) { + fromP = fromP[len(path):] + } else if len(path) > len(fromP) && bytes.HasPrefix(path, fromP) { + fromP = []byte{} + } else { + cmp := bytes.Compare(path, fromP) + if cmp < 0 == rng.Backwards { + // No matching items. + return + } + fromP = []byte{} + } + } + + b := NewBillet(m.trie.root.Hash(), m.trie.mode, 0, m.trie.Store) + process := func(pathToNode []byte, node Node, _ []byte) bool { + if leaf, ok := node.(*LeafNode); ok { + // (*Billet).traverse includes `from` path into the result if so. It's OK for Seek, so shouldn't be filtered out. + kv := storage.KeyValue{ + Key: append(slice.Copy(rng.Prefix), pathToNode...), // Do not cut prefix. + Value: slice.Copy(leaf.value), + } + return !f(kv.Key, kv.Value) // Should return whether to stop. + } + return false + } + _, err = b.traverse(start, path, fromP, process, false, rng.Backwards) + if err != nil && !errors.Is(err, errStop) { + panic(fmt.Errorf("failed to perform Seek operation on TrieStore: %w", err)) + } +} + +// SeekGC implements the Store interface. +func (m *TrieStore) SeekGC(rng storage.SeekRange, keep func(k, v []byte) bool) error { + return fmt.Errorf("%w: SeekGC is not supported", ErrForbiddenTrieStoreOperation) +} + +// Close implements the Store interface. +func (m *TrieStore) Close() error { + m.trie = nil + return nil +} diff --git a/pkg/core/mpt/trie_store_test.go b/pkg/core/mpt/trie_store_test.go new file mode 100644 index 000000000..063d23c73 --- /dev/null +++ b/pkg/core/mpt/trie_store_test.go @@ -0,0 +1,73 @@ +package mpt + +import ( + "bytes" + "testing" + + "github.com/nspcc-dev/neo-go/pkg/core/storage" + "github.com/stretchr/testify/require" +) + +func TestTrieStore_TestTrieOperations(t *testing.T) { + source := newTestTrie(t) + backed := source.Store + + st := NewTrieStore(source.root.Hash(), ModeAll, backed) + + t.Run("forbidden operations", func(t *testing.T) { + require.ErrorIs(t, st.SeekGC(storage.SeekRange{}, nil), ErrForbiddenTrieStoreOperation) + _, err := st.Get([]byte{byte(storage.STTokenTransferInfo)}) + require.ErrorIs(t, err, ErrForbiddenTrieStoreOperation) + require.ErrorIs(t, st.PutChangeSet(nil, nil), ErrForbiddenTrieStoreOperation) + }) + + t.Run("Get", func(t *testing.T) { + t.Run("good", func(t *testing.T) { + res, err := st.Get(append([]byte{byte(storage.STStorage)}, 0xAC, 0xae)) // leaf `hello` + require.NoError(t, err) + require.Equal(t, []byte("hello"), res) + }) + t.Run("bad path", func(t *testing.T) { + _, err := st.Get(append([]byte{byte(storage.STStorage)}, 0xAC, 0xa0)) // bad path + require.ErrorIs(t, err, storage.ErrKeyNotFound) + }) + t.Run("path to not-a-leaf", func(t *testing.T) { + _, err := st.Get(append([]byte{byte(storage.STStorage)}, 0xAC)) // path to extension + require.ErrorIs(t, err, storage.ErrKeyNotFound) + }) + }) + + t.Run("Seek", func(t *testing.T) { + check := func(t *testing.T, backwards bool) { + var res [][]byte + st.Seek(storage.SeekRange{ + Prefix: []byte{byte(storage.STStorage)}, + Start: nil, + Backwards: backwards, + }, func(k, v []byte) bool { + res = append(res, k) + return true + }) + require.Equal(t, 4, len(res)) + for i := 0; i < len(res); i++ { + require.Equal(t, byte(storage.STStorage), res[i][0]) + if i < len(res)-1 { + cmp := bytes.Compare(res[i], res[i+1]) + if backwards { + require.True(t, cmp > 0) + } else { + require.True(t, cmp < 0) + } + } + } + } + t.Run("good: over whole storage", func(t *testing.T) { + t.Run("forwards", func(t *testing.T) { + check(t, false) + }) + t.Run("backwards", func(t *testing.T) { + check(t, true) + }) + }) + }) +} diff --git a/pkg/core/native/contract.go b/pkg/core/native/contract.go index 13f8ae0d9..d6deaa375 100644 --- a/pkg/core/native/contract.go +++ b/pkg/core/native/contract.go @@ -76,7 +76,7 @@ func NewContracts(cfg config.ProtocolConfiguration) *Contracts { cs.Contracts = append(cs.Contracts, ledger) gas := newGAS(int64(cfg.InitialGASSupply), cfg.P2PSigExtensions) - neo := newNEO() + neo := newNEO(cfg) policy := newPolicy() neo.GAS = gas neo.Policy = policy diff --git a/pkg/core/native/designate.go b/pkg/core/native/designate.go index 7a33e667a..e57e3c598 100644 --- a/pkg/core/native/designate.go +++ b/pkg/core/native/designate.go @@ -3,6 +3,7 @@ package native import ( "encoding/binary" "errors" + "fmt" "math" "math/big" "sort" @@ -31,12 +32,6 @@ type Designate struct { interop.ContractMD NEO *NEO - rolesChangedFlag atomic.Value - oracles atomic.Value - stateVals atomic.Value - neofsAlphabet atomic.Value - notaries atomic.Value - // p2pSigExtensionsEnabled defines whether the P2P signature extensions logic is relevant. p2pSigExtensionsEnabled bool @@ -53,6 +48,16 @@ type roleData struct { height uint32 } +type DesignationCache struct { + // rolesChangedFlag shows whether any of designated nodes were changed within the current block. + // It is used to notify dependant services about updated node roles during PostPersist. + rolesChangedFlag bool + oracles roleData + stateVals roleData + neofsAlphabet roleData + notaries roleData +} + const ( designateContractID = -8 @@ -73,6 +78,22 @@ var ( ErrNoBlock = errors.New("no persisting block in the context") ) +var ( + _ interop.Contract = (*Designate)(nil) + _ dao.NativeContractCache = (*DesignationCache)(nil) +) + +// Copy implements NativeContractCache interface. +func (c *DesignationCache) Copy() dao.NativeContractCache { + cp := &DesignationCache{} + copyDesignationCache(c, cp) + return cp +} + +func copyDesignationCache(src, dst *DesignationCache) { + *dst = *src +} + func (s *Designate) isValidRole(r noderoles.Role) bool { return r == noderoles.Oracle || r == noderoles.StateValidator || r == noderoles.NeoFSAlphabet || (s.p2pSigExtensionsEnabled && r == noderoles.P2PNotary) @@ -102,8 +123,30 @@ func newDesignate(p2pSigExtensionsEnabled bool) *Designate { return s } -// Initialize initializes Oracle contract. +// Initialize initializes Designation contract. It is called once at native Management's OnPersist +// at the genesis block, and we can't properly fill the cache at this point, as there are no roles +// data in the storage. func (s *Designate) Initialize(ic *interop.Context) error { + cache := &DesignationCache{} + ic.DAO.SetCache(s.ID, cache) + return nil +} + +// InitializeCache fills native Designate cache from DAO. It is called at non-zero height, thus +// we can fetch the roles data right from the storage. +func (s *Designate) InitializeCache(d *dao.Simple) error { + cache := &DesignationCache{} + roles := []noderoles.Role{noderoles.Oracle, noderoles.NeoFSAlphabet, noderoles.StateValidator} + if s.p2pSigExtensionsEnabled { + roles = append(roles, noderoles.P2PNotary) + } + for _, r := range roles { + err := s.updateCachedRoleData(cache, d, r) + if err != nil { + return fmt.Errorf("failed to get nodes from storage for %d role: %w", r, err) + } + } + d.SetCache(s.ID, cache) return nil } @@ -114,26 +157,19 @@ func (s *Designate) OnPersist(ic *interop.Context) error { // PostPersist implements Contract interface. func (s *Designate) PostPersist(ic *interop.Context) error { - if !s.rolesChanged() { + cache := ic.DAO.GetRWCache(s.ID).(*DesignationCache) + if !cache.rolesChangedFlag { return nil } - if err := s.updateCachedRoleData(&s.oracles, ic.DAO, noderoles.Oracle); err != nil { - return err - } - if err := s.updateCachedRoleData(&s.stateVals, ic.DAO, noderoles.StateValidator); err != nil { - return err - } - if err := s.updateCachedRoleData(&s.neofsAlphabet, ic.DAO, noderoles.NeoFSAlphabet); err != nil { - return err - } + s.notifyRoleChanged(&cache.oracles, noderoles.Oracle) + s.notifyRoleChanged(&cache.stateVals, noderoles.StateValidator) + s.notifyRoleChanged(&cache.neofsAlphabet, noderoles.NeoFSAlphabet) if s.p2pSigExtensionsEnabled { - if err := s.updateCachedRoleData(&s.notaries, ic.DAO, noderoles.P2PNotary); err != nil { - return err - } + s.notifyRoleChanged(&cache.notaries, noderoles.P2PNotary) } - s.rolesChangedFlag.Store(false) + cache.rolesChangedFlag = false return nil } @@ -152,7 +188,7 @@ func (s *Designate) getDesignatedByRole(ic *interop.Context, args []stackitem.It panic(ErrInvalidIndex) } index := ind.Uint64() - if index > uint64(ic.Chain.BlockHeight()+1) { + if index > uint64(ic.BlockHeight()+1) { panic(ErrInvalidIndex) } pubs, _, err := s.GetDesignatedByRole(ic.DAO, r, uint32(index)) @@ -162,11 +198,6 @@ func (s *Designate) getDesignatedByRole(ic *interop.Context, args []stackitem.It return pubsToArray(pubs) } -func (s *Designate) rolesChanged() bool { - rc := s.rolesChangedFlag.Load() - return rc == nil || rc.(bool) -} - func (s *Designate) hashFromNodes(r noderoles.Role, nodes keys.PublicKeys) util.Uint160 { if len(nodes) == 0 { return util.Uint160{} @@ -181,47 +212,58 @@ func (s *Designate) hashFromNodes(r noderoles.Role, nodes keys.PublicKeys) util. return hash.Hash160(script) } -func (s *Designate) updateCachedRoleData(v *atomic.Value, d *dao.Simple, r noderoles.Role) error { - nodeKeys, height, err := s.GetDesignatedByRole(d, r, math.MaxUint32) +// updateCachedRoleData fetches the most recent role data from the storage and +// updates the given cache. +func (s *Designate) updateCachedRoleData(cache *DesignationCache, d *dao.Simple, r noderoles.Role) error { + var v *roleData + switch r { + case noderoles.Oracle: + v = &cache.oracles + case noderoles.StateValidator: + v = &cache.stateVals + case noderoles.NeoFSAlphabet: + v = &cache.neofsAlphabet + case noderoles.P2PNotary: + v = &cache.notaries + } + nodeKeys, height, err := s.getDesignatedByRoleFromStorage(d, r, math.MaxUint32) if err != nil { return err } - v.Store(&roleData{ - nodes: nodeKeys, - addr: s.hashFromNodes(r, nodeKeys), - height: height, - }) - switch r { - case noderoles.Oracle: - if orc, _ := s.OracleService.Load().(services.Oracle); orc != nil { - orc.UpdateOracleNodes(nodeKeys.Copy()) - } - case noderoles.P2PNotary: - if ntr, _ := s.NotaryService.Load().(services.Notary); ntr != nil { - ntr.UpdateNotaryNodes(nodeKeys.Copy()) - } - case noderoles.StateValidator: - if s.StateRootService != nil { - s.StateRootService.UpdateStateValidators(height, nodeKeys.Copy()) - } - } + v.nodes = nodeKeys + v.addr = s.hashFromNodes(r, nodeKeys) + v.height = height + cache.rolesChangedFlag = true return nil } -func (s *Designate) getCachedRoleData(r noderoles.Role) *roleData { - var val interface{} +func (s *Designate) notifyRoleChanged(v *roleData, r noderoles.Role) { switch r { case noderoles.Oracle: - val = s.oracles.Load() - case noderoles.StateValidator: - val = s.stateVals.Load() - case noderoles.NeoFSAlphabet: - val = s.neofsAlphabet.Load() + if orc, _ := s.OracleService.Load().(services.Oracle); orc != nil { + orc.UpdateOracleNodes(v.nodes.Copy()) + } case noderoles.P2PNotary: - val = s.notaries.Load() + if ntr, _ := s.NotaryService.Load().(services.Notary); ntr != nil { + ntr.UpdateNotaryNodes(v.nodes.Copy()) + } + case noderoles.StateValidator: + if s.StateRootService != nil { + s.StateRootService.UpdateStateValidators(v.height, v.nodes.Copy()) + } } - if val != nil { - return val.(*roleData) +} + +func getCachedRoleData(cache *DesignationCache, r noderoles.Role) *roleData { + switch r { + case noderoles.Oracle: + return &cache.oracles + case noderoles.StateValidator: + return &cache.stateVals + case noderoles.NeoFSAlphabet: + return &cache.neofsAlphabet + case noderoles.P2PNotary: + return &cache.notaries } return nil } @@ -231,17 +273,11 @@ func (s *Designate) GetLastDesignatedHash(d *dao.Simple, r noderoles.Role) (util if !s.isValidRole(r) { return util.Uint160{}, ErrInvalidRole } - if !s.rolesChanged() { - if val := s.getCachedRoleData(r); val != nil { - return val.addr, nil - } + cache := d.GetROCache(s.ID).(*DesignationCache) + if val := getCachedRoleData(cache, r); val != nil { + return val.addr, nil } - nodes, _, err := s.GetDesignatedByRole(d, r, math.MaxUint32) - if err != nil { - return util.Uint160{}, err - } - // We only have hashing defined for oracles now. - return s.hashFromNodes(r, nodes), nil + return util.Uint160{}, nil } // GetDesignatedByRole returns nodes for role r. @@ -249,11 +285,22 @@ func (s *Designate) GetDesignatedByRole(d *dao.Simple, r noderoles.Role, index u if !s.isValidRole(r) { return nil, 0, ErrInvalidRole } - if !s.rolesChanged() { - if val := s.getCachedRoleData(r); val != nil && val.height <= index { + cache := d.GetROCache(s.ID).(*DesignationCache) + if val := getCachedRoleData(cache, r); val != nil { + if val.height <= index { return val.nodes.Copy(), val.height, nil } + } else { + // Cache is always valid, thus if there's no cache then there's no designated nodes for this role. + return nil, 0, nil } + // Cache stores only latest designated nodes, so if the old info is requested, then we still need + // to search in the storage. + return s.getDesignatedByRoleFromStorage(d, r, index) +} + +// getDesignatedByRoleFromStorage returns nodes for role r from the storage. +func (s *Designate) getDesignatedByRoleFromStorage(d *dao.Simple, r noderoles.Role, index uint32) (keys.PublicKeys, uint32, error) { var ( ns NodeList bestIndex uint32 @@ -310,7 +357,7 @@ func (s *Designate) DesignateAsRole(ic *interop.Context, r noderoles.Role, pubs if !s.isValidRole(r) { return ErrInvalidRole } - h := s.NEO.GetCommitteeAddress() + h := s.NEO.GetCommitteeAddress(ic.DAO) if ok, err := runtime.CheckHashedWitness(ic, h); err != nil || !ok { return ErrInvalidWitness } @@ -327,12 +374,18 @@ func (s *Designate) DesignateAsRole(ic *interop.Context, r noderoles.Role, pubs } sort.Sort(pubs) nl := NodeList(pubs) - s.rolesChangedFlag.Store(true) + err := putConvertibleToDAO(s.ID, ic.DAO, key, &nl) if err != nil { return err } + cache := ic.DAO.GetRWCache(s.ID).(*DesignationCache) + err = s.updateCachedRoleData(cache, ic.DAO, r) + if err != nil { + return fmt.Errorf("failed to update Designation role data cache: %w", err) + } + ic.Notifications = append(ic.Notifications, state.NotificationEvent{ ScriptHash: s.Hash, Name: DesignationEventName, @@ -355,8 +408,3 @@ func (s *Designate) getRole(item stackitem.Item) (noderoles.Role, bool) { u := bi.Uint64() return noderoles.Role(u), u <= math.MaxUint8 && s.isValidRole(noderoles.Role(u)) } - -// InitializeCache invalidates native Designate cache. -func (s *Designate) InitializeCache() { - s.rolesChangedFlag.Store(true) -} diff --git a/pkg/core/native/interop.go b/pkg/core/native/interop.go index 3fd2ab57a..28fd6edc4 100644 --- a/pkg/core/native/interop.go +++ b/pkg/core/native/interop.go @@ -31,7 +31,7 @@ func Call(ic *interop.Context) error { if len(history) == 0 { return fmt.Errorf("native contract %s is disabled", c.Metadata().Name) } - if history[0] > ic.Chain.BlockHeight() { + if history[0] > ic.BlockHeight() { return fmt.Errorf("native contract %s is active after height = %d", c.Metadata().Name, history[0]) } m, ok := c.Metadata().GetMethodByOffset(ic.VM.Context().IP()) diff --git a/pkg/core/native/ledger.go b/pkg/core/native/ledger.go index 64506e506..dabd443a8 100644 --- a/pkg/core/native/ledger.go +++ b/pkg/core/native/ledger.go @@ -103,19 +103,19 @@ func (l *Ledger) PostPersist(ic *interop.Context) error { // currentHash implements currentHash SC method. func (l *Ledger) currentHash(ic *interop.Context, _ []stackitem.Item) stackitem.Item { - return stackitem.Make(ic.Chain.CurrentBlockHash().BytesBE()) + return stackitem.Make(ic.CurrentBlockHash().BytesBE()) } // currentIndex implements currentIndex SC method. func (l *Ledger) currentIndex(ic *interop.Context, _ []stackitem.Item) stackitem.Item { - return stackitem.Make(ic.Chain.BlockHeight()) + return stackitem.Make(ic.BlockHeight()) } // getBlock implements getBlock SC method. func (l *Ledger) getBlock(ic *interop.Context, params []stackitem.Item) stackitem.Item { - hash := getBlockHashFromItem(ic.Chain, params[0]) - block, err := ic.Chain.GetBlock(hash) - if err != nil || !isTraceableBlock(ic.Chain, block.Index) { + hash := getBlockHashFromItem(ic, params[0]) + block, err := ic.GetBlock(hash) + if err != nil || !isTraceableBlock(ic, block.Index) { return stackitem.Null{} } return BlockToStackItem(block) @@ -124,7 +124,7 @@ func (l *Ledger) getBlock(ic *interop.Context, params []stackitem.Item) stackite // getTransaction returns transaction to the SC. func (l *Ledger) getTransaction(ic *interop.Context, params []stackitem.Item) stackitem.Item { tx, h, err := getTransactionAndHeight(ic.DAO, params[0]) - if err != nil || !isTraceableBlock(ic.Chain, h) { + if err != nil || !isTraceableBlock(ic, h) { return stackitem.Null{} } return TransactionToStackItem(tx) @@ -133,7 +133,7 @@ func (l *Ledger) getTransaction(ic *interop.Context, params []stackitem.Item) st // getTransactionHeight returns transaction height to the SC. func (l *Ledger) getTransactionHeight(ic *interop.Context, params []stackitem.Item) stackitem.Item { _, h, err := getTransactionAndHeight(ic.DAO, params[0]) - if err != nil || !isTraceableBlock(ic.Chain, h) { + if err != nil || !isTraceableBlock(ic, h) { return stackitem.Make(-1) } return stackitem.Make(h) @@ -142,10 +142,10 @@ func (l *Ledger) getTransactionHeight(ic *interop.Context, params []stackitem.It // getTransactionFromBlock returns transaction with the given index from the // block with height or hash specified. func (l *Ledger) getTransactionFromBlock(ic *interop.Context, params []stackitem.Item) stackitem.Item { - hash := getBlockHashFromItem(ic.Chain, params[0]) + hash := getBlockHashFromItem(ic, params[0]) index := toUint32(params[1]) - block, err := ic.Chain.GetBlock(hash) - if err != nil || !isTraceableBlock(ic.Chain, block.Index) { + block, err := ic.GetBlock(hash) + if err != nil || !isTraceableBlock(ic, block.Index) { return stackitem.Null{} } if index >= uint32(len(block.Transactions)) { @@ -157,7 +157,7 @@ func (l *Ledger) getTransactionFromBlock(ic *interop.Context, params []stackitem // getTransactionSigners returns transaction signers to the SC. func (l *Ledger) getTransactionSigners(ic *interop.Context, params []stackitem.Item) stackitem.Item { tx, h, err := getTransactionAndHeight(ic.DAO, params[0]) - if err != nil || !isTraceableBlock(ic.Chain, h) { + if err != nil || !isTraceableBlock(ic, h) { return stackitem.Null{} } return SignersToStackItem(tx.Signers) @@ -170,7 +170,7 @@ func (l *Ledger) getTransactionVMState(ic *interop.Context, params []stackitem.I panic(err) } h, _, aer, err := ic.DAO.GetTxExecResult(hash) - if err != nil || !isTraceableBlock(ic.Chain, h) { + if err != nil || !isTraceableBlock(ic, h) { return stackitem.Make(vm.NoneState) } return stackitem.Make(aer.VMState) @@ -178,9 +178,9 @@ func (l *Ledger) getTransactionVMState(ic *interop.Context, params []stackitem.I // isTraceableBlock defines whether we're able to give information about // the block with index specified. -func isTraceableBlock(bc interop.Ledger, index uint32) bool { - height := bc.BlockHeight() - MaxTraceableBlocks := bc.GetConfig().MaxTraceableBlocks +func isTraceableBlock(ic *interop.Context, index uint32) bool { + height := ic.BlockHeight() + MaxTraceableBlocks := ic.Chain.GetConfig().MaxTraceableBlocks return index <= height && index+MaxTraceableBlocks > height } @@ -188,17 +188,17 @@ func isTraceableBlock(bc interop.Ledger, index uint32) bool { // Ledger if needed. Interop functions accept both block numbers and // block hashes as parameters, thus this function is needed. It's supposed to // be called within VM context, so it panics if anything goes wrong. -func getBlockHashFromItem(bc interop.Ledger, item stackitem.Item) util.Uint256 { +func getBlockHashFromItem(ic *interop.Context, item stackitem.Item) util.Uint256 { bigindex, err := item.TryInteger() if err == nil && bigindex.IsUint64() { index := bigindex.Uint64() if index > math.MaxUint32 { panic("bad block index") } - if uint32(index) > bc.BlockHeight() { + if uint32(index) > ic.BlockHeight() { panic(fmt.Errorf("no block with index %d", index)) } - return bc.GetHeaderHash(int(index)) + return ic.Chain.GetHeaderHash(int(index)) } hash, err := getUint256FromItem(item) if err != nil { diff --git a/pkg/core/native/management.go b/pkg/core/native/management.go index dc9257331..6ed98cd97 100644 --- a/pkg/core/native/management.go +++ b/pkg/core/native/management.go @@ -6,7 +6,6 @@ import ( "fmt" "math" "math/big" - "sync" "unicode/utf8" "github.com/nspcc-dev/neo-go/pkg/core/dao" @@ -30,8 +29,9 @@ import ( type Management struct { interop.ContractMD NEO *NEO +} - mtx sync.RWMutex +type ManagementCache struct { contracts map[util.Uint160]*state.Contract // nep11 is a map of NEP11-compliant contracts which is updated with every PostPersist. nep11 map[util.Uint160]struct{} @@ -57,6 +57,33 @@ var ( keyMinimumDeploymentFee = []byte{20} ) +var ( + _ interop.Contract = (*Management)(nil) + _ dao.NativeContractCache = (*ManagementCache)(nil) +) + +// Copy implements NativeContractCache interface. +func (c *ManagementCache) Copy() dao.NativeContractCache { + cp := &ManagementCache{ + contracts: make(map[util.Uint160]*state.Contract), + nep11: make(map[util.Uint160]struct{}), + nep17: make(map[util.Uint160]struct{}), + } + // Copy the whole set of contracts is too expensive. We will create a separate map + // holding the same set of pointers to contracts, and in case if some contract is + // supposed to be changed, Management will create the copy in-place. + for hash, ctr := range c.contracts { + cp.contracts[hash] = ctr + } + for hash := range c.nep17 { + cp.nep17[hash] = struct{}{} + } + for hash := range c.nep11 { + cp.nep11[hash] = struct{}{} + } + return cp +} + // MakeContractKey creates a key from account script hash. func MakeContractKey(h util.Uint160) []byte { return makeUint160Key(prefixContract, h) @@ -66,9 +93,6 @@ func MakeContractKey(h util.Uint160) []byte { func newManagement() *Management { var m = &Management{ ContractMD: *interop.NewContractMD(nativenames.Management, ManagementContractID), - contracts: make(map[util.Uint160]*state.Contract), - nep11: make(map[util.Uint160]struct{}), - nep17: make(map[util.Uint160]struct{}), } defer m.UpdateHash() @@ -146,25 +170,12 @@ func (m *Management) getContract(ic *interop.Context, args []stackitem.Item) sta // GetContract returns contract with given hash from given DAO. func (m *Management) GetContract(d *dao.Simple, hash util.Uint160) (*state.Contract, error) { - m.mtx.RLock() - cs, ok := m.contracts[hash] - m.mtx.RUnlock() + cache := d.GetROCache(m.ID).(*ManagementCache) + cs, ok := cache.contracts[hash] if !ok { return nil, storage.ErrKeyNotFound - } else if cs != nil { - return cs, nil } - return m.getContractFromDAO(d, hash) -} - -func (m *Management) getContractFromDAO(d *dao.Simple, hash util.Uint160) (*state.Contract, error) { - contract := new(state.Contract) - key := MakeContractKey(hash) - err := getConvertibleFromDAO(m.ID, d, key, contract) - if err != nil { - return nil, err - } - return contract, nil + return cs, nil } func getLimitedSlice(arg stackitem.Item, max int) ([]byte, error) { @@ -260,20 +271,23 @@ func (m *Management) deployWithData(ic *interop.Context, args []stackitem.Item) return contractToStack(newcontract) } -func (m *Management) markUpdated(h util.Uint160) { - m.mtx.Lock() - // Just set it to nil, to refresh cache in `PostPersist`. - m.contracts[h] = nil - m.mtx.Unlock() +func (m *Management) markUpdated(d *dao.Simple, hash util.Uint160, cs *state.Contract) { + cache := d.GetRWCache(m.ID).(*ManagementCache) + delete(cache.nep11, hash) + delete(cache.nep17, hash) + if cs == nil { + delete(cache.contracts, hash) + return + } + updateContractCache(cache, cs) } // Deploy creates contract's hash/ID and saves new contract into the given DAO. // It doesn't run _deploy method and doesn't emit notification. func (m *Management) Deploy(d *dao.Simple, sender util.Uint160, neff *nef.File, manif *manifest.Manifest) (*state.Contract, error) { h := state.CreateContractHash(sender, neff.Checksum, manif.Name) - key := MakeContractKey(h) - si := d.GetStorageItem(m.ID, key) - if si != nil { + _, err := m.GetContract(d, h) + if err == nil { return nil, errors.New("contract already exists") } id, err := m.getNextContractID(d) @@ -300,7 +314,6 @@ func (m *Management) Deploy(d *dao.Simple, sender util.Uint160, neff *nef.File, if err != nil { return nil, err } - m.markUpdated(newcontract.Hash) return newcontract, nil } @@ -340,7 +353,6 @@ func (m *Management) Update(d *dao.Simple, hash util.Uint160, neff *nef.File, ma contract = *oldcontract // Make a copy, don't ruin (potentially) cached contract. // if NEF was provided, update the contract script if neff != nil { - m.markUpdated(hash) contract.NEF = *neff } // if manifest was provided, update the contract manifest @@ -352,7 +364,6 @@ func (m *Management) Update(d *dao.Simple, hash util.Uint160, neff *nef.File, ma if err != nil { return nil, fmt.Errorf("invalid manifest: %w", err) } - m.markUpdated(hash) contract.Manifest = *manif } err = checkScriptAndMethods(contract.NEF.Script, contract.Manifest.ABI.Methods) @@ -393,7 +404,7 @@ func (m *Management) Destroy(d *dao.Simple, hash util.Uint160) error { d.DeleteStorageItem(contract.ID, k) return true }) - m.markUpdated(hash) + m.markUpdated(d, hash, nil) return nil } @@ -444,18 +455,19 @@ func (m *Management) Metadata() *interop.ContractMD { // updateContractCache saves contract in the common and NEP-related caches. It's // an internal method that must be called with m.mtx lock taken. -func (m *Management) updateContractCache(cs *state.Contract) { - m.contracts[cs.Hash] = cs +func updateContractCache(cache *ManagementCache, cs *state.Contract) { + cache.contracts[cs.Hash] = cs if cs.Manifest.IsStandardSupported(manifest.NEP11StandardName) { - m.nep11[cs.Hash] = struct{}{} + cache.nep11[cs.Hash] = struct{}{} } if cs.Manifest.IsStandardSupported(manifest.NEP17StandardName) { - m.nep17[cs.Hash] = struct{}{} + cache.nep17[cs.Hash] = struct{}{} } } // OnPersist implements Contract interface. func (m *Management) OnPersist(ic *interop.Context) error { + var cache *ManagementCache for _, native := range ic.Natives { md := native.Metadata() history := md.UpdateHistory @@ -466,16 +478,17 @@ func (m *Management) OnPersist(ic *interop.Context) error { cs := &state.Contract{ ContractBase: md.ContractBase, } - err := m.PutContractState(ic.DAO, cs) - if err != nil { - return err - } if err := native.Initialize(ic); err != nil { return fmt.Errorf("initializing %s native contract: %w", md.Name, err) } - m.mtx.Lock() - m.updateContractCache(cs) - m.mtx.Unlock() + err := m.putContractState(ic.DAO, cs, false) // Perform cache update manually. + if err != nil { + return err + } + if cache == nil { + cache = ic.DAO.GetRWCache(m.ID).(*ManagementCache) + } + updateContractCache(cache, cs) } return nil @@ -485,8 +498,11 @@ func (m *Management) OnPersist(ic *interop.Context) error { // Cache initialisation should be done apart from Initialize because Initialize is // called only when deploying native contracts. func (m *Management) InitializeCache(d *dao.Simple) error { - m.mtx.Lock() - defer m.mtx.Unlock() + cache := &ManagementCache{ + contracts: make(map[util.Uint160]*state.Contract), + nep11: make(map[util.Uint160]struct{}), + nep17: make(map[util.Uint160]struct{}), + } var initErr error d.Seek(m.ID, storage.SeekRange{Prefix: []byte{prefixContract}}, func(_, v []byte) bool { @@ -495,56 +511,42 @@ func (m *Management) InitializeCache(d *dao.Simple) error { if initErr != nil { return false } - m.updateContractCache(cs) + updateContractCache(cache, cs) return true }) - return initErr + if initErr != nil { + return initErr + } + d.SetCache(m.ID, cache) + return nil } // PostPersist implements Contract interface. func (m *Management) PostPersist(ic *interop.Context) error { - m.mtx.Lock() - for h, cs := range m.contracts { - if cs != nil { - continue - } - delete(m.nep11, h) - delete(m.nep17, h) - newCs, err := m.getContractFromDAO(ic.DAO, h) - if err != nil { - // Contract was destroyed. - delete(m.contracts, h) - continue - } - m.updateContractCache(newCs) - } - m.mtx.Unlock() return nil } // GetNEP11Contracts returns hashes of all deployed contracts that support NEP-11 standard. The list // is updated every PostPersist, so until PostPersist is called, the result for the previous block // is returned. -func (m *Management) GetNEP11Contracts() []util.Uint160 { - m.mtx.RLock() - result := make([]util.Uint160, 0, len(m.nep11)) - for h := range m.nep11 { +func (m *Management) GetNEP11Contracts(d *dao.Simple) []util.Uint160 { + cache := d.GetROCache(m.ID).(*ManagementCache) + result := make([]util.Uint160, 0, len(cache.nep11)) + for h := range cache.nep11 { result = append(result, h) } - m.mtx.RUnlock() return result } // GetNEP17Contracts returns hashes of all deployed contracts that support NEP-17 standard. The list // is updated every PostPersist, so until PostPersist is called, the result for the previous block // is returned. -func (m *Management) GetNEP17Contracts() []util.Uint160 { - m.mtx.RLock() - result := make([]util.Uint160, 0, len(m.nep17)) - for h := range m.nep17 { +func (m *Management) GetNEP17Contracts(d *dao.Simple) []util.Uint160 { + cache := d.GetROCache(m.ID).(*ManagementCache) + result := make([]util.Uint160, 0, len(cache.nep17)) + for h := range cache.nep17 { result = append(result, h) } - m.mtx.RUnlock() return result } @@ -552,16 +554,30 @@ func (m *Management) GetNEP17Contracts() []util.Uint160 { func (m *Management) Initialize(ic *interop.Context) error { setIntWithKey(m.ID, ic.DAO, keyMinimumDeploymentFee, defaultMinimumDeploymentFee) setIntWithKey(m.ID, ic.DAO, keyNextAvailableID, 1) + + cache := &ManagementCache{ + contracts: make(map[util.Uint160]*state.Contract), + nep11: make(map[util.Uint160]struct{}), + nep17: make(map[util.Uint160]struct{}), + } + ic.DAO.SetCache(m.ID, cache) return nil } // PutContractState saves given contract state into given DAO. func (m *Management) PutContractState(d *dao.Simple, cs *state.Contract) error { + return m.putContractState(d, cs, true) +} + +// putContractState is an internal PutContractState representation. +func (m *Management) putContractState(d *dao.Simple, cs *state.Contract, updateCache bool) error { key := MakeContractKey(cs.Hash) if err := putConvertibleToDAO(m.ID, d, key, cs); err != nil { return err } - m.markUpdated(cs.Hash) + if updateCache { + m.markUpdated(d, cs.Hash, cs) + } if cs.UpdateCounter != 0 { // Update. return nil } diff --git a/pkg/core/native/management_test.go b/pkg/core/native/management_test.go index 15b6cf0c1..bb669a961 100644 --- a/pkg/core/native/management_test.go +++ b/pkg/core/native/management_test.go @@ -89,8 +89,11 @@ func TestManagement_GetNEP17Contracts(t *testing.T) { d := dao.NewSimple(storage.NewMemoryStore(), false, false) err := mgmt.Initialize(&interop.Context{DAO: d}) require.NoError(t, err) + err = mgmt.InitializeCache(d) + require.NoError(t, err) - require.Empty(t, mgmt.GetNEP17Contracts()) + require.Empty(t, mgmt.GetNEP17Contracts(d)) + private := d.GetPrivate() // Deploy NEP-17 contract script := []byte{byte(opcode.RET)} @@ -104,29 +107,46 @@ func TestManagement_GetNEP17Contracts(t *testing.T) { Parameters: []manifest.Parameter{}, }) manif.SupportedStandards = []string{manifest.NEP17StandardName} - c1, err := mgmt.Deploy(d, sender, ne, manif) + c1, err := mgmt.Deploy(private, sender, ne, manif) require.NoError(t, err) - // PostPersist is not yet called, thus no NEP-17 contracts are expected - require.Empty(t, mgmt.GetNEP17Contracts()) + // c1 contract hash should be returned, as private DAO already contains changed cache. + require.Equal(t, []util.Uint160{c1.Hash}, mgmt.GetNEP17Contracts(private)) - // Call PostPersist, check c1 contract hash is returned - require.NoError(t, mgmt.PostPersist(&interop.Context{DAO: d})) - require.Equal(t, []util.Uint160{c1.Hash}, mgmt.GetNEP17Contracts()) + // Lower DAO still shouldn't contain c1, as no Persist was called. + require.Empty(t, mgmt.GetNEP17Contracts(d)) + + // Call Persist, check c1 contract hash is returned + _, err = private.Persist() + require.NoError(t, err) + require.Equal(t, []util.Uint160{c1.Hash}, mgmt.GetNEP17Contracts(d)) // Update contract + private = d.GetPrivate() manif.ABI.Methods = append(manif.ABI.Methods, manifest.Method{ Name: "dummy2", ReturnType: smartcontract.VoidType, Parameters: []manifest.Parameter{}, }) - c2, err := mgmt.Update(d, c1.Hash, ne, manif) + c1Updated, err := mgmt.Update(private, c1.Hash, ne, manif) require.NoError(t, err) + require.Equal(t, c1.Hash, c1Updated.Hash) - // No changes expected before PostPersist call. - require.Equal(t, []util.Uint160{c1.Hash}, mgmt.GetNEP17Contracts()) + // No changes expected in lower store. + require.Equal(t, []util.Uint160{c1.Hash}, mgmt.GetNEP17Contracts(d)) + c1Lower, err := mgmt.GetContract(d, c1.Hash) + require.NoError(t, err) + require.Equal(t, 1, len(c1Lower.Manifest.ABI.Methods)) + require.Equal(t, []util.Uint160{c1Updated.Hash}, mgmt.GetNEP17Contracts(private)) + c1Upper, err := mgmt.GetContract(private, c1Updated.Hash) + require.NoError(t, err) + require.Equal(t, 2, len(c1Upper.Manifest.ABI.Methods)) - // Call PostPersist, check c2 contract hash is returned - require.NoError(t, mgmt.PostPersist(&interop.Context{DAO: d})) - require.Equal(t, []util.Uint160{c2.Hash}, mgmt.GetNEP17Contracts()) + // Call Persist, check c1Updated state is returned from lower. + _, err = private.Persist() + require.NoError(t, err) + require.Equal(t, []util.Uint160{c1.Hash}, mgmt.GetNEP17Contracts(d)) + c1Lower, err = mgmt.GetContract(d, c1.Hash) + require.NoError(t, err) + require.Equal(t, 2, len(c1Lower.Manifest.ABI.Methods)) } diff --git a/pkg/core/native/native_gas.go b/pkg/core/native/native_gas.go index d02b672e5..14e83694c 100644 --- a/pkg/core/native/native_gas.go +++ b/pkg/core/native/native_gas.go @@ -108,7 +108,7 @@ func (g *GAS) OnPersist(ic *interop.Context) error { absAmount := big.NewInt(tx.SystemFee + tx.NetworkFee) g.burn(ic, tx.Sender(), absAmount) } - validators := g.NEO.GetNextBlockValidatorsInternal() + validators := g.NEO.GetNextBlockValidatorsInternal(ic.DAO) primary := validators[ic.Block.PrimaryIndex].GetScriptHash() var netFee int64 for _, tx := range ic.Block.Transactions { diff --git a/pkg/core/native/native_neo.go b/pkg/core/native/native_neo.go index 8ded7a5d5..84f66bee0 100644 --- a/pkg/core/native/native_neo.go +++ b/pkg/core/native/native_neo.go @@ -8,7 +8,6 @@ import ( "math/big" "sort" "strings" - "sync/atomic" "github.com/nspcc-dev/neo-go/pkg/config" "github.com/nspcc-dev/neo-go/pkg/core/dao" @@ -35,33 +34,33 @@ type NEO struct { GAS *GAS Policy *Policy - // gasPerBlock represents current value of generated gas per block. - // It is append-only and doesn't need to be copied when used. - gasPerBlock atomic.Value - gasPerBlockChanged atomic.Value + // Configuration and standby keys are set in constructor and then + // only read from. + cfg config.ProtocolConfiguration + standbyKeys keys.PublicKeys +} - registerPrice atomic.Value - registerPriceChanged atomic.Value +type NeoCache struct { + // gasPerBlock represents the history of generated gas per block. + gasPerBlock gasRecord - votesChanged atomic.Value - nextValidators atomic.Value - validators atomic.Value + registerPrice int64 + + votesChanged bool + nextValidators keys.PublicKeys + validators keys.PublicKeys // committee contains cached committee members and their votes. // It is updated once in a while depending on committee size // (every 28 blocks for mainnet). It's value // is always equal to value stored by `prefixCommittee`. - committee atomic.Value + committee keysWithVotes // committeeHash contains script hash of the committee. - committeeHash atomic.Value + committeeHash util.Uint160 // gasPerVoteCache contains last updated value of GAS per vote reward for candidates. // It is set in state-modifying methods only and read in `PostPersist` thus is not protected // by any mutex. gasPerVoteCache map[string]big.Int - // Configuration and standby keys are set during initialization and then - // only read from. - cfg config.ProtocolConfiguration - standbyKeys keys.PublicKeys } const ( @@ -105,6 +104,41 @@ var ( big100 = big.NewInt(100) ) +var ( + _ interop.Contract = (*NEO)(nil) + _ dao.NativeContractCache = (*NeoCache)(nil) +) + +// Copy implements NativeContractCache interface. +func (c *NeoCache) Copy() dao.NativeContractCache { + cp := &NeoCache{} + copyNeoCache(c, cp) + return cp +} + +func copyNeoCache(src, dst *NeoCache) { + dst.votesChanged = src.votesChanged + // Can safely omit copying because the new array is created each time + // validators list, nextValidators and committee are updated. + dst.nextValidators = src.nextValidators + dst.validators = src.validators + dst.committee = src.committee + dst.committeeHash = src.committeeHash + + dst.registerPrice = src.registerPrice + + // Can't omit copying because gasPerBlock is append-only, thus to be able to + // discard cache changes in case of FAULTed transaction we need a separate + // container for updated gasPerBlock values. + dst.gasPerBlock = make(gasRecord, len(src.gasPerBlock)) + copy(dst.gasPerBlock, src.gasPerBlock) + + dst.gasPerVoteCache = make(map[string]big.Int) + for k, v := range src.gasPerVoteCache { + dst.gasPerVoteCache[k] = v + } +} + // makeValidatorKey creates a key from account script hash. func makeValidatorKey(key *keys.PublicKey) []byte { b := key.Bytes() @@ -116,7 +150,7 @@ func makeValidatorKey(key *keys.PublicKey) []byte { } // newNEO returns NEO native contract. -func newNEO() *NEO { +func newNEO(cfg config.ProtocolConfiguration) *NEO { n := &NEO{} defer n.UpdateHash() @@ -128,13 +162,11 @@ func newNEO() *NEO { nep17.balFromBytes = n.balanceFromBytes n.nep17TokenNative = *nep17 - n.votesChanged.Store(true) - n.nextValidators.Store(keys.PublicKeys(nil)) - n.validators.Store(keys.PublicKeys(nil)) - n.committee.Store(keysWithVotes(nil)) - n.committeeHash.Store(util.Uint160{}) - n.registerPriceChanged.Store(true) - n.gasPerVoteCache = make(map[string]big.Int) + + err := n.initConfigCache(cfg) + if err != nil { + panic(fmt.Errorf("failed to initialize NEO config cache: %w", err)) + } desc := newDescriptor("unclaimedGas", smartcontract.IntegerType, manifest.NewParameter("account", smartcontract.Hash160Type), @@ -198,10 +230,6 @@ func newNEO() *NEO { // Initialize initializes NEO contract. func (n *NEO) Initialize(ic *interop.Context) error { - err := n.initConfigCache(ic.Chain) - if err != nil { - return nil - } if err := n.nep17TokenNative.Initialize(ic); err != nil { return err } @@ -211,9 +239,17 @@ func (n *NEO) Initialize(ic *interop.Context) error { return errors.New("already initialized") } + cache := &NeoCache{ + gasPerVoteCache: make(map[string]big.Int), + votesChanged: true, + } + + // We need cache to be present in DAO before the subsequent call to `mint`. + ic.DAO.SetCache(n.ID, cache) + committee0 := n.standbyKeys[:n.cfg.GetCommitteeSize(ic.Block.Index)] cvs := toKeysWithVotes(committee0) - err = n.updateCache(cvs, ic.Chain) + err := n.updateCache(cache, cvs, ic.BlockHeight()) if err != nil { return err } @@ -231,80 +267,79 @@ func (n *NEO) Initialize(ic *interop.Context) error { n.putGASRecord(ic.DAO, index, value) gr := &gasRecord{{Index: index, GASPerBlock: *value}} - n.gasPerBlock.Store(*gr) - n.gasPerBlockChanged.Store(false) + cache.gasPerBlock = *gr ic.DAO.PutStorageItem(n.ID, []byte{prefixVotersCount}, state.StorageItem{}) setIntWithKey(n.ID, ic.DAO, []byte{prefixRegisterPrice}, DefaultRegisterPrice) - n.registerPrice.Store(int64(DefaultRegisterPrice)) - n.registerPriceChanged.Store(false) + cache.registerPrice = int64(DefaultRegisterPrice) + return nil } // InitializeCache initializes all NEO cache with the proper values from storage. // Cache initialisation should be done apart from Initialize because Initialize is // called only when deploying native contracts. -func (n *NEO) InitializeCache(bc interop.Ledger, d *dao.Simple) error { - err := n.initConfigCache(bc) - if err != nil { - return nil +func (n *NEO) InitializeCache(blockHeight uint32, d *dao.Simple) error { + cache := &NeoCache{ + gasPerVoteCache: make(map[string]big.Int), + votesChanged: true, } + var committee = keysWithVotes{} si := d.GetStorageItem(n.ID, prefixCommittee) if err := committee.DecodeBytes(si); err != nil { - return err + return fmt.Errorf("failed to decode committee: %w", err) } - if err := n.updateCache(committee, bc); err != nil { - return err + if err := n.updateCache(cache, committee, blockHeight); err != nil { + return fmt.Errorf("failed to update cache: %w", err) } - n.gasPerBlock.Store(n.getSortedGASRecordFromDAO(d)) - n.gasPerBlockChanged.Store(false) + cache.gasPerBlock = n.getSortedGASRecordFromDAO(d) + cache.registerPrice = getIntWithKey(n.ID, d, []byte{prefixRegisterPrice}) + d.SetCache(n.ID, cache) return nil } -func (n *NEO) initConfigCache(bc interop.Ledger) error { +func (n *NEO) initConfigCache(cfg config.ProtocolConfiguration) error { var err error - n.cfg = bc.GetConfig() + n.cfg = cfg n.standbyKeys, err = keys.NewPublicKeysFromStrings(n.cfg.StandbyCommittee) return err } -func (n *NEO) updateCache(cvs keysWithVotes, bc interop.Ledger) error { - n.committee.Store(cvs) +func (n *NEO) updateCache(cache *NeoCache, cvs keysWithVotes, blockHeight uint32) error { + cache.committee = cvs - var committee = n.GetCommitteeMembers() + var committee = getCommitteeMembers(cache) script, err := smartcontract.CreateMajorityMultiSigRedeemScript(committee.Copy()) if err != nil { return err } - n.committeeHash.Store(hash.Hash160(script)) + cache.committeeHash = hash.Hash160(script) - nextVals := committee[:n.cfg.GetNumOfCNs(bc.BlockHeight()+1)].Copy() + nextVals := committee[:n.cfg.GetNumOfCNs(blockHeight+1)].Copy() sort.Sort(nextVals) - n.nextValidators.Store(nextVals) + cache.nextValidators = nextVals return nil } -func (n *NEO) updateCommittee(ic *interop.Context) error { - votesChanged := n.votesChanged.Load().(bool) - if !votesChanged { +func (n *NEO) updateCommittee(cache *NeoCache, ic *interop.Context) error { + if !cache.votesChanged { // We need to put in storage anyway, as it affects dumps - committee := n.committee.Load().(keysWithVotes) - ic.DAO.PutStorageItem(n.ID, prefixCommittee, committee.Bytes()) + ic.DAO.PutStorageItem(n.ID, prefixCommittee, cache.committee.Bytes()) return nil } - _, cvs, err := n.computeCommitteeMembers(ic.Chain, ic.DAO) + _, cvs, err := n.computeCommitteeMembers(ic.BlockHeight(), ic.DAO) if err != nil { return err } - if err := n.updateCache(cvs, ic.Chain); err != nil { + if err := n.updateCache(cache, cvs, ic.BlockHeight()); err != nil { return err } - n.votesChanged.Store(false) + cache.votesChanged = false ic.DAO.PutStorageItem(n.ID, prefixCommittee, cvs.Bytes()) return nil } @@ -312,13 +347,14 @@ func (n *NEO) updateCommittee(ic *interop.Context) error { // OnPersist implements Contract interface. func (n *NEO) OnPersist(ic *interop.Context) error { if n.cfg.ShouldUpdateCommitteeAt(ic.Block.Index) { - oldKeys := n.nextValidators.Load().(keys.PublicKeys) - oldCom := n.committee.Load().(keysWithVotes) + cache := ic.DAO.GetRWCache(n.ID).(*NeoCache) + oldKeys := cache.nextValidators + oldCom := cache.committee if n.cfg.GetNumOfCNs(ic.Block.Index) != len(oldKeys) || n.cfg.GetCommitteeSize(ic.Block.Index) != len(oldCom) { - n.votesChanged.Store(true) + cache.votesChanged = true } - if err := n.updateCommittee(ic); err != nil { + if err := n.updateCommittee(cache, ic); err != nil { return err } } @@ -328,7 +364,8 @@ func (n *NEO) OnPersist(ic *interop.Context) error { // PostPersist implements Contract interface. func (n *NEO) PostPersist(ic *interop.Context) error { gas := n.GetGASPerBlock(ic.DAO, ic.Block.Index) - pubs := n.GetCommitteeMembers() + cache := ic.DAO.GetROCache(n.ID).(*NeoCache) + pubs := getCommitteeMembers(cache) committeeSize := n.cfg.GetCommitteeSize(ic.Block.Index) index := int(ic.Block.Index) % committeeSize committeeReward := new(big.Int).Mul(gas, bigCommitteeRewardRatio) @@ -342,8 +379,11 @@ func (n *NEO) PostPersist(ic *interop.Context) error { voterReward.Div(voterReward, big.NewInt(int64(committeeSize+validatorsCount))) voterReward.Div(voterReward, big100) - var cs = n.committee.Load().(keysWithVotes) - var key = make([]byte, 38) + var ( + cs = cache.committee + isCacheRW bool + key = make([]byte, 38) + ) for i := range cs { if cs[i].Votes.Sign() > 0 { var tmp = new(big.Int) @@ -358,7 +398,7 @@ func (n *NEO) PostPersist(ic *interop.Context) error { key = makeVoterKey([]byte(cs[i].Key), key) var r *big.Int - if g, ok := n.gasPerVoteCache[cs[i].Key]; ok { + if g, ok := cache.gasPerVoteCache[cs[i].Key]; ok { r = &g } else { reward := n.getGASPerVote(ic.DAO, key[:34], []uint32{ic.Block.Index + 1}) @@ -367,22 +407,16 @@ func (n *NEO) PostPersist(ic *interop.Context) error { tmp.Add(tmp, r) binary.BigEndian.PutUint32(key[34:], ic.Block.Index+1) - n.gasPerVoteCache[cs[i].Key] = *tmp + if !isCacheRW { + cache = ic.DAO.GetRWCache(n.ID).(*NeoCache) + isCacheRW = true + } + cache.gasPerVoteCache[cs[i].Key] = *tmp ic.DAO.PutStorageItem(n.ID, key, bigint.ToBytes(tmp)) } } } - if n.gasPerBlockChanged.Load().(bool) { - n.gasPerBlock.Store(n.getSortedGASRecordFromDAO(ic.DAO)) - n.gasPerBlockChanged.Store(false) - } - - if n.registerPriceChanged.Load().(bool) { - p := getIntWithKey(n.ID, ic.DAO, []byte{prefixRegisterPrice}) - n.registerPrice.Store(p) - n.registerPriceChanged.Store(false) - } return nil } @@ -504,28 +538,25 @@ func (n *NEO) getSortedGASRecordFromDAO(d *dao.Simple) gasRecord { // GetGASPerBlock returns gas generated for block with provided index. func (n *NEO) GetGASPerBlock(d *dao.Simple, index uint32) *big.Int { - var gr gasRecord - if n.gasPerBlockChanged.Load().(bool) { - gr = n.getSortedGASRecordFromDAO(d) - } else { - gr = n.gasPerBlock.Load().(gasRecord) - } + cache := d.GetROCache(n.ID).(*NeoCache) + gr := cache.gasPerBlock for i := len(gr) - 1; i >= 0; i-- { if gr[i].Index <= index { g := gr[i].GASPerBlock return &g } } - panic("contract not initialized") + panic("NEO cache not initialized") } // GetCommitteeAddress returns address of the committee. -func (n *NEO) GetCommitteeAddress() util.Uint160 { - return n.committeeHash.Load().(util.Uint160) +func (n *NEO) GetCommitteeAddress(d *dao.Simple) util.Uint160 { + cache := d.GetROCache(n.ID).(*NeoCache) + return cache.committeeHash } func (n *NEO) checkCommittee(ic *interop.Context) bool { - ok, err := runtime.CheckHashedWitness(ic, n.GetCommitteeAddress()) + ok, err := runtime.CheckHashedWitness(ic, n.GetCommitteeAddress(ic.DAO)) if err != nil { panic(err) } @@ -549,8 +580,12 @@ func (n *NEO) SetGASPerBlock(ic *interop.Context, index uint32, gas *big.Int) er if !n.checkCommittee(ic) { return errors.New("invalid committee signature") } - n.gasPerBlockChanged.Store(true) n.putGASRecord(ic.DAO, index, gas) + cache := ic.DAO.GetRWCache(n.ID).(*NeoCache) + cache.gasPerBlock = append(cache.gasPerBlock, gasIndexPair{ + Index: index, + GASPerBlock: *gas, + }) return nil } @@ -559,10 +594,8 @@ func (n *NEO) getRegisterPrice(ic *interop.Context, _ []stackitem.Item) stackite } func (n *NEO) getRegisterPriceInternal(d *dao.Simple) int64 { - if !n.registerPriceChanged.Load().(bool) { - return n.registerPrice.Load().(int64) - } - return getIntWithKey(n.ID, d, []byte{prefixRegisterPrice}) + cache := d.GetROCache(n.ID).(*NeoCache) + return cache.registerPrice } func (n *NEO) setRegisterPrice(ic *interop.Context, args []stackitem.Item) stackitem.Item { @@ -575,13 +608,14 @@ func (n *NEO) setRegisterPrice(ic *interop.Context, args []stackitem.Item) stack } setIntWithKey(n.ID, ic.DAO, []byte{prefixRegisterPrice}, price.Int64()) - n.registerPriceChanged.Store(true) + cache := ic.DAO.GetRWCache(n.ID).(*NeoCache) + cache.registerPrice = price.Int64() return stackitem.Null{} } -func (n *NEO) dropCandidateIfZero(d *dao.Simple, pub *keys.PublicKey, c *candidate) (bool, error) { +func (n *NEO) dropCandidateIfZero(d *dao.Simple, cache *NeoCache, pub *keys.PublicKey, c *candidate) bool { if c.Registered || c.Votes.Sign() != 0 { - return false, nil + return false } d.DeleteStorageItem(n.ID, makeValidatorKey(pub)) @@ -590,9 +624,9 @@ func (n *NEO) dropCandidateIfZero(d *dao.Simple, pub *keys.PublicKey, c *candida d.DeleteStorageItem(n.ID, append(voterKey, k...)) // d.Seek cuts prefix, thus need to append it again. return true }) - delete(n.gasPerVoteCache, string(voterKey)) + delete(cache.gasPerVoteCache, string(voterKey)) - return true, nil + return true } func makeVoterKey(pub []byte, prealloc ...[]byte) []byte { @@ -644,12 +678,8 @@ func (n *NEO) CalculateNEOHolderReward(d *dao.Simple, value *big.Int, start, end } else if value.Sign() < 0 { return nil, errors.New("negative value") } - var gr gasRecord - if !n.gasPerBlockChanged.Load().(bool) { - gr = n.gasPerBlock.Load().(gasRecord) - } else { - gr = n.getSortedGASRecordFromDAO(d) - } + cache := d.GetROCache(n.ID).(*NeoCache) + gr := cache.gasPerBlock var sum, tmp big.Int for i := len(gr) - 1; i >= 0; i-- { if gr[i].Index >= end { @@ -719,12 +749,13 @@ func (n *NEO) UnregisterCandidateInternal(ic *interop.Context, pub *keys.PublicK if si == nil { return nil } - n.validators.Store(keys.PublicKeys(nil)) + cache := ic.DAO.GetRWCache(n.ID).(*NeoCache) + cache.validators = nil c := new(candidate).FromBytes(si) c.Registered = false - ok, err := n.dropCandidateIfZero(ic.DAO, pub, c) + ok := n.dropCandidateIfZero(ic.DAO, cache, pub, c) if ok { - return err + return nil } return putConvertibleToDAO(n.ID, ic.DAO, key, c) } @@ -798,7 +829,8 @@ func (n *NEO) VoteInternal(ic *interop.Context, h util.Uint160, pub *keys.Public // ModifyAccountVotes modifies votes of the specified account by value (can be negative). // typ specifies if this modify is occurring during transfer or vote (with old or new validator). func (n *NEO) ModifyAccountVotes(acc *state.NEOBalance, d *dao.Simple, value *big.Int, isNewVote bool) error { - n.votesChanged.Store(true) + cache := d.GetRWCache(n.ID).(*NeoCache) + cache.votesChanged = true if acc.VoteTo != nil { key := makeValidatorKey(acc.VoteTo) si := d.GetStorageItem(n.ID, key) @@ -808,12 +840,12 @@ func (n *NEO) ModifyAccountVotes(acc *state.NEOBalance, d *dao.Simple, value *bi cd := new(candidate).FromBytes(si) cd.Votes.Add(&cd.Votes, value) if !isNewVote { - ok, err := n.dropCandidateIfZero(d, acc.VoteTo, cd) + ok := n.dropCandidateIfZero(d, cache, acc.VoteTo, cd) if ok { - return err + return nil } } - n.validators.Store(keys.PublicKeys(nil)) + cache.validators = nil return putConvertibleToDAO(n.ID, d, key, cd) } return nil @@ -825,7 +857,7 @@ func (n *NEO) getCandidates(d *dao.Simple, sortByKey bool) ([]keyWithVotes, erro d.Seek(n.ID, storage.SeekRange{Prefix: []byte{prefixCandidate}}, func(k, v []byte) bool { c := new(candidate).FromBytes(v) emit.CheckSig(buf.BinWriter, k) - if c.Registered && !n.Policy.IsBlockedInternal(d, hash.Hash160(buf.Bytes())) { + if c.Registered && !n.Policy.IsBlocked(d, hash.Hash160(buf.Bytes())) { arr = append(arr, keyWithVotes{Key: string(k), Votes: &c.Votes}) } buf.Reset() @@ -906,23 +938,27 @@ func (n *NEO) getAccountState(ic *interop.Context, args []stackitem.Item) stacki } // ComputeNextBlockValidators returns an actual list of current validators. -func (n *NEO) ComputeNextBlockValidators(bc interop.Ledger, d *dao.Simple) (keys.PublicKeys, error) { - numOfCNs := n.cfg.GetNumOfCNs(bc.BlockHeight() + 1) - if vals := n.validators.Load().(keys.PublicKeys); vals != nil && numOfCNs == len(vals) { +func (n *NEO) ComputeNextBlockValidators(blockHeight uint32, d *dao.Simple) (keys.PublicKeys, error) { + numOfCNs := n.cfg.GetNumOfCNs(blockHeight + 1) + // Most of the time it should be OK with RO cache, thus try to retrieve + // validators without RW cache creation to avoid cached values copying. + cache := d.GetROCache(n.ID).(*NeoCache) + if vals := cache.validators; vals != nil && numOfCNs == len(vals) { return vals.Copy(), nil } - result, _, err := n.computeCommitteeMembers(bc, d) + cache = d.GetRWCache(n.ID).(*NeoCache) + result, _, err := n.computeCommitteeMembers(blockHeight, d) if err != nil { return nil, err } result = result[:numOfCNs] sort.Sort(result) - n.validators.Store(result) + cache.validators = result return result, nil } func (n *NEO) getCommittee(ic *interop.Context, _ []stackitem.Item) stackitem.Item { - pubs := n.GetCommitteeMembers() + pubs := n.GetCommitteeMembers(ic.DAO) sort.Sort(pubs) return pubsToArray(pubs) } @@ -941,8 +977,13 @@ func (n *NEO) modifyVoterTurnout(d *dao.Simple, amount *big.Int) error { } // GetCommitteeMembers returns public keys of nodes in committee using cached value. -func (n *NEO) GetCommitteeMembers() keys.PublicKeys { - var cvs = n.committee.Load().(keysWithVotes) +func (n *NEO) GetCommitteeMembers(d *dao.Simple) keys.PublicKeys { + cache := d.GetROCache(n.ID).(*NeoCache) + return getCommitteeMembers(cache) +} + +func getCommitteeMembers(cache *NeoCache) keys.PublicKeys { + var cvs = cache.committee var committee = make(keys.PublicKeys, len(cvs)) var err error for i := range committee { @@ -965,7 +1006,7 @@ func toKeysWithVotes(pubs keys.PublicKeys) keysWithVotes { } // computeCommitteeMembers returns public keys of nodes in committee. -func (n *NEO) computeCommitteeMembers(bc interop.Ledger, d *dao.Simple) (keys.PublicKeys, keysWithVotes, error) { +func (n *NEO) computeCommitteeMembers(blockHeight uint32, d *dao.Simple) (keys.PublicKeys, keysWithVotes, error) { key := []byte{prefixVotersCount} si := d.GetStorageItem(n.ID, key) if si == nil { @@ -977,7 +1018,7 @@ func (n *NEO) computeCommitteeMembers(bc interop.Ledger, d *dao.Simple) (keys.Pu _, totalSupply := n.getTotalSupply(d) voterTurnout := votersCount.Div(votersCount, totalSupply) - count := n.cfg.GetCommitteeSize(bc.BlockHeight() + 1) + count := n.cfg.GetCommitteeSize(blockHeight + 1) // Can be sorted and/or returned to outside users, thus needs to be copied. sbVals := keys.PublicKeys(n.standbyKeys[:count]).Copy() cs, err := n.getCandidates(d, false) @@ -1011,13 +1052,14 @@ func (n *NEO) computeCommitteeMembers(bc interop.Ledger, d *dao.Simple) (keys.Pu } func (n *NEO) getNextBlockValidators(ic *interop.Context, _ []stackitem.Item) stackitem.Item { - result := n.GetNextBlockValidatorsInternal() + result := n.GetNextBlockValidatorsInternal(ic.DAO) return pubsToArray(result) } // GetNextBlockValidatorsInternal returns next block validators. -func (n *NEO) GetNextBlockValidatorsInternal() keys.PublicKeys { - return n.nextValidators.Load().(keys.PublicKeys).Copy() +func (n *NEO) GetNextBlockValidatorsInternal(d *dao.Simple) keys.PublicKeys { + cache := d.GetROCache(n.ID).(*NeoCache) + return cache.nextValidators.Copy() } // BalanceOf returns native NEO token balance for the acc. diff --git a/pkg/core/native/native_test/common_test.go b/pkg/core/native/native_test/common_test.go index 851b18e16..53a927a52 100644 --- a/pkg/core/native/native_test/common_test.go +++ b/pkg/core/native/native_test/common_test.go @@ -8,9 +8,12 @@ import ( "github.com/nspcc-dev/neo-go/pkg/core/native/noderoles" "github.com/nspcc-dev/neo-go/pkg/core/state" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" - + "github.com/nspcc-dev/neo-go/pkg/io" "github.com/nspcc-dev/neo-go/pkg/neotest" "github.com/nspcc-dev/neo-go/pkg/neotest/chain" + "github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag" + "github.com/nspcc-dev/neo-go/pkg/vm/emit" + "github.com/nspcc-dev/neo-go/pkg/vm/opcode" "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" "github.com/stretchr/testify/require" ) @@ -77,6 +80,38 @@ func testGetSet(t *testing.T, c *neotest.ContractInvoker, name string, defaultVa }) } +func testGetSetCache(t *testing.T, c *neotest.ContractInvoker, name string, defaultValue int64) { + getName := "get" + name + setName := "set" + name + + committeeInvoker := c.WithSigners(c.Committee) + + newVal := defaultValue - 1 + + // Change fee, abort the transaction and check that contract cache wasn't persisted + // for FAULTed tx at the same block. + w := io.NewBufBinWriter() + emit.AppCall(w.BinWriter, committeeInvoker.Hash, setName, callflag.All, newVal) + emit.Opcodes(w.BinWriter, opcode.ABORT) + tx1 := committeeInvoker.PrepareInvocation(t, w.Bytes(), committeeInvoker.Signers) + tx2 := committeeInvoker.PrepareInvoke(t, getName) + committeeInvoker.AddNewBlock(t, tx1, tx2) + committeeInvoker.CheckFault(t, tx1.Hash(), "ABORT") + committeeInvoker.CheckHalt(t, tx2.Hash(), stackitem.Make(defaultValue)) + + // Change fee and check that change is available for the next tx. + tx1 = committeeInvoker.PrepareInvoke(t, setName, newVal) + tx2 = committeeInvoker.PrepareInvoke(t, getName) + committeeInvoker.AddNewBlock(t, tx1, tx2) + committeeInvoker.CheckHalt(t, tx1.Hash()) + if name != "GasPerBlock" { + committeeInvoker.CheckHalt(t, tx2.Hash(), stackitem.Make(newVal)) + } else { + committeeInvoker.CheckHalt(t, tx2.Hash(), stackitem.Make(defaultValue)) + committeeInvoker.Invoke(t, newVal, getName) + } +} + func setNodesByRole(t *testing.T, designateInvoker *neotest.ContractInvoker, ok bool, r noderoles.Role, nodes keys.PublicKeys) { pubs := make([]interface{}, len(nodes)) for i := range nodes { diff --git a/pkg/core/native/native_test/designate_test.go b/pkg/core/native/native_test/designate_test.go index bbfac3d84..98d3c362b 100644 --- a/pkg/core/native/native_test/designate_test.go +++ b/pkg/core/native/native_test/designate_test.go @@ -5,8 +5,15 @@ import ( "github.com/nspcc-dev/neo-go/pkg/core/native/nativenames" "github.com/nspcc-dev/neo-go/pkg/core/native/noderoles" + "github.com/nspcc-dev/neo-go/pkg/core/state" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + "github.com/nspcc-dev/neo-go/pkg/io" "github.com/nspcc-dev/neo-go/pkg/neotest" + "github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/vm/emit" + "github.com/nspcc-dev/neo-go/pkg/vm/opcode" + "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" "github.com/stretchr/testify/require" ) @@ -45,3 +52,88 @@ func TestDesignate_DesignateAsRole(t *testing.T) { checkNodeRoles(t, designateInvoker, true, noderoles.NeoFSAlphabet, e.Chain.BlockHeight()+1, pubs) }) } + +type dummyOracle struct { + updateNodes func(k keys.PublicKeys) +} + +// AddRequests processes new requests. +func (o *dummyOracle) AddRequests(map[uint64]*state.OracleRequest) { +} + +// RemoveRequests removes already processed requests. +func (o *dummyOracle) RemoveRequests([]uint64) { + panic("TODO") +} + +// UpdateOracleNodes updates oracle nodes. +func (o *dummyOracle) UpdateOracleNodes(k keys.PublicKeys) { + if o.updateNodes != nil { + o.updateNodes(k) + return + } + panic("TODO") +} + +// UpdateNativeContract updates oracle contract native script and hash. +func (o *dummyOracle) UpdateNativeContract([]byte, []byte, util.Uint160, int) { +} + +// Start runs oracle module. +func (o *dummyOracle) Start() { + panic("TODO") +} + +// Shutdown shutdowns oracle module. +func (o *dummyOracle) Shutdown() { + panic("TODO") +} + +func TestDesignate_Cache(t *testing.T) { + c := newDesignateClient(t) + e := c.Executor + designateInvoker := c.WithSigners(c.Committee) + r := int64(noderoles.Oracle) + var ( + updatedNodes keys.PublicKeys + updateCalled bool + ) + oracleServ := &dummyOracle{ + updateNodes: func(k keys.PublicKeys) { + updatedNodes = k + updateCalled = true + }, + } + privGood, err := keys.NewPrivateKey() + require.NoError(t, err) + pubsGood := []interface{}{privGood.PublicKey().Bytes()} + + privBad, err := keys.NewPrivateKey() + require.NoError(t, err) + pubsBad := []interface{}{privBad.PublicKey().Bytes()} + + // Firstly, designate good Oracle node and check that OracleService callback was called during PostPersist. + e.Chain.SetOracle(oracleServ) + txDesignateGood := designateInvoker.PrepareInvoke(t, "designateAsRole", r, pubsGood) + e.AddNewBlock(t, txDesignateGood) + e.CheckHalt(t, txDesignateGood.Hash(), stackitem.Null{}) + require.True(t, updateCalled) + require.Equal(t, keys.PublicKeys{privGood.PublicKey()}, updatedNodes) + updatedNodes = nil + updateCalled = false + + // Check designated node in a separate block. + checkNodeRoles(t, designateInvoker, true, noderoles.Oracle, e.Chain.BlockHeight()+1, keys.PublicKeys{privGood.PublicKey()}) + + // Designate privBad as oracle node and abort the transaction. Designation cache changes + // shouldn't be persisted to the contract and no notification should be sent. + w := io.NewBufBinWriter() + emit.AppCall(w.BinWriter, designateInvoker.Hash, "designateAsRole", callflag.All, int64(r), pubsBad) + emit.Opcodes(w.BinWriter, opcode.ABORT) + require.NoError(t, w.Err) + script := w.Bytes() + + designateInvoker.InvokeScriptCheckFAULT(t, script, designateInvoker.Signers, "ABORT") + require.Nil(t, updatedNodes) + require.False(t, updateCalled) +} diff --git a/pkg/core/native/native_test/management_test.go b/pkg/core/native/native_test/management_test.go index c34b1c8d8..6ff49178a 100644 --- a/pkg/core/native/native_test/management_test.go +++ b/pkg/core/native/native_test/management_test.go @@ -20,6 +20,8 @@ import ( "github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag" "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" "github.com/nspcc-dev/neo-go/pkg/smartcontract/nef" + "github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger" + "github.com/nspcc-dev/neo-go/pkg/vm" "github.com/nspcc-dev/neo-go/pkg/vm/emit" "github.com/nspcc-dev/neo-go/pkg/vm/opcode" "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" @@ -34,6 +36,43 @@ func TestManagement_MinimumDeploymentFee(t *testing.T) { testGetSet(t, newManagementClient(t), "MinimumDeploymentFee", 10_00000000, 0, 0) } +func TestManagement_MinimumDeploymentFeeCache(t *testing.T) { + c := newManagementClient(t) + testGetSetCache(t, c, "MinimumDeploymentFee", 10_00000000) +} + +func TestManagement_ContractCache(t *testing.T) { + c := newManagementClient(t) + managementInvoker := c.WithSigners(c.Committee) + + cs1, _ := contracts.GetTestContractState(t, pathToInternalContracts, 1, 2, c.Committee.ScriptHash()) + manifestBytes, err := json.Marshal(cs1.Manifest) + require.NoError(t, err) + nefBytes, err := cs1.NEF.Bytes() + require.NoError(t, err) + + // Deploy contract, abort the transaction and check that Management cache wasn't persisted + // for FAULTed tx at the same block. + w := io.NewBufBinWriter() + emit.AppCall(w.BinWriter, managementInvoker.Hash, "deploy", callflag.All, nefBytes, manifestBytes) + emit.Opcodes(w.BinWriter, opcode.ABORT) + tx1 := managementInvoker.PrepareInvocation(t, w.Bytes(), managementInvoker.Signers) + tx2 := managementInvoker.PrepareInvoke(t, "getContract", cs1.Hash.BytesBE()) + managementInvoker.AddNewBlock(t, tx1, tx2) + managementInvoker.CheckFault(t, tx1.Hash(), "ABORT") + managementInvoker.CheckHalt(t, tx2.Hash(), stackitem.Null{}) + + // Deploy the contract and check that cache was persisted for HALTed transaction at the same block. + tx1 = managementInvoker.PrepareInvoke(t, "deploy", nefBytes, manifestBytes) + tx2 = managementInvoker.PrepareInvoke(t, "getContract", cs1.Hash.BytesBE()) + managementInvoker.AddNewBlock(t, tx1, tx2) + managementInvoker.CheckHalt(t, tx1.Hash()) + aer, err := managementInvoker.Chain.GetAppExecResults(tx2.Hash(), trigger.Application) + require.NoError(t, err) + require.Equal(t, vm.HaltState, aer[0].VMState, aer[0].FaultException) + require.NotEqual(t, stackitem.Null{}, aer[0].Stack) +} + func TestManagement_ContractDeploy(t *testing.T) { c := newManagementClient(t) managementInvoker := c.WithSigners(c.Committee) diff --git a/pkg/core/native/native_test/neo_test.go b/pkg/core/native/native_test/neo_test.go index 3ade90aa4..9314efd45 100644 --- a/pkg/core/native/native_test/neo_test.go +++ b/pkg/core/native/native_test/neo_test.go @@ -42,10 +42,18 @@ func TestNEO_GasPerBlock(t *testing.T) { testGetSet(t, newNeoCommitteeClient(t, 100_0000_0000), "GasPerBlock", 5*native.GASFactor, 0, 10*native.GASFactor) } +func TestNEO_GasPerBlockCache(t *testing.T) { + testGetSetCache(t, newNeoCommitteeClient(t, 100_0000_0000), "GasPerBlock", 5*native.GASFactor) +} + func TestNEO_RegisterPrice(t *testing.T) { testGetSet(t, newNeoCommitteeClient(t, 100_0000_0000), "RegisterPrice", native.DefaultRegisterPrice, 1, math.MaxInt64) } +func TestNEO_RegisterPriceCache(t *testing.T) { + testGetSetCache(t, newNeoCommitteeClient(t, 100_0000_0000), "RegisterPrice", native.DefaultRegisterPrice) +} + func TestNEO_Vote(t *testing.T) { neoCommitteeInvoker := newNeoCommitteeClient(t, 100_0000_0000) neoValidatorsInvoker := neoCommitteeInvoker.WithSigners(neoCommitteeInvoker.Validator) diff --git a/pkg/core/native/native_test/notary_test.go b/pkg/core/native/native_test/notary_test.go index fd948294e..a827d0ed3 100644 --- a/pkg/core/native/native_test/notary_test.go +++ b/pkg/core/native/native_test/notary_test.go @@ -32,11 +32,21 @@ func TestNotary_MaxNotValidBeforeDelta(t *testing.T) { testGetSet(t, c, "MaxNotValidBeforeDelta", 140, int64(c.Chain.GetConfig().ValidatorsCount), int64(c.Chain.GetConfig().MaxValidUntilBlockIncrement/2)) } +func TestNotary_MaxNotValidBeforeDeltaCache(t *testing.T) { + c := newNotaryClient(t) + testGetSetCache(t, c, "MaxNotValidBeforeDelta", 140) +} + func TestNotary_NotaryServiceFeePerKey(t *testing.T) { c := newNotaryClient(t) testGetSet(t, c, "NotaryServiceFeePerKey", 1000_0000, 0, 0) } +func TestNotary_NotaryServiceFeePerKeyCache(t *testing.T) { + c := newNotaryClient(t) + testGetSetCache(t, c, "NotaryServiceFeePerKey", 1000_0000) +} + func TestNotary_Pipeline(t *testing.T) { notaryCommitteeInvoker := newNotaryClient(t) e := notaryCommitteeInvoker.Executor diff --git a/pkg/core/native/native_test/oracle_test.go b/pkg/core/native/native_test/oracle_test.go index 77247f80b..1542d6813 100644 --- a/pkg/core/native/native_test/oracle_test.go +++ b/pkg/core/native/native_test/oracle_test.go @@ -28,10 +28,14 @@ func newOracleClient(t *testing.T) *neotest.ContractInvoker { return newNativeClient(t, nativenames.Oracle) } -func TestGetSetPrice(t *testing.T) { +func TestOracle_GetSetPrice(t *testing.T) { testGetSet(t, newOracleClient(t), "Price", native.DefaultOracleRequestPrice, 1, math.MaxInt64) } +func TestOracle_GetSetPriceCache(t *testing.T) { + testGetSetCache(t, newOracleClient(t), "Price", native.DefaultOracleRequestPrice) +} + func putOracleRequest(t *testing.T, oracleInvoker *neotest.ContractInvoker, url string, filter *string, cb string, userData []byte, gas int64, errStr ...string) { var filtItem interface{} diff --git a/pkg/core/native/native_test/policy_test.go b/pkg/core/native/native_test/policy_test.go index 9dfc1a5eb..3d4a2c469 100644 --- a/pkg/core/native/native_test/policy_test.go +++ b/pkg/core/native/native_test/policy_test.go @@ -19,14 +19,26 @@ func TestPolicy_FeePerByte(t *testing.T) { testGetSet(t, newPolicyClient(t), "FeePerByte", 1000, 0, 100_000_000) } +func TestPolicy_FeePerByteCache(t *testing.T) { + testGetSetCache(t, newPolicyClient(t), "FeePerByte", 1000) +} + func TestPolicy_ExecFeeFactor(t *testing.T) { testGetSet(t, newPolicyClient(t), "ExecFeeFactor", interop.DefaultBaseExecFee, 1, 1000) } +func TestPolicy_ExecFeeFactorCache(t *testing.T) { + testGetSetCache(t, newPolicyClient(t), "ExecFeeFactor", interop.DefaultBaseExecFee) +} + func TestPolicy_StoragePrice(t *testing.T) { testGetSet(t, newPolicyClient(t), "StoragePrice", native.DefaultStoragePrice, 1, 10000000) } +func TestPolicy_StoragePriceCache(t *testing.T) { + testGetSetCache(t, newPolicyClient(t), "StoragePrice", native.DefaultStoragePrice) +} + func TestPolicy_BlockedAccounts(t *testing.T) { c := newPolicyClient(t) e := c.Executor diff --git a/pkg/core/native/notary.go b/pkg/core/native/notary.go index 086a11da1..2d5ab29ec 100644 --- a/pkg/core/native/notary.go +++ b/pkg/core/native/notary.go @@ -5,7 +5,6 @@ import ( "fmt" "math" "math/big" - "sync" "github.com/nspcc-dev/neo-go/pkg/core/dao" "github.com/nspcc-dev/neo-go/pkg/core/interop" @@ -32,12 +31,9 @@ type Notary struct { GAS *GAS NEO *NEO Desig *Designate +} - lock sync.RWMutex - // isValid defies whether cached values were changed during the current - // consensus iteration. If false, these values will be updated after - // blockchain DAO persisting. If true, we can safely use cached values. - isValid bool +type NotaryCache struct { maxNotValidBeforeDelta uint32 notaryServiceFeePerKey int64 } @@ -56,6 +52,22 @@ var ( notaryServiceFeeKey = []byte{5} ) +var ( + _ interop.Contract = (*Notary)(nil) + _ dao.NativeContractCache = (*NotaryCache)(nil) +) + +// Copy implements NativeContractCache interface. +func (c *NotaryCache) Copy() dao.NativeContractCache { + cp := &NotaryCache{} + copyNotaryCache(c, cp) + return cp +} + +func copyNotaryCache(src, dst *NotaryCache) { + *dst = *src +} + // newNotary returns Notary native contract. func newNotary() *Notary { n := &Notary{ContractMD: *interop.NewContractMD(nativenames.Notary, notaryContractID)} @@ -125,9 +137,22 @@ func (n *Notary) Metadata() *interop.ContractMD { func (n *Notary) Initialize(ic *interop.Context) error { setIntWithKey(n.ID, ic.DAO, maxNotValidBeforeDeltaKey, defaultMaxNotValidBeforeDelta) setIntWithKey(n.ID, ic.DAO, notaryServiceFeeKey, defaultNotaryServiceFeePerKey) - n.isValid = true - n.maxNotValidBeforeDelta = defaultMaxNotValidBeforeDelta - n.notaryServiceFeePerKey = defaultNotaryServiceFeePerKey + + cache := &NotaryCache{ + maxNotValidBeforeDelta: defaultMaxNotValidBeforeDelta, + notaryServiceFeePerKey: defaultNotaryServiceFeePerKey, + } + ic.DAO.SetCache(n.ID, cache) + return nil +} + +func (n *Notary) InitializeCache(d *dao.Simple) error { + cache := &NotaryCache{ + maxNotValidBeforeDelta: uint32(getIntWithKey(n.ID, d, maxNotValidBeforeDeltaKey)), + notaryServiceFeePerKey: getIntWithKey(n.ID, d, notaryServiceFeeKey), + } + + d.SetCache(n.ID, cache) return nil } @@ -176,15 +201,6 @@ func (n *Notary) OnPersist(ic *interop.Context) error { // PostPersist implements Contract interface. func (n *Notary) PostPersist(ic *interop.Context) error { - n.lock.Lock() - defer n.lock.Unlock() - if n.isValid { - return nil - } - - n.maxNotValidBeforeDelta = uint32(getIntWithKey(n.ID, ic.DAO, maxNotValidBeforeDeltaKey)) - n.notaryServiceFeePerKey = getIntWithKey(n.ID, ic.DAO, notaryServiceFeeKey) - n.isValid = true return nil } @@ -207,7 +223,7 @@ func (n *Notary) onPayment(ic *interop.Context, args []stackitem.Item) stackitem } allowedChangeTill := ic.Tx.Sender() == to - currentHeight := ic.Chain.BlockHeight() + currentHeight := ic.BlockHeight() deposit := n.GetDepositFor(ic.DAO, to) till := toUint32(additionalParams[1]) if till < currentHeight { @@ -250,7 +266,7 @@ func (n *Notary) lockDepositUntil(ic *interop.Context, args []stackitem.Item) st return stackitem.NewBool(false) } till := toUint32(args[1]) - if till < ic.Chain.BlockHeight() { + if till < ic.BlockHeight() { return stackitem.NewBool(false) } deposit := n.GetDepositFor(ic.DAO, addr) @@ -286,7 +302,7 @@ func (n *Notary) withdraw(ic *interop.Context, args []stackitem.Item) stackitem. if deposit == nil { return stackitem.NewBool(false) } - if ic.Chain.BlockHeight() < deposit.Till { + if ic.BlockHeight() < deposit.Till { return stackitem.NewBool(false) } cs, err := ic.GetContract(n.GAS.Hash) @@ -391,12 +407,8 @@ func (n *Notary) getMaxNotValidBeforeDelta(ic *interop.Context, _ []stackitem.It // GetMaxNotValidBeforeDelta is an internal representation of Notary getMaxNotValidBeforeDelta method. func (n *Notary) GetMaxNotValidBeforeDelta(dao *dao.Simple) uint32 { - n.lock.RLock() - defer n.lock.RUnlock() - if n.isValid { - return n.maxNotValidBeforeDelta - } - return uint32(getIntWithKey(n.ID, dao, maxNotValidBeforeDeltaKey)) + cache := dao.GetROCache(n.ID).(*NotaryCache) + return cache.maxNotValidBeforeDelta } // setMaxNotValidBeforeDelta is Notary contract method and sets the maximum NotValidBefore delta. @@ -404,16 +416,15 @@ func (n *Notary) setMaxNotValidBeforeDelta(ic *interop.Context, args []stackitem value := toUint32(args[0]) cfg := ic.Chain.GetConfig() maxInc := cfg.MaxValidUntilBlockIncrement - if value > maxInc/2 || value < uint32(cfg.GetNumOfCNs(ic.Chain.BlockHeight())) { - panic(fmt.Errorf("MaxNotValidBeforeDelta cannot be more than %d or less than %d", maxInc/2, cfg.GetNumOfCNs(ic.Chain.BlockHeight()))) + if value > maxInc/2 || value < uint32(cfg.GetNumOfCNs(ic.BlockHeight())) { + panic(fmt.Errorf("MaxNotValidBeforeDelta cannot be more than %d or less than %d", maxInc/2, cfg.GetNumOfCNs(ic.BlockHeight()))) } if !n.NEO.checkCommittee(ic) { panic("invalid committee signature") } - n.lock.Lock() - defer n.lock.Unlock() setIntWithKey(n.ID, ic.DAO, maxNotValidBeforeDeltaKey, int64(value)) - n.isValid = false + cache := ic.DAO.GetRWCache(n.ID).(*NotaryCache) + cache.maxNotValidBeforeDelta = value return stackitem.Null{} } @@ -424,12 +435,8 @@ func (n *Notary) getNotaryServiceFeePerKey(ic *interop.Context, _ []stackitem.It // GetNotaryServiceFeePerKey is an internal representation of Notary getNotaryServiceFeePerKey method. func (n *Notary) GetNotaryServiceFeePerKey(dao *dao.Simple) int64 { - n.lock.RLock() - defer n.lock.RUnlock() - if n.isValid { - return n.notaryServiceFeePerKey - } - return getIntWithKey(n.ID, dao, notaryServiceFeeKey) + cache := dao.GetROCache(n.ID).(*NotaryCache) + return cache.notaryServiceFeePerKey } // setNotaryServiceFeePerKey is Notary contract method and sets a reward per notary request key for notary nodes. @@ -441,10 +448,9 @@ func (n *Notary) setNotaryServiceFeePerKey(ic *interop.Context, args []stackitem if !n.NEO.checkCommittee(ic) { panic("invalid committee signature") } - n.lock.Lock() - defer n.lock.Unlock() setIntWithKey(n.ID, ic.DAO, notaryServiceFeeKey, int64(value)) - n.isValid = false + cache := ic.DAO.GetRWCache(n.ID).(*NotaryCache) + cache.notaryServiceFeePerKey = value return stackitem.Null{} } diff --git a/pkg/core/native/oracle.go b/pkg/core/native/oracle.go index 63b813c5b..7b3914b5c 100644 --- a/pkg/core/native/oracle.go +++ b/pkg/core/native/oracle.go @@ -40,15 +40,16 @@ type Oracle struct { Desig *Designate oracleScript []byte - requestPrice atomic.Value - requestPriceChanged atomic.Value - // Module is an oracle module capable of talking with the external world. Module atomic.Value // newRequests contains new requests created during current block. newRequests map[uint64]*state.OracleRequest } +type OracleCache struct { + requestPrice int64 +} + const ( oracleContractID = -9 maxURLLength = 256 @@ -82,6 +83,22 @@ var ( ErrResponseNotFound = errors.New("oracle response not found") ) +var ( + _ interop.Contract = (*Oracle)(nil) + _ dao.NativeContractCache = (*OracleCache)(nil) +) + +// Copy implements NativeContractCache interface. +func (c *OracleCache) Copy() dao.NativeContractCache { + cp := &OracleCache{} + copyOracleCache(c, cp) + return cp +} + +func copyOracleCache(src, dst *OracleCache) { + *dst = *src +} + func newOracle() *Oracle { o := &Oracle{ContractMD: *interop.NewContractMD(nativenames.Oracle, oracleContractID)} defer o.UpdateHash() @@ -121,8 +138,6 @@ func newOracle() *Oracle { md = newMethodAndPrice(o.setPrice, 1<<15, callflag.States) o.AddMethod(md, desc) - o.requestPriceChanged.Store(true) - return o } @@ -143,10 +158,6 @@ func (o *Oracle) OnPersist(ic *interop.Context) error { // PostPersist represents `postPersist` method. func (o *Oracle) PostPersist(ic *interop.Context) error { p := o.getPriceInternal(ic.DAO) - if o.requestPriceChanged.Load().(bool) { - o.requestPrice.Store(p) - o.requestPriceChanged.Store(false) - } var nodes keys.PublicKeys var reward []big.Int @@ -220,11 +231,20 @@ func (o *Oracle) Metadata() *interop.ContractMD { func (o *Oracle) Initialize(ic *interop.Context) error { setIntWithKey(o.ID, ic.DAO, prefixRequestID, 0) setIntWithKey(o.ID, ic.DAO, prefixRequestPrice, DefaultOracleRequestPrice) - o.requestPrice.Store(int64(DefaultOracleRequestPrice)) - o.requestPriceChanged.Store(false) + + cache := &OracleCache{ + requestPrice: int64(DefaultOracleRequestPrice), + } + ic.DAO.SetCache(o.ID, cache) return nil } +func (o *Oracle) InitializeCache(d *dao.Simple) { + cache := &OracleCache{} + cache.requestPrice = getIntWithKey(o.ID, d, prefixRequestPrice) + d.SetCache(o.ID, cache) +} + func getResponse(tx *transaction.Transaction) *transaction.OracleResponse { for i := range tx.Attributes { if tx.Attributes[i].Type == transaction.OracleResponseT { @@ -439,10 +459,8 @@ func (o *Oracle) getPrice(ic *interop.Context, _ []stackitem.Item) stackitem.Ite } func (o *Oracle) getPriceInternal(d *dao.Simple) int64 { - if !o.requestPriceChanged.Load().(bool) { - return o.requestPrice.Load().(int64) - } - return getIntWithKey(o.ID, d, prefixRequestPrice) + cache := d.GetROCache(o.ID).(*OracleCache) + return cache.requestPrice } func (o *Oracle) setPrice(ic *interop.Context, args []stackitem.Item) stackitem.Item { @@ -454,7 +472,8 @@ func (o *Oracle) setPrice(ic *interop.Context, args []stackitem.Item) stackitem. panic("invalid committee signature") } setIntWithKey(o.ID, ic.DAO, prefixRequestPrice, price.Int64()) - o.requestPriceChanged.Store(true) + cache := ic.DAO.GetRWCache(o.ID).(*OracleCache) + cache.requestPrice = price.Int64() return stackitem.Null{} } diff --git a/pkg/core/native/policy.go b/pkg/core/native/policy.go index c097b8e30..9dd40876d 100644 --- a/pkg/core/native/policy.go +++ b/pkg/core/native/policy.go @@ -4,7 +4,6 @@ import ( "fmt" "math/big" "sort" - "sync" "github.com/nspcc-dev/neo-go/pkg/core/dao" "github.com/nspcc-dev/neo-go/pkg/core/interop" @@ -52,12 +51,10 @@ var ( // Policy represents Policy native contract. type Policy struct { interop.ContractMD - NEO *NEO - lock sync.RWMutex - // isValid defies whether cached values were changed during the current - // consensus iteration. If false, these values will be updated after - // blockchain DAO persisting. If true, we can safely use cached values. - isValid bool + NEO *NEO +} + +type PolicyCache struct { execFeeFactor uint32 feePerByte int64 maxVerificationGas int64 @@ -65,7 +62,23 @@ type Policy struct { blockedAccounts []util.Uint160 } -var _ interop.Contract = (*Policy)(nil) +var ( + _ interop.Contract = (*Policy)(nil) + _ dao.NativeContractCache = (*PolicyCache)(nil) +) + +// Copy implements NativeContractCache interface. +func (c *PolicyCache) Copy() dao.NativeContractCache { + cp := &PolicyCache{} + copyPolicyCache(c, cp) + return cp +} + +func copyPolicyCache(src, dst *PolicyCache) { + *dst = *src + dst.blockedAccounts = make([]util.Uint160, len(src.blockedAccounts)) + copy(dst.blockedAccounts, src.blockedAccounts) +} // newPolicy returns Policy native contract. func newPolicy() *Policy { @@ -128,16 +141,51 @@ func (p *Policy) Initialize(ic *interop.Context) error { setIntWithKey(p.ID, ic.DAO, execFeeFactorKey, defaultExecFeeFactor) setIntWithKey(p.ID, ic.DAO, storagePriceKey, DefaultStoragePrice) - p.isValid = true - p.execFeeFactor = defaultExecFeeFactor - p.feePerByte = defaultFeePerByte - p.maxVerificationGas = defaultMaxVerificationGas - p.storagePrice = DefaultStoragePrice - p.blockedAccounts = make([]util.Uint160, 0) + cache := &PolicyCache{ + execFeeFactor: defaultExecFeeFactor, + feePerByte: defaultFeePerByte, + maxVerificationGas: defaultMaxVerificationGas, + storagePrice: DefaultStoragePrice, + blockedAccounts: make([]util.Uint160, 0), + } + ic.DAO.SetCache(p.ID, cache) return nil } +func (p *Policy) InitializeCache(d *dao.Simple) error { + cache := &PolicyCache{} + err := p.fillCacheFromDAO(cache, d) + if err != nil { + return err + } + d.SetCache(p.ID, cache) + return nil +} + +func (p *Policy) fillCacheFromDAO(cache *PolicyCache, d *dao.Simple) error { + cache.execFeeFactor = uint32(getIntWithKey(p.ID, d, execFeeFactorKey)) + cache.feePerByte = getIntWithKey(p.ID, d, feePerByteKey) + cache.maxVerificationGas = defaultMaxVerificationGas + cache.storagePrice = uint32(getIntWithKey(p.ID, d, storagePriceKey)) + + cache.blockedAccounts = make([]util.Uint160, 0) + var fErr error + d.Seek(p.ID, storage.SeekRange{Prefix: []byte{blockedAccountPrefix}}, func(k, _ []byte) bool { + hash, err := util.Uint160DecodeBytesBE(k) + if err != nil { + fErr = fmt.Errorf("failed to decode blocked account hash: %w", err) + return false + } + cache.blockedAccounts = append(cache.blockedAccounts, hash) + return true + }) + if fErr != nil { + return fmt.Errorf("failed to initialize blocked accounts: %w", fErr) + } + return nil +} + // OnPersist implements Contract interface. func (p *Policy) OnPersist(ic *interop.Context) error { return nil @@ -145,32 +193,7 @@ func (p *Policy) OnPersist(ic *interop.Context) error { // PostPersist implements Contract interface. func (p *Policy) PostPersist(ic *interop.Context) error { - p.lock.Lock() - defer p.lock.Unlock() - if p.isValid { - return nil - } - - p.execFeeFactor = uint32(getIntWithKey(p.ID, ic.DAO, execFeeFactorKey)) - p.feePerByte = getIntWithKey(p.ID, ic.DAO, feePerByteKey) - p.maxVerificationGas = defaultMaxVerificationGas - p.storagePrice = uint32(getIntWithKey(p.ID, ic.DAO, storagePriceKey)) - - p.blockedAccounts = make([]util.Uint160, 0) - var fErr error - ic.DAO.Seek(p.ID, storage.SeekRange{Prefix: []byte{blockedAccountPrefix}}, func(k, _ []byte) bool { - hash, err := util.Uint160DecodeBytesBE(k) - if err != nil { - fErr = fmt.Errorf("failed to decode blocked account hash: %w", err) - return false - } - p.blockedAccounts = append(p.blockedAccounts, hash) - return true - }) - if fErr == nil { - p.isValid = true - } - return fErr + return nil } // getFeePerByte is Policy contract method and returns required transaction's fee @@ -181,20 +204,14 @@ func (p *Policy) getFeePerByte(ic *interop.Context, _ []stackitem.Item) stackite // GetFeePerByteInternal returns required transaction's fee per byte. func (p *Policy) GetFeePerByteInternal(dao *dao.Simple) int64 { - p.lock.RLock() - defer p.lock.RUnlock() - if p.isValid { - return p.feePerByte - } - return getIntWithKey(p.ID, dao, feePerByteKey) + cache := dao.GetROCache(p.ID).(*PolicyCache) + return cache.feePerByte } // GetMaxVerificationGas returns maximum gas allowed to be burned during verificaion. -func (p *Policy) GetMaxVerificationGas(_ *dao.Simple) int64 { - if p.isValid { - return p.maxVerificationGas - } - return defaultMaxVerificationGas +func (p *Policy) GetMaxVerificationGas(dao *dao.Simple) int64 { + cache := dao.GetROCache(p.ID).(*PolicyCache) + return cache.maxVerificationGas } func (p *Policy) getExecFeeFactor(ic *interop.Context, _ []stackitem.Item) stackitem.Item { @@ -203,12 +220,8 @@ func (p *Policy) getExecFeeFactor(ic *interop.Context, _ []stackitem.Item) stack // GetExecFeeFactorInternal returns current execution fee factor. func (p *Policy) GetExecFeeFactorInternal(d *dao.Simple) int64 { - p.lock.RLock() - defer p.lock.RUnlock() - if p.isValid { - return int64(p.execFeeFactor) - } - return getIntWithKey(p.ID, d, execFeeFactorKey) + cache := d.GetROCache(p.ID).(*PolicyCache) + return int64(cache.execFeeFactor) } func (p *Policy) setExecFeeFactor(ic *interop.Context, args []stackitem.Item) stackitem.Item { @@ -219,35 +232,38 @@ func (p *Policy) setExecFeeFactor(ic *interop.Context, args []stackitem.Item) st if !p.NEO.checkCommittee(ic) { panic("invalid committee signature") } - p.lock.Lock() - defer p.lock.Unlock() setIntWithKey(p.ID, ic.DAO, execFeeFactorKey, int64(value)) - p.isValid = false + cache := ic.DAO.GetRWCache(p.ID).(*PolicyCache) + cache.execFeeFactor = value return stackitem.Null{} } // isBlocked is Policy contract method and checks whether provided account is blocked. func (p *Policy) isBlocked(ic *interop.Context, args []stackitem.Item) stackitem.Item { hash := toUint160(args[0]) - return stackitem.NewBool(p.IsBlockedInternal(ic.DAO, hash)) + _, blocked := p.isBlockedInternal(ic.DAO, hash) + return stackitem.NewBool(blocked) } -// IsBlockedInternal checks whether provided account is blocked. -func (p *Policy) IsBlockedInternal(dao *dao.Simple, hash util.Uint160) bool { - p.lock.RLock() - defer p.lock.RUnlock() - if p.isValid { - length := len(p.blockedAccounts) - i := sort.Search(length, func(i int) bool { - return !p.blockedAccounts[i].Less(hash) - }) - if length != 0 && i != length && p.blockedAccounts[i].Equals(hash) { - return true - } - return false +// IsBlocked checks whether provided account is blocked. +func (p *Policy) IsBlocked(dao *dao.Simple, hash util.Uint160) bool { + _, isBlocked := p.isBlockedInternal(dao, hash) + return isBlocked +} + +// isBlockedInternal checks whether provided account is blocked. It returns position +// of the blocked account in the blocked accounts list (or the position it should be +// put at). +func (p *Policy) isBlockedInternal(dao *dao.Simple, hash util.Uint160) (int, bool) { + cache := dao.GetROCache(p.ID).(*PolicyCache) + length := len(cache.blockedAccounts) + i := sort.Search(length, func(i int) bool { + return !cache.blockedAccounts[i].Less(hash) + }) + if length != 0 && i != length && cache.blockedAccounts[i].Equals(hash) { + return i, true } - key := append([]byte{blockedAccountPrefix}, hash.BytesBE()...) - return dao.GetStorageItem(p.ID, key) != nil + return i, false } func (p *Policy) getStoragePrice(ic *interop.Context, _ []stackitem.Item) stackitem.Item { @@ -256,12 +272,8 @@ func (p *Policy) getStoragePrice(ic *interop.Context, _ []stackitem.Item) stacki // GetStoragePriceInternal returns current execution fee factor. func (p *Policy) GetStoragePriceInternal(d *dao.Simple) int64 { - p.lock.RLock() - defer p.lock.RUnlock() - if p.isValid { - return int64(p.storagePrice) - } - return getIntWithKey(p.ID, d, storagePriceKey) + cache := d.GetROCache(p.ID).(*PolicyCache) + return int64(cache.storagePrice) } func (p *Policy) setStoragePrice(ic *interop.Context, args []stackitem.Item) stackitem.Item { @@ -272,10 +284,9 @@ func (p *Policy) setStoragePrice(ic *interop.Context, args []stackitem.Item) sta if !p.NEO.checkCommittee(ic) { panic("invalid committee signature") } - p.lock.Lock() - defer p.lock.Unlock() setIntWithKey(p.ID, ic.DAO, storagePriceKey, int64(value)) - p.isValid = false + cache := ic.DAO.GetRWCache(p.ID).(*PolicyCache) + cache.storagePrice = value return stackitem.Null{} } @@ -288,10 +299,9 @@ func (p *Policy) setFeePerByte(ic *interop.Context, args []stackitem.Item) stack if !p.NEO.checkCommittee(ic) { panic("invalid committee signature") } - p.lock.Lock() - defer p.lock.Unlock() setIntWithKey(p.ID, ic.DAO, feePerByteKey, value) - p.isValid = false + cache := ic.DAO.GetRWCache(p.ID).(*PolicyCache) + cache.feePerByte = value return stackitem.Null{} } @@ -307,14 +317,19 @@ func (p *Policy) blockAccount(ic *interop.Context, args []stackitem.Item) stacki panic("cannot block native contract") } } - if p.IsBlockedInternal(ic.DAO, hash) { + i, blocked := p.isBlockedInternal(ic.DAO, hash) + if blocked { return stackitem.NewBool(false) } key := append([]byte{blockedAccountPrefix}, hash.BytesBE()...) - p.lock.Lock() - defer p.lock.Unlock() ic.DAO.PutStorageItem(p.ID, key, state.StorageItem{}) - p.isValid = false + cache := ic.DAO.GetRWCache(p.ID).(*PolicyCache) + if len(cache.blockedAccounts) == i { + cache.blockedAccounts = append(cache.blockedAccounts, hash) + } else { + cache.blockedAccounts = append(cache.blockedAccounts[:i+1], cache.blockedAccounts[i:]...) + cache.blockedAccounts[i] = hash + } return stackitem.NewBool(true) } @@ -325,14 +340,14 @@ func (p *Policy) unblockAccount(ic *interop.Context, args []stackitem.Item) stac panic("invalid committee signature") } hash := toUint160(args[0]) - if !p.IsBlockedInternal(ic.DAO, hash) { + i, blocked := p.isBlockedInternal(ic.DAO, hash) + if !blocked { return stackitem.NewBool(false) } key := append([]byte{blockedAccountPrefix}, hash.BytesBE()...) - p.lock.Lock() - defer p.lock.Unlock() ic.DAO.DeleteStorageItem(p.ID, key) - p.isValid = false + cache := ic.DAO.GetRWCache(p.ID).(*PolicyCache) + cache.blockedAccounts = append(cache.blockedAccounts[:i], cache.blockedAccounts[i+1:]...) return stackitem.NewBool(true) } @@ -341,7 +356,7 @@ func (p *Policy) unblockAccount(ic *interop.Context, args []stackitem.Item) stac // fee limit. func (p *Policy) CheckPolicy(d *dao.Simple, tx *transaction.Transaction) error { for _, signer := range tx.Signers { - if p.IsBlockedInternal(d, signer.Account) { + if _, isBlocked := p.isBlockedInternal(d, signer.Account); isBlocked { return fmt.Errorf("account %s is blocked", signer.Account.StringLE()) } } diff --git a/pkg/core/stateroot/module.go b/pkg/core/stateroot/module.go index b09c5a953..fc43e7a50 100644 --- a/pkg/core/stateroot/module.go +++ b/pkg/core/stateroot/module.go @@ -1,6 +1,7 @@ package stateroot import ( + "bytes" "encoding/binary" "errors" "fmt" @@ -101,6 +102,34 @@ func (s *Module) GetStateRoot(height uint32) (*state.MPTRoot, error) { return s.getStateRoot(makeStateRootKey(height)) } +// GetLatestStateHeight returns the latest blockchain height by the given stateroot. +func (s *Module) GetLatestStateHeight(root util.Uint256) (uint32, error) { + rootBytes := root.BytesBE() + rootStartOffset := 1 + 4 // stateroot version (1 byte) + stateroot index (4 bytes) + rootEndOffset := rootStartOffset + util.Uint256Size + var ( + h uint32 + found bool + rootKey = makeStateRootKey(s.localHeight.Load()) + ) + s.Store.Seek(storage.SeekRange{ + Prefix: []byte{rootKey[0]}, // DataMPTAux + Start: rootKey[1:], // Start is a value that should be appended to the Prefix + Backwards: true, + }, func(k, v []byte) bool { + if len(k) == 5 && bytes.Equal(v[rootStartOffset:rootEndOffset], rootBytes) { + h = binary.BigEndian.Uint32(k[1:]) // cut prefix DataMPTAux + found = true + return false + } + return true + }) + if found { + return h, nil + } + return h, storage.ErrKeyNotFound +} + // CurrentLocalStateRoot returns hash of the local state root. func (s *Module) CurrentLocalStateRoot() util.Uint256 { return s.currentLocal.Load().(util.Uint256) diff --git a/pkg/core/stateroot_test.go b/pkg/core/stateroot_test.go index 3a958def5..86df6c8b5 100644 --- a/pkg/core/stateroot_test.go +++ b/pkg/core/stateroot_test.go @@ -296,3 +296,20 @@ func checkVoteBroadcasted(t *testing.T, bc *core.Blockchain, p *payload.Extensib require.True(t, len(pubs) > int(valIndex)) require.True(t, pubs[valIndex].VerifyHashable(vote.Signature, uint32(netmode.UnitTestNet), r)) } + +func TestStateroot_GetLatestStateHeight(t *testing.T) { + bc, validators, committee := chain.NewMultiWithCustomConfig(t, func(c *config.ProtocolConfiguration) { + c.P2PSigExtensions = true + }) + e := neotest.NewExecutor(t, bc, validators, committee) + initBasicChain(t, e) + + m := bc.GetStateModule() + for i := uint32(0); i < bc.BlockHeight(); i++ { + r, err := m.GetStateRoot(i) + require.NoError(t, err) + h, err := bc.GetStateModule().GetLatestStateHeight(r.Root) + require.NoError(t, err, i) + require.Equal(t, i, h) + } +} diff --git a/pkg/core/storage/memcached_store.go b/pkg/core/storage/memcached_store.go index 38623d9e4..10461545e 100644 --- a/pkg/core/storage/memcached_store.go +++ b/pkg/core/storage/memcached_store.go @@ -365,7 +365,6 @@ func (s *MemCachedStore) persist(isSync bool) (int, error) { if !isSync { s.mut.Unlock() } - err = tempstore.ps.PutChangeSet(tempstore.mem, tempstore.stor) if !isSync { diff --git a/pkg/rpc/client/rpc.go b/pkg/rpc/client/rpc.go index d643d4414..02648d1fb 100644 --- a/pkg/rpc/client/rpc.go +++ b/pkg/rpc/client/rpc.go @@ -588,6 +588,33 @@ func (c *Client) InvokeScript(script []byte, signers []transaction.Signer) (*res return c.invokeSomething("invokescript", p, signers) } +// InvokeScriptAtHeight returns the result of the given script after running it +// true the VM using the provided chain state retrieved from the specified chain +// height. +// NOTE: This is a test invoke and will not affect the blockchain. +func (c *Client) InvokeScriptAtHeight(height uint32, script []byte, signers []transaction.Signer) (*result.Invoke, error) { + var p = request.NewRawParams(height, script) + return c.invokeSomething("invokescripthistoric", p, signers) +} + +// InvokeScriptAtBlock returns the result of the given script after running it +// true the VM using the provided chain state retrieved from the specified block +// hash. +// NOTE: This is a test invoke and will not affect the blockchain. +func (c *Client) InvokeScriptAtBlock(blockHash util.Uint256, script []byte, signers []transaction.Signer) (*result.Invoke, error) { + var p = request.NewRawParams(blockHash.StringLE(), script) + return c.invokeSomething("invokescripthistoric", p, signers) +} + +// InvokeScriptWithState returns the result of the given script after running it +// true the VM using the provided chain state retrieved from the specified +// stateroot hash. +// NOTE: This is a test invoke and will not affect the blockchain. +func (c *Client) InvokeScriptWithState(stateroot util.Uint256, script []byte, signers []transaction.Signer) (*result.Invoke, error) { + var p = request.NewRawParams(stateroot.StringLE(), script) + return c.invokeSomething("invokescripthistoric", p, signers) +} + // InvokeFunction returns the results after calling the smart contract scripthash // with the given operation and parameters. // NOTE: this is test invoke and will not affect the blockchain. @@ -596,6 +623,33 @@ func (c *Client) InvokeFunction(contract util.Uint160, operation string, params return c.invokeSomething("invokefunction", p, signers) } +// InvokeFunctionAtHeight returns the results after calling the smart contract +// with the given operation and parameters at the given blockchain state +// specified by the blockchain height. +// NOTE: this is test invoke and will not affect the blockchain. +func (c *Client) InvokeFunctionAtHeight(height uint32, contract util.Uint160, operation string, params []smartcontract.Parameter, signers []transaction.Signer) (*result.Invoke, error) { + var p = request.NewRawParams(height, contract.StringLE(), operation, params) + return c.invokeSomething("invokefunctionhistoric", p, signers) +} + +// InvokeFunctionAtBlock returns the results after calling the smart contract +// with the given operation and parameters at given the blockchain state +// specified by the block hash. +// NOTE: this is test invoke and will not affect the blockchain. +func (c *Client) InvokeFunctionAtBlock(blockHash util.Uint256, contract util.Uint160, operation string, params []smartcontract.Parameter, signers []transaction.Signer) (*result.Invoke, error) { + var p = request.NewRawParams(blockHash.StringLE(), contract.StringLE(), operation, params) + return c.invokeSomething("invokefunctionhistoric", p, signers) +} + +// InvokeFunctionWithState returns the results after calling the smart contract +// with the given operation and parameters at the given blockchain state defined +// by the specified stateroot hash. +// NOTE: this is test invoke and will not affect the blockchain. +func (c *Client) InvokeFunctionWithState(stateroot util.Uint256, contract util.Uint160, operation string, params []smartcontract.Parameter, signers []transaction.Signer) (*result.Invoke, error) { + var p = request.NewRawParams(stateroot.StringLE(), contract.StringLE(), operation, params) + return c.invokeSomething("invokefunctionhistoric", p, signers) +} + // InvokeContractVerify returns the results after calling `verify` method of the smart contract // with the given parameters under verification trigger type. // NOTE: this is test invoke and will not affect the blockchain. @@ -604,6 +658,33 @@ func (c *Client) InvokeContractVerify(contract util.Uint160, params []smartcontr return c.invokeSomething("invokecontractverify", p, signers, witnesses...) } +// InvokeContractVerifyAtHeight returns the results after calling `verify` method +// of the smart contract with the given parameters under verification trigger type +// at the blockchain state specified by the blockchain height. +// NOTE: this is test invoke and will not affect the blockchain. +func (c *Client) InvokeContractVerifyAtHeight(height uint32, contract util.Uint160, params []smartcontract.Parameter, signers []transaction.Signer, witnesses ...transaction.Witness) (*result.Invoke, error) { + var p = request.NewRawParams(height, contract.StringLE(), params) + return c.invokeSomething("invokecontractverifyhistoric", p, signers, witnesses...) +} + +// InvokeContractVerifyAtBlock returns the results after calling `verify` method +// of the smart contract with the given parameters under verification trigger type +// at the blockchain state specified by the block hash. +// NOTE: this is test invoke and will not affect the blockchain. +func (c *Client) InvokeContractVerifyAtBlock(blockHash util.Uint256, contract util.Uint160, params []smartcontract.Parameter, signers []transaction.Signer, witnesses ...transaction.Witness) (*result.Invoke, error) { + var p = request.NewRawParams(blockHash.StringLE(), contract.StringLE(), params) + return c.invokeSomething("invokecontractverifyhistoric", p, signers, witnesses...) +} + +// InvokeContractVerifyWithState returns the results after calling `verify` method +// of the smart contract with the given parameters under verification trigger type +// at the blockchain state specified by the stateroot hash. +// NOTE: this is test invoke and will not affect the blockchain. +func (c *Client) InvokeContractVerifyWithState(stateroot util.Uint256, contract util.Uint160, params []smartcontract.Parameter, signers []transaction.Signer, witnesses ...transaction.Witness) (*result.Invoke, error) { + var p = request.NewRawParams(stateroot.StringLE(), contract.StringLE(), params) + return c.invokeSomething("invokecontractverifyhistoric", p, signers, witnesses...) +} + // invokeSomething is an inner wrapper for Invoke* functions. func (c *Client) invokeSomething(method string, p request.RawParams, signers []transaction.Signer, witnesses ...transaction.Witness) (*result.Invoke, error) { var resp = new(result.Invoke) diff --git a/pkg/rpc/server/client_test.go b/pkg/rpc/server/client_test.go index dc1ae0c5e..14826ea13 100644 --- a/pkg/rpc/server/client_test.go +++ b/pkg/rpc/server/client_test.go @@ -4,9 +4,11 @@ import ( "context" "encoding/base64" "encoding/hex" + "strings" "testing" "github.com/nspcc-dev/neo-go/internal/testchain" + "github.com/nspcc-dev/neo-go/pkg/core" "github.com/nspcc-dev/neo-go/pkg/core/fee" "github.com/nspcc-dev/neo-go/pkg/core/native/nativenames" "github.com/nspcc-dev/neo-go/pkg/core/transaction" @@ -766,6 +768,56 @@ func TestInvokeVerify(t *testing.T) { require.True(t, res.Stack[0].Value().(bool)) }) + t.Run("positive, historic, by height, with signer", func(t *testing.T) { + h := chain.BlockHeight() - 1 + res, err := c.InvokeContractVerifyAtHeight(h, contract, smartcontract.Params{}, []transaction.Signer{{Account: testchain.PrivateKeyByID(0).PublicKey().GetScriptHash()}}) + require.NoError(t, err) + require.Equal(t, "HALT", res.State) + require.Equal(t, 1, len(res.Stack)) + require.True(t, res.Stack[0].Value().(bool)) + }) + + t.Run("positive, historic, by block, with signer", func(t *testing.T) { + res, err := c.InvokeContractVerifyAtBlock(chain.GetHeaderHash(int(chain.BlockHeight())-1), contract, smartcontract.Params{}, []transaction.Signer{{Account: testchain.PrivateKeyByID(0).PublicKey().GetScriptHash()}}) + require.NoError(t, err) + require.Equal(t, "HALT", res.State) + require.Equal(t, 1, len(res.Stack)) + require.True(t, res.Stack[0].Value().(bool)) + }) + + t.Run("positive, historic, by stateroot, with signer", func(t *testing.T) { + h := chain.BlockHeight() - 1 + sr, err := chain.GetStateModule().GetStateRoot(h) + require.NoError(t, err) + res, err := c.InvokeContractVerifyWithState(sr.Root, contract, smartcontract.Params{}, []transaction.Signer{{Account: testchain.PrivateKeyByID(0).PublicKey().GetScriptHash()}}) + require.NoError(t, err) + require.Equal(t, "HALT", res.State) + require.Equal(t, 1, len(res.Stack)) + require.True(t, res.Stack[0].Value().(bool)) + }) + + t.Run("bad, historic, by hash: contract not found", func(t *testing.T) { + var h uint32 = 1 + _, err = c.InvokeContractVerifyAtHeight(h, contract, smartcontract.Params{}, []transaction.Signer{{Account: testchain.PrivateKeyByID(0).PublicKey().GetScriptHash()}}) + require.Error(t, err) + require.True(t, strings.Contains(err.Error(), core.ErrUnknownVerificationContract.Error())) // contract wasn't deployed at block #1 yet + }) + + t.Run("bad, historic, by block: contract not found", func(t *testing.T) { + _, err = c.InvokeContractVerifyAtBlock(chain.GetHeaderHash(1), contract, smartcontract.Params{}, []transaction.Signer{{Account: testchain.PrivateKeyByID(0).PublicKey().GetScriptHash()}}) + require.Error(t, err) + require.True(t, strings.Contains(err.Error(), core.ErrUnknownVerificationContract.Error())) // contract wasn't deployed at block #1 yet + }) + + t.Run("bad, historic, by stateroot: contract not found", func(t *testing.T) { + var h uint32 = 1 + sr, err := chain.GetStateModule().GetStateRoot(h) + require.NoError(t, err) + _, err = c.InvokeContractVerifyWithState(sr.Root, contract, smartcontract.Params{}, []transaction.Signer{{Account: testchain.PrivateKeyByID(0).PublicKey().GetScriptHash()}}) + require.Error(t, err) + require.True(t, strings.Contains(err.Error(), core.ErrUnknownVerificationContract.Error())) // contract wasn't deployed at block #1 yet + }) + t.Run("positive, with signer and witness", func(t *testing.T) { res, err := c.InvokeContractVerify(contract, smartcontract.Params{}, []transaction.Signer{{Account: testchain.PrivateKeyByID(0).PublicKey().GetScriptHash()}}, transaction.Witness{InvocationScript: []byte{byte(opcode.PUSH1), byte(opcode.RET)}}) require.NoError(t, err) diff --git a/pkg/rpc/server/server.go b/pkg/rpc/server/server.go index d08758173..e370fb8f4 100644 --- a/pkg/rpc/server/server.go +++ b/pkg/rpc/server/server.go @@ -24,6 +24,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/core/block" "github.com/nspcc-dev/neo-go/pkg/core/blockchainer" "github.com/nspcc-dev/neo-go/pkg/core/fee" + "github.com/nspcc-dev/neo-go/pkg/core/interop" "github.com/nspcc-dev/neo-go/pkg/core/interop/iterator" "github.com/nspcc-dev/neo-go/pkg/core/mempoolevent" "github.com/nspcc-dev/neo-go/pkg/core/mpt" @@ -108,46 +109,49 @@ const ( ) var rpcHandlers = map[string]func(*Server, request.Params) (interface{}, *response.Error){ - "calculatenetworkfee": (*Server).calculateNetworkFee, - "findstates": (*Server).findStates, - "getapplicationlog": (*Server).getApplicationLog, - "getbestblockhash": (*Server).getBestBlockHash, - "getblock": (*Server).getBlock, - "getblockcount": (*Server).getBlockCount, - "getblockhash": (*Server).getBlockHash, - "getblockheader": (*Server).getBlockHeader, - "getblockheadercount": (*Server).getBlockHeaderCount, - "getblocksysfee": (*Server).getBlockSysFee, - "getcommittee": (*Server).getCommittee, - "getconnectioncount": (*Server).getConnectionCount, - "getcontractstate": (*Server).getContractState, - "getnativecontracts": (*Server).getNativeContracts, - "getnep11balances": (*Server).getNEP11Balances, - "getnep11properties": (*Server).getNEP11Properties, - "getnep11transfers": (*Server).getNEP11Transfers, - "getnep17balances": (*Server).getNEP17Balances, - "getnep17transfers": (*Server).getNEP17Transfers, - "getpeers": (*Server).getPeers, - "getproof": (*Server).getProof, - "getrawmempool": (*Server).getRawMempool, - "getrawtransaction": (*Server).getrawtransaction, - "getstate": (*Server).getState, - "getstateheight": (*Server).getStateHeight, - "getstateroot": (*Server).getStateRoot, - "getstorage": (*Server).getStorage, - "gettransactionheight": (*Server).getTransactionHeight, - "getunclaimedgas": (*Server).getUnclaimedGas, - "getnextblockvalidators": (*Server).getNextBlockValidators, - "getversion": (*Server).getVersion, - "invokefunction": (*Server).invokeFunction, - "invokescript": (*Server).invokescript, - "invokecontractverify": (*Server).invokeContractVerify, - "sendrawtransaction": (*Server).sendrawtransaction, - "submitblock": (*Server).submitBlock, - "submitnotaryrequest": (*Server).submitNotaryRequest, - "submitoracleresponse": (*Server).submitOracleResponse, - "validateaddress": (*Server).validateAddress, - "verifyproof": (*Server).verifyProof, + "calculatenetworkfee": (*Server).calculateNetworkFee, + "findstates": (*Server).findStates, + "getapplicationlog": (*Server).getApplicationLog, + "getbestblockhash": (*Server).getBestBlockHash, + "getblock": (*Server).getBlock, + "getblockcount": (*Server).getBlockCount, + "getblockhash": (*Server).getBlockHash, + "getblockheader": (*Server).getBlockHeader, + "getblockheadercount": (*Server).getBlockHeaderCount, + "getblocksysfee": (*Server).getBlockSysFee, + "getcommittee": (*Server).getCommittee, + "getconnectioncount": (*Server).getConnectionCount, + "getcontractstate": (*Server).getContractState, + "getnativecontracts": (*Server).getNativeContracts, + "getnep11balances": (*Server).getNEP11Balances, + "getnep11properties": (*Server).getNEP11Properties, + "getnep11transfers": (*Server).getNEP11Transfers, + "getnep17balances": (*Server).getNEP17Balances, + "getnep17transfers": (*Server).getNEP17Transfers, + "getpeers": (*Server).getPeers, + "getproof": (*Server).getProof, + "getrawmempool": (*Server).getRawMempool, + "getrawtransaction": (*Server).getrawtransaction, + "getstate": (*Server).getState, + "getstateheight": (*Server).getStateHeight, + "getstateroot": (*Server).getStateRoot, + "getstorage": (*Server).getStorage, + "gettransactionheight": (*Server).getTransactionHeight, + "getunclaimedgas": (*Server).getUnclaimedGas, + "getnextblockvalidators": (*Server).getNextBlockValidators, + "getversion": (*Server).getVersion, + "invokefunction": (*Server).invokeFunction, + "invokefunctionhistoric": (*Server).invokeFunctionHistoric, + "invokescript": (*Server).invokescript, + "invokescripthistoric": (*Server).invokescripthistoric, + "invokecontractverify": (*Server).invokeContractVerify, + "invokecontractverifyhistoric": (*Server).invokeContractVerifyHistoric, + "sendrawtransaction": (*Server).sendrawtransaction, + "submitblock": (*Server).submitBlock, + "submitnotaryrequest": (*Server).submitNotaryRequest, + "submitoracleresponse": (*Server).submitOracleResponse, + "validateaddress": (*Server).validateAddress, + "verifyproof": (*Server).verifyProof, } var rpcWsHandlers = map[string]func(*Server, request.Params, *subscriber) (interface{}, *response.Error){ @@ -866,7 +870,7 @@ func (s *Server) invokeReadOnly(bw *io.BufBinWriter, h util.Uint160, method stri } script := bw.Bytes() tx := &transaction.Transaction{Script: script} - b, err := s.getFakeNextBlock() + b, err := s.getFakeNextBlock(s.chain.BlockHeight() + 1) if err != nil { return nil, nil, err } @@ -1571,16 +1575,40 @@ func (s *Server) getCommittee(_ request.Params) (interface{}, *response.Error) { // invokeFunction implements the `invokeFunction` RPC call. func (s *Server) invokeFunction(reqParams request.Params) (interface{}, *response.Error) { + tx, verbose, respErr := s.getInvokeFunctionParams(reqParams) + if respErr != nil { + return nil, respErr + } + return s.runScriptInVM(trigger.Application, tx.Script, util.Uint160{}, tx, nil, verbose) +} + +// invokeFunctionHistoric implements the `invokeFunctionHistoric` RPC call. +func (s *Server) invokeFunctionHistoric(reqParams request.Params) (interface{}, *response.Error) { + b, respErr := s.getHistoricParams(reqParams) + if respErr != nil { + return nil, respErr + } if len(reqParams) < 2 { return nil, response.ErrInvalidParams } + tx, verbose, respErr := s.getInvokeFunctionParams(reqParams[1:]) + if respErr != nil { + return nil, respErr + } + return s.runScriptInVM(trigger.Application, tx.Script, util.Uint160{}, tx, b, verbose) +} + +func (s *Server) getInvokeFunctionParams(reqParams request.Params) (*transaction.Transaction, bool, *response.Error) { + if len(reqParams) < 2 { + return nil, false, response.ErrInvalidParams + } scriptHash, responseErr := s.contractScriptHashFromParam(reqParams.Value(0)) if responseErr != nil { - return nil, responseErr + return nil, false, responseErr } method, err := reqParams[1].GetString() if err != nil { - return nil, response.ErrInvalidParams + return nil, false, response.ErrInvalidParams } var params *request.Param if len(reqParams) > 2 { @@ -1590,7 +1618,7 @@ func (s *Server) invokeFunction(reqParams request.Params) (interface{}, *respons if len(reqParams) > 3 { signers, _, err := reqParams[3].GetSignersWithWitnesses() if err != nil { - return nil, response.ErrInvalidParams + return nil, false, response.ErrInvalidParams } tx.Signers = signers } @@ -1598,7 +1626,7 @@ func (s *Server) invokeFunction(reqParams request.Params) (interface{}, *respons if len(reqParams) > 4 { verbose, err = reqParams[4].GetBoolean() if err != nil { - return nil, response.ErrInvalidParams + return nil, false, response.ErrInvalidParams } } if len(tx.Signers) == 0 { @@ -1606,28 +1634,48 @@ func (s *Server) invokeFunction(reqParams request.Params) (interface{}, *respons } script, err := request.CreateFunctionInvocationScript(scriptHash, method, params) if err != nil { - return nil, response.NewInternalServerError("can't create invocation script", err) + return nil, false, response.NewInternalServerError("can't create invocation script", err) } tx.Script = script - return s.runScriptInVM(trigger.Application, script, util.Uint160{}, tx, verbose) + return tx, verbose, nil } // invokescript implements the `invokescript` RPC call. func (s *Server) invokescript(reqParams request.Params) (interface{}, *response.Error) { - if len(reqParams) < 1 { + tx, verbose, respErr := s.getInvokeScriptParams(reqParams) + if respErr != nil { + return nil, respErr + } + return s.runScriptInVM(trigger.Application, tx.Script, util.Uint160{}, tx, nil, verbose) +} + +// invokescripthistoric implements the `invokescripthistoric` RPC call. +func (s *Server) invokescripthistoric(reqParams request.Params) (interface{}, *response.Error) { + b, respErr := s.getHistoricParams(reqParams) + if respErr != nil { + return nil, respErr + } + if len(reqParams) < 2 { return nil, response.ErrInvalidParams } + tx, verbose, respErr := s.getInvokeScriptParams(reqParams[1:]) + if respErr != nil { + return nil, respErr + } + return s.runScriptInVM(trigger.Application, tx.Script, util.Uint160{}, tx, b, verbose) +} - script, err := reqParams[0].GetBytesBase64() +func (s *Server) getInvokeScriptParams(reqParams request.Params) (*transaction.Transaction, bool, *response.Error) { + script, err := reqParams.Value(0).GetBytesBase64() if err != nil { - return nil, response.ErrInvalidParams + return nil, false, response.ErrInvalidParams } tx := &transaction.Transaction{} if len(reqParams) > 1 { signers, witnesses, err := reqParams[1].GetSignersWithWitnesses() if err != nil { - return nil, response.ErrInvalidParams + return nil, false, response.ErrInvalidParams } tx.Signers = signers tx.Scripts = witnesses @@ -1636,33 +1684,57 @@ func (s *Server) invokescript(reqParams request.Params) (interface{}, *response. if len(reqParams) > 2 { verbose, err = reqParams[2].GetBoolean() if err != nil { - return nil, response.ErrInvalidParams + return nil, false, response.ErrInvalidParams } } if len(tx.Signers) == 0 { tx.Signers = []transaction.Signer{{Account: util.Uint160{}, Scopes: transaction.None}} } tx.Script = script - return s.runScriptInVM(trigger.Application, script, util.Uint160{}, tx, verbose) + return tx, verbose, nil } // invokeContractVerify implements the `invokecontractverify` RPC call. func (s *Server) invokeContractVerify(reqParams request.Params) (interface{}, *response.Error) { + scriptHash, tx, invocationScript, respErr := s.getInvokeContractVerifyParams(reqParams) + if respErr != nil { + return nil, respErr + } + return s.runScriptInVM(trigger.Verification, invocationScript, scriptHash, tx, nil, false) +} + +// invokeContractVerifyHistoric implements the `invokecontractverifyhistoric` RPC call. +func (s *Server) invokeContractVerifyHistoric(reqParams request.Params) (interface{}, *response.Error) { + b, respErr := s.getHistoricParams(reqParams) + if respErr != nil { + return nil, respErr + } + if len(reqParams) < 2 { + return nil, response.ErrInvalidParams + } + scriptHash, tx, invocationScript, respErr := s.getInvokeContractVerifyParams(reqParams[1:]) + if respErr != nil { + return nil, respErr + } + return s.runScriptInVM(trigger.Verification, invocationScript, scriptHash, tx, b, false) +} + +func (s *Server) getInvokeContractVerifyParams(reqParams request.Params) (util.Uint160, *transaction.Transaction, []byte, *response.Error) { scriptHash, responseErr := s.contractScriptHashFromParam(reqParams.Value(0)) if responseErr != nil { - return nil, responseErr + return util.Uint160{}, nil, nil, responseErr } bw := io.NewBufBinWriter() if len(reqParams) > 1 { args, err := reqParams[1].GetArray() // second `invokecontractverify` parameter is an array of arguments for `verify` method if err != nil { - return nil, response.WrapErrorWithData(response.ErrInvalidParams, err) + return util.Uint160{}, nil, nil, response.WrapErrorWithData(response.ErrInvalidParams, err) } if len(args) > 0 { err := request.ExpandArrayIntoScript(bw.BinWriter, args) if err != nil { - return nil, response.NewRPCError("can't create witness invocation script", err.Error(), err) + return util.Uint160{}, nil, nil, response.NewRPCError("can't create witness invocation script", err.Error(), err) } } } @@ -1672,7 +1744,7 @@ func (s *Server) invokeContractVerify(reqParams request.Params) (interface{}, *r if len(reqParams) > 2 { signers, witnesses, err := reqParams[2].GetSignersWithWitnesses() if err != nil { - return nil, response.ErrInvalidParams + return util.Uint160{}, nil, nil, response.ErrInvalidParams } tx.Signers = signers tx.Scripts = witnesses @@ -1680,16 +1752,51 @@ func (s *Server) invokeContractVerify(reqParams request.Params) (interface{}, *r tx.Signers = []transaction.Signer{{Account: scriptHash}} tx.Scripts = []transaction.Witness{{InvocationScript: invocationScript, VerificationScript: []byte{}}} } - return s.runScriptInVM(trigger.Verification, invocationScript, scriptHash, tx, false) + return scriptHash, tx, invocationScript, nil } -func (s *Server) getFakeNextBlock() (*block.Block, error) { +// getHistoricParams checks that historic calls are supported and returns fake block +// with the specified index to perform the historic call. It also checks that +// specified stateroot is stored at the specified height for further request +// handling consistency. +func (s *Server) getHistoricParams(reqParams request.Params) (*block.Block, *response.Error) { + if s.chain.GetConfig().KeepOnlyLatestState { + return nil, response.NewInvalidRequestError("only latest state is supported", errKeepOnlyLatestState) + } + if len(reqParams) < 1 { + return nil, response.ErrInvalidParams + } + height, respErr := s.blockHeightFromParam(reqParams.Value(0)) + if respErr != nil { + hash, err := reqParams.Value(0).GetUint256() + if err != nil { + return nil, response.NewInvalidParamsError("invalid block hash or index or stateroot hash", err) + } + b, err := s.chain.GetBlock(hash) + if err != nil { + stateH, err := s.chain.GetStateModule().GetLatestStateHeight(hash) + if err != nil { + return nil, response.NewInvalidParamsError(fmt.Sprintf("unknown block or stateroot: %s", err), err) + } + height = int(stateH) + } else { + height = int(b.Index) + } + } + b, err := s.getFakeNextBlock(uint32(height)) + if err != nil { + return nil, response.NewInternalServerError(fmt.Sprintf("can't create fake block for height %d: %s", height, err.Error()), err) + } + return b, nil +} + +func (s *Server) getFakeNextBlock(nextBlockHeight uint32) (*block.Block, error) { // When transferring funds, script execution does no auto GAS claim, // because it depends on persisting tx height. // This is why we provide block here. b := block.New(s.stateRootEnabled) - b.Index = s.chain.BlockHeight() + 1 - hdr, err := s.chain.GetHeader(s.chain.GetHeaderHash(int(s.chain.BlockHeight()))) + b.Index = nextBlockHeight + hdr, err := s.chain.GetHeader(s.chain.GetHeaderHash(int(nextBlockHeight - 1))) if err != nil { return nil, err } @@ -1702,12 +1809,23 @@ func (s *Server) getFakeNextBlock() (*block.Block, error) { // witness invocation script in case of `verification` trigger (it pushes `verify` // arguments on stack before verification). In case of contract verification // contractScriptHash should be specified. -func (s *Server) runScriptInVM(t trigger.Type, script []byte, contractScriptHash util.Uint160, tx *transaction.Transaction, verbose bool) (*result.Invoke, *response.Error) { - b, err := s.getFakeNextBlock() - if err != nil { - return nil, response.NewInternalServerError("can't create fake block", err) +func (s *Server) runScriptInVM(t trigger.Type, script []byte, contractScriptHash util.Uint160, tx *transaction.Transaction, b *block.Block, verbose bool) (*result.Invoke, *response.Error) { + var ( + err error + ic *interop.Context + ) + if b == nil { + b, err = s.getFakeNextBlock(s.chain.BlockHeight() + 1) + if err != nil { + return nil, response.NewInternalServerError("can't create fake block", err) + } + ic = s.chain.GetTestVM(t, tx, b) + } else { + ic, err = s.chain.GetTestHistoricVM(t, tx, b) + if err != nil { + return nil, response.NewInternalServerError("failed to create historic VM", err) + } } - ic := s.chain.GetTestVM(t, tx, b) if verbose { ic.VM.EnableInvocationTree() } @@ -1720,9 +1838,9 @@ func (s *Server) runScriptInVM(t trigger.Type, script []byte, contractScriptHash ic.VM.GasLimit = gasPolicy } - err := s.chain.InitVerificationContext(ic, contractScriptHash, &transaction.Witness{InvocationScript: script, VerificationScript: []byte{}}) + err = s.chain.InitVerificationContext(ic, contractScriptHash, &transaction.Witness{InvocationScript: script, VerificationScript: []byte{}}) if err != nil { - return nil, response.NewInternalServerError("can't prepare verification VM", err) + return nil, response.NewInternalServerError(fmt.Sprintf("can't prepare verification VM: %s", err.Error()), err) } } else { ic.VM.LoadScriptWithFlags(script, callflag.All) diff --git a/pkg/rpc/server/server_test.go b/pkg/rpc/server/server_test.go index 868e19eb4..b7bfc46fc 100644 --- a/pkg/rpc/server/server_test.go +++ b/pkg/rpc/server/server_test.go @@ -75,6 +75,7 @@ const ( nfsoContractHash = "5f9ebd6b001b54c7bc70f96e0412fcf415dfe09f" nfsoToken1ID = "7e244ffd6aa85fb1579d2ed22e9b761ab62e3486" invokescriptContractAVM = "VwIADBQBDAMOBQYMDQIODw0DDgcJAAAAAErZMCQE2zBwaEH4J+yMqiYEEUAMFA0PAwIJAAIBAwcDBAUCAQAOBgwJStkwJATbMHFpQfgn7IyqJgQSQBNA" + block20StateRootLE = "19ec3c3d01afe5274e8bb4a393c97da708c5608c5b0ad116c16108b6a04fb08e" ) var ( @@ -999,6 +1000,134 @@ var rpcTestCases = map[string][]rpcTestCase{ fail: true, }, }, + "invokefunctionhistoric": { + { + name: "positive, by index", + params: `[20, "50befd26fdf6e4d957c11e078b24ebce6291456f", "test", []]`, + result: func(e *executor) interface{} { return &result.Invoke{} }, + check: func(t *testing.T, e *executor, inv interface{}) { + res, ok := inv.(*result.Invoke) + require.True(t, ok) + assert.NotNil(t, res.Script) + assert.NotEqual(t, "", res.State) + assert.NotEqual(t, 0, res.GasConsumed) + }, + }, + { + name: "positive, by stateroot", + params: `["` + block20StateRootLE + `", "50befd26fdf6e4d957c11e078b24ebce6291456f", "test", []]`, + result: func(e *executor) interface{} { return &result.Invoke{} }, + check: func(t *testing.T, e *executor, inv interface{}) { + res, ok := inv.(*result.Invoke) + require.True(t, ok) + assert.NotNil(t, res.Script) + assert.NotEqual(t, "", res.State) + assert.NotEqual(t, 0, res.GasConsumed) + }, + }, + { + name: "positive, with notifications", + params: `[20, "` + nnsContractHash + `", "transfer", [{"type":"Hash160", "value":"0x0bcd2978634d961c24f5aea0802297ff128724d6"},{"type":"String", "value":"neo.com"},{"type":"Any", "value":null}],["0xb248508f4ef7088e10c48f14d04be3272ca29eee"]]`, + result: func(e *executor) interface{} { + script := []byte{0x0b, 0x0c, 0x07, 0x6e, 0x65, 0x6f, 0x2e, 0x63, 0x6f, 0x6d, 0x0c, 0x14, 0xd6, 0x24, 0x87, 0x12, 0xff, 0x97, 0x22, 0x80, 0xa0, 0xae, 0xf5, 0x24, 0x1c, 0x96, 0x4d, 0x63, 0x78, 0x29, 0xcd, 0xb, 0x13, 0xc0, 0x1f, 0xc, 0x8, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x65, 0x72, 0xc, 0x14, 0x1f, 0xe2, 0x37, 0x5c, 0xdc, 0xdb, 0xb2, 0x80, 0x40, 0x78, 0x65, 0x35, 0xd5, 0xef, 0xe4, 0x3, 0x39, 0x56, 0x92, 0xee, 0x41, 0x62, 0x7d, 0x5b, 0x52} + return &result.Invoke{ + State: "HALT", + GasConsumed: 32167260, + Script: script, + Stack: []stackitem.Item{stackitem.Make(true)}, + Notifications: []state.NotificationEvent{{ + ScriptHash: nnsHash, + Name: "Transfer", + Item: stackitem.NewArray([]stackitem.Item{ + stackitem.Make([]byte{0xee, 0x9e, 0xa2, 0x2c, 0x27, 0xe3, 0x4b, 0xd0, 0x14, 0x8f, 0xc4, 0x10, 0x8e, 0x08, 0xf7, 0x4e, 0x8f, 0x50, 0x48, 0xb2}), + stackitem.Make([]byte{0xd6, 0x24, 0x87, 0x12, 0xff, 0x97, 0x22, 0x80, 0xa0, 0xae, 0xf5, 0x24, 0x1c, 0x96, 0x4d, 0x63, 0x78, 0x29, 0xcd, 0x0b}), + stackitem.Make(1), + stackitem.Make("neo.com"), + }), + }}, + } + }, + }, + { + name: "positive, verbose", + params: `[20, "` + nnsContractHash + `", "resolve", [{"type":"String", "value":"neo.com"},{"type":"Integer","value":1}], [], true]`, + result: func(e *executor) interface{} { + script := []byte{0x11, 0xc, 0x7, 0x6e, 0x65, 0x6f, 0x2e, 0x63, 0x6f, 0x6d, 0x12, 0xc0, 0x1f, 0xc, 0x7, 0x72, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0xc, 0x14, 0x1f, 0xe2, 0x37, 0x5c, 0xdc, 0xdb, 0xb2, 0x80, 0x40, 0x78, 0x65, 0x35, 0xd5, 0xef, 0xe4, 0x3, 0x39, 0x56, 0x92, 0xee, 0x41, 0x62, 0x7d, 0x5b, 0x52} + stdHash, _ := e.chain.GetNativeContractScriptHash(nativenames.StdLib) + cryptoHash, _ := e.chain.GetNativeContractScriptHash(nativenames.CryptoLib) + return &result.Invoke{ + State: "HALT", + GasConsumed: 15928320, + Script: script, + Stack: []stackitem.Item{stackitem.Make("1.2.3.4")}, + Notifications: []state.NotificationEvent{}, + Diagnostics: &result.InvokeDiag{ + Changes: []storage.Operation{}, + Invocations: []*vm.InvocationTree{{ + Current: hash.Hash160(script), + Calls: []*vm.InvocationTree{ + { + Current: nnsHash, + Calls: []*vm.InvocationTree{ + { + Current: stdHash, + }, + { + Current: cryptoHash, + }, + { + Current: stdHash, + }, + { + Current: cryptoHash, + }, + { + Current: cryptoHash, + }, + }, + }, + }, + }}, + }, + } + }, + }, + { + name: "no params", + params: `[]`, + fail: true, + }, + { + name: "no args", + params: `[20]`, + fail: true, + }, + { + name: "not a string", + params: `[20, 42, "test", []]`, + fail: true, + }, + { + name: "not a scripthash", + params: `[20,"qwerty", "test", []]`, + fail: true, + }, + { + name: "bad params", + params: `[20,"50befd26fdf6e4d957c11e078b24ebce6291456f", "test", [{"type": "Integer", "value": "qwerty"}]]`, + fail: true, + }, + { + name: "bad height", + params: `[100500,"50befd26fdf6e4d957c11e078b24ebce6291456f", "test", [{"type": "Integer", "value": 1}]]`, + fail: true, + }, + { + name: "bad stateroot", + params: `["` + util.Uint256{1, 2, 3}.StringLE() + `","50befd26fdf6e4d957c11e078b24ebce6291456f", "test", [{"type": "Integer", "value": 1}]]`, + fail: true, + }, + }, "invokescript": { { name: "positive", @@ -1098,6 +1227,132 @@ var rpcTestCases = map[string][]rpcTestCase{ fail: true, }, }, + "invokescripthistoric": { + { + name: "positive, by index", + params: `[20,"UcVrDUhlbGxvLCB3b3JsZCFoD05lby5SdW50aW1lLkxvZ2FsdWY="]`, + result: func(e *executor) interface{} { return &result.Invoke{} }, + check: func(t *testing.T, e *executor, inv interface{}) { + res, ok := inv.(*result.Invoke) + require.True(t, ok) + assert.NotEqual(t, "", res.Script) + assert.NotEqual(t, "", res.State) + assert.NotEqual(t, 0, res.GasConsumed) + }, + }, + { + name: "positive, by stateroot", + params: `["` + block20StateRootLE + `","UcVrDUhlbGxvLCB3b3JsZCFoD05lby5SdW50aW1lLkxvZ2FsdWY="]`, + result: func(e *executor) interface{} { return &result.Invoke{} }, + check: func(t *testing.T, e *executor, inv interface{}) { + res, ok := inv.(*result.Invoke) + require.True(t, ok) + assert.NotEqual(t, "", res.Script) + assert.NotEqual(t, "", res.State) + assert.NotEqual(t, 0, res.GasConsumed) + }, + }, + { + name: "positive,verbose", + params: `[20, "UcVrDUhlbGxvLCB3b3JsZCFoD05lby5SdW50aW1lLkxvZ2FsdWY=",[],true]`, + result: func(e *executor) interface{} { + script := []byte{0x51, 0xc5, 0x6b, 0xd, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x2c, 0x20, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x21, 0x68, 0xf, 0x4e, 0x65, 0x6f, 0x2e, 0x52, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x4c, 0x6f, 0x67, 0x61, 0x6c, 0x75, 0x66} + return &result.Invoke{ + State: "FAULT", + GasConsumed: 60, + Script: script, + Stack: []stackitem.Item{}, + FaultException: "at instruction 0 (ROT): too big index", + Notifications: []state.NotificationEvent{}, + Diagnostics: &result.InvokeDiag{ + Changes: []storage.Operation{}, + Invocations: []*vm.InvocationTree{{ + Current: hash.Hash160(script), + }}, + }, + } + }, + }, + { + name: "positive, good witness", + // script is base64-encoded `invokescript_contract.avm` representation, hashes are hex-encoded LE bytes of hashes used in the contract with `0x` prefix + params: fmt.Sprintf(`[20,"%s",["0x0000000009070e030d0f0e020d0c06050e030c01","0x090c060e00010205040307030102000902030f0d"]]`, invokescriptContractAVM), + result: func(e *executor) interface{} { return &result.Invoke{} }, + check: func(t *testing.T, e *executor, inv interface{}) { + res, ok := inv.(*result.Invoke) + require.True(t, ok) + assert.Equal(t, "HALT", res.State) + require.Equal(t, 1, len(res.Stack)) + require.Equal(t, big.NewInt(3), res.Stack[0].Value()) + }, + }, + { + name: "positive, bad witness of second hash", + params: fmt.Sprintf(`[20,"%s",["0x0000000009070e030d0f0e020d0c06050e030c01"]]`, invokescriptContractAVM), + result: func(e *executor) interface{} { return &result.Invoke{} }, + check: func(t *testing.T, e *executor, inv interface{}) { + res, ok := inv.(*result.Invoke) + require.True(t, ok) + assert.Equal(t, "HALT", res.State) + require.Equal(t, 1, len(res.Stack)) + require.Equal(t, big.NewInt(2), res.Stack[0].Value()) + }, + }, + { + name: "positive, no good hashes", + params: fmt.Sprintf(`[20,"%s"]`, invokescriptContractAVM), + result: func(e *executor) interface{} { return &result.Invoke{} }, + check: func(t *testing.T, e *executor, inv interface{}) { + res, ok := inv.(*result.Invoke) + require.True(t, ok) + assert.Equal(t, "HALT", res.State) + require.Equal(t, 1, len(res.Stack)) + require.Equal(t, big.NewInt(1), res.Stack[0].Value()) + }, + }, + { + name: "positive, bad hashes witness", + params: fmt.Sprintf(`[20,"%s",["0x0000000009070e030d0f0e020d0c06050e030c02"]]`, invokescriptContractAVM), + result: func(e *executor) interface{} { return &result.Invoke{} }, + check: func(t *testing.T, e *executor, inv interface{}) { + res, ok := inv.(*result.Invoke) + require.True(t, ok) + assert.Equal(t, "HALT", res.State) + assert.Equal(t, 1, len(res.Stack)) + assert.Equal(t, big.NewInt(1), res.Stack[0].Value()) + }, + }, + { + name: "no params", + params: `[]`, + fail: true, + }, + { + name: "no script", + params: `[20]`, + fail: true, + }, + { + name: "not a string", + params: `[20,42]`, + fail: true, + }, + { + name: "bas string", + params: `[20, "qwerty"]`, + fail: true, + }, + { + name: "bas height", + params: `[100500,"qwerty"]`, + fail: true, + }, + { + name: "bas stateroot", + params: `["` + util.Uint256{1, 2, 3}.StringLE() + `","UcVrDUhlbGxvLCB3b3JsZCFoD05lby5SdW50aW1lLkxvZ2FsdWY="]`, + fail: true, + }, + }, "invokecontractverify": { { name: "positive", @@ -1203,6 +1458,129 @@ var rpcTestCases = map[string][]rpcTestCase{ fail: true, }, }, + "invokecontractverifyhistoric": { + { + name: "positive, by index", + params: fmt.Sprintf(`[20,"%s", [], [{"account":"%s"}]]`, verifyContractHash, testchain.PrivateKeyByID(0).PublicKey().GetScriptHash().StringLE()), + result: func(e *executor) interface{} { return &result.Invoke{} }, + check: func(t *testing.T, e *executor, inv interface{}) { + res, ok := inv.(*result.Invoke) + require.True(t, ok) + assert.Nil(t, res.Script) // empty witness invocation script (pushes args of `verify` on stack, but this `verify` don't have args) + assert.Equal(t, "HALT", res.State) + assert.NotEqual(t, 0, res.GasConsumed) + assert.Equal(t, true, res.Stack[0].Value().(bool), fmt.Sprintf("check address in verification_contract.go: expected %s", testchain.PrivateKeyByID(0).Address())) + }, + }, + { + name: "positive, by stateroot", + params: fmt.Sprintf(`["`+block20StateRootLE+`","%s", [], [{"account":"%s"}]]`, verifyContractHash, testchain.PrivateKeyByID(0).PublicKey().GetScriptHash().StringLE()), + result: func(e *executor) interface{} { return &result.Invoke{} }, + check: func(t *testing.T, e *executor, inv interface{}) { + res, ok := inv.(*result.Invoke) + require.True(t, ok) + assert.Nil(t, res.Script) // empty witness invocation script (pushes args of `verify` on stack, but this `verify` don't have args) + assert.Equal(t, "HALT", res.State) + assert.NotEqual(t, 0, res.GasConsumed) + assert.Equal(t, true, res.Stack[0].Value().(bool), fmt.Sprintf("check address in verification_contract.go: expected %s", testchain.PrivateKeyByID(0).Address())) + }, + }, + { + name: "positive, no signers", + params: fmt.Sprintf(`[20,"%s", []]`, verifyContractHash), + result: func(e *executor) interface{} { return &result.Invoke{} }, + check: func(t *testing.T, e *executor, inv interface{}) { + res, ok := inv.(*result.Invoke) + require.True(t, ok) + assert.Nil(t, res.Script) + assert.Equal(t, "HALT", res.State, res.FaultException) + assert.NotEqual(t, 0, res.GasConsumed) + assert.Equal(t, false, res.Stack[0].Value().(bool)) + }, + }, + { + name: "positive, no arguments", + params: fmt.Sprintf(`[20,"%s"]`, verifyContractHash), + result: func(e *executor) interface{} { return &result.Invoke{} }, + check: func(t *testing.T, e *executor, inv interface{}) { + res, ok := inv.(*result.Invoke) + require.True(t, ok) + assert.Nil(t, res.Script) + assert.Equal(t, "HALT", res.State, res.FaultException) + assert.NotEqual(t, 0, res.GasConsumed) + assert.Equal(t, false, res.Stack[0].Value().(bool)) + }, + }, + { + name: "positive, with signers and scripts", + params: fmt.Sprintf(`[20,"%s", [], [{"account":"%s", "invocation":"MQo=", "verification": ""}]]`, verifyContractHash, testchain.PrivateKeyByID(0).PublicKey().GetScriptHash().StringLE()), + result: func(e *executor) interface{} { return &result.Invoke{} }, + check: func(t *testing.T, e *executor, inv interface{}) { + res, ok := inv.(*result.Invoke) + require.True(t, ok) + assert.Nil(t, res.Script) + assert.Equal(t, "HALT", res.State) + assert.NotEqual(t, 0, res.GasConsumed) + assert.Equal(t, true, res.Stack[0].Value().(bool)) + }, + }, + { + name: "positive, with arguments, result=true", + params: fmt.Sprintf(`[20,"%s", [{"type": "String", "value": "good_string"}, {"type": "Integer", "value": "4"}, {"type":"Boolean", "value": false}]]`, verifyWithArgsContractHash), + result: func(e *executor) interface{} { return &result.Invoke{} }, + check: func(t *testing.T, e *executor, inv interface{}) { + res, ok := inv.(*result.Invoke) + require.True(t, ok) + expectedInvScript := io.NewBufBinWriter() + emit.Int(expectedInvScript.BinWriter, 0) + emit.Int(expectedInvScript.BinWriter, int64(4)) + emit.String(expectedInvScript.BinWriter, "good_string") + require.NoError(t, expectedInvScript.Err) + assert.Equal(t, expectedInvScript.Bytes(), res.Script) // witness invocation script (pushes args of `verify` on stack) + assert.Equal(t, "HALT", res.State, res.FaultException) + assert.NotEqual(t, 0, res.GasConsumed) + assert.Equal(t, true, res.Stack[0].Value().(bool)) + }, + }, + { + name: "positive, with arguments, result=false", + params: fmt.Sprintf(`[20, "%s", [{"type": "String", "value": "invalid_string"}, {"type": "Integer", "value": "4"}, {"type":"Boolean", "value": false}]]`, verifyWithArgsContractHash), + result: func(e *executor) interface{} { return &result.Invoke{} }, + check: func(t *testing.T, e *executor, inv interface{}) { + res, ok := inv.(*result.Invoke) + require.True(t, ok) + expectedInvScript := io.NewBufBinWriter() + emit.Int(expectedInvScript.BinWriter, 0) + emit.Int(expectedInvScript.BinWriter, int64(4)) + emit.String(expectedInvScript.BinWriter, "invalid_string") + require.NoError(t, expectedInvScript.Err) + assert.Equal(t, expectedInvScript.Bytes(), res.Script) + assert.Equal(t, "HALT", res.State, res.FaultException) + assert.NotEqual(t, 0, res.GasConsumed) + assert.Equal(t, false, res.Stack[0].Value().(bool)) + }, + }, + { + name: "unknown contract", + params: fmt.Sprintf(`[20, "%s", []]`, util.Uint160{}.String()), + fail: true, + }, + { + name: "no params", + params: `[]`, + fail: true, + }, + { + name: "no args", + params: `[20]`, + fail: true, + }, + { + name: "not a string", + params: `[20,42, []]`, + fail: true, + }, + }, "sendrawtransaction": { { name: "positive",