Merge pull request #2431 from nspcc-dev/rpc/historicall

rpc: support historic calls
This commit is contained in:
Roman Khimov 2022-04-29 18:24:51 +03:00 committed by GitHub
commit 83f8ecf2e0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
41 changed files with 2055 additions and 591 deletions

View file

@ -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 to see how much GAS is burned with particular block (because system fees are
burned). 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 #### `submitnotaryrequest` call
This method can be used on P2P Notary enabled networks to submit new notary This method can be used on P2P Notary enabled networks to submit new notary

View file

@ -201,7 +201,8 @@ func TestAppCall(t *testing.T) {
} }
fc := fakechain.NewFakeChain() 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) { t.Run("valid script", func(t *testing.T) {
src := getAppCallScript(fmt.Sprintf("%#v", ih.BytesBE())) src := getAppCallScript(fmt.Sprintf("%#v", ih.BytesBE()))

View file

@ -76,7 +76,7 @@ func TestCreateBasicChain(t *testing.T) {
func initBasicChain(t *testing.T, e *neotest.Executor) { func initBasicChain(t *testing.T, e *neotest.Executor) {
if !e.Chain.GetConfig().P2PSigExtensions { 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 const neoAmount = 99999000

View file

@ -433,14 +433,9 @@ func (bc *Blockchain) init() error {
return fmt.Errorf("can't init MPT at height %d: %w", bHeight, err) 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 { if err != nil {
return fmt.Errorf("can't init cache for NEO native contract: %w", err) return fmt.Errorf("can't init 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)
} }
// Check autogenerated native contracts' manifests and NEFs against the stored ones. // 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, Root: block.PrevStateRoot,
}) })
err = bc.contracts.NEO.InitializeCache(bc, bc.dao) err = bc.initializeNativeCache(block.Index, bc.dao)
if err != nil { 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 { if err := bc.updateExtensibleWhitelist(p); err != nil {
return fmt.Errorf("failed to update extensible whitelist: %w", err) 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 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 // Run runs chain loop, it needs to be run as goroutine and executing it is
// critical for correct Blockchain operation. // critical for correct Blockchain operation.
func (bc *Blockchain) Run() { func (bc *Blockchain) Run() {
@ -1220,14 +1237,14 @@ func (bc *Blockchain) updateExtensibleWhitelist(height uint32) error {
return nil return nil
} }
newList := []util.Uint160{bc.contracts.NEO.GetCommitteeAddress()} newList := []util.Uint160{bc.contracts.NEO.GetCommitteeAddress(bc.dao)}
nextVals := bc.contracts.NEO.GetNextBlockValidatorsInternal() nextVals := bc.contracts.NEO.GetNextBlockValidatorsInternal(bc.dao)
script, err := smartcontract.CreateDefaultMultiSigRedeemScript(nextVals) script, err := smartcontract.CreateDefaultMultiSigRedeemScript(nextVals)
if err != nil { if err != nil {
return err return err
} }
newList = append(newList, hash.Hash160(script)) 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 { if len(stateVals) > 0 {
h, err := bc.contracts.Designate.GetLastDesignatedHash(bc.dao, noderoles.StateValidator) 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. // GetNEP17Contracts returns the list of deployed NEP-17 contracts.
func (bc *Blockchain) GetNEP17Contracts() []util.Uint160 { 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. // GetNEP11Contracts returns the list of deployed NEP-11 contracts.
func (bc *Blockchain) GetNEP11Contracts() []util.Uint160 { 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 // 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) curVC := bc.config.GetNumOfCNs(bc.BlockHeight() + 1)
if oldVC == nil || oldVC != curVC { if oldVC == nil || oldVC != curVC {
m := smartcontract.GetDefaultHonestNodeCount(curVC) m := smartcontract.GetDefaultHonestNodeCount(curVC)
verification, _ := smartcontract.CreateDefaultMultiSigRedeemScript(bc.contracts.NEO.GetNextBlockValidatorsInternal()) verification, _ := smartcontract.CreateDefaultMultiSigRedeemScript(bc.contracts.NEO.GetNextBlockValidatorsInternal(bc.dao))
defaultWitness = transaction.Witness{ defaultWitness = transaction.Witness{
InvocationScript: make([]byte, 66*m), InvocationScript: make([]byte, 66*m),
VerificationScript: verification, VerificationScript: verification,
@ -1939,7 +1956,7 @@ func (bc *Blockchain) verifyAndPoolTx(t *transaction.Transaction, pool *mempool.
if err != nil { if err != nil {
return err return err
} }
if err := bc.verifyTxAttributes(t, isPartialTx); err != nil { if err := bc.verifyTxAttributes(bc.dao, t, isPartialTx); err != nil {
return err return err
} }
err = pool.Add(t, feer, data...) err = pool.Add(t, feer, data...)
@ -1963,11 +1980,11 @@ func (bc *Blockchain) verifyAndPoolTx(t *transaction.Transaction, pool *mempool.
return nil 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 { for i := range tx.Attributes {
switch attrType := tx.Attributes[i].Type; attrType { switch attrType := tx.Attributes[i].Type; attrType {
case transaction.HighPriority: case transaction.HighPriority:
h := bc.contracts.NEO.GetCommitteeAddress() h := bc.contracts.NEO.GetCommitteeAddress(d)
if !tx.HasSigner(h) { if !tx.HasSigner(h) {
return fmt.Errorf("%w: high priority tx is not signed by committee", ErrInvalidAttribute) 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) { } else if txpool.HasConflicts(t, bc) {
return false return false
} }
if err := bc.verifyTxAttributes(t, isPartialTx); err != nil { if err := bc.verifyTxAttributes(bc.dao, t, isPartialTx); err != nil {
return false return false
} }
for i := range t.Scripts { 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. // GetCommittee returns the sorted list of public keys of nodes in committee.
func (bc *Blockchain) GetCommittee() (keys.PublicKeys, error) { func (bc *Blockchain) GetCommittee() (keys.PublicKeys, error) {
pubs := bc.contracts.NEO.GetCommitteeMembers() pubs := bc.contracts.NEO.GetCommitteeMembers(bc.dao)
sort.Sort(pubs) sort.Sort(pubs)
return pubs, nil return pubs, nil
} }
// GetValidators returns current validators. // GetValidators returns current validators.
func (bc *Blockchain) GetValidators() ([]*keys.PublicKey, error) { 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. // GetNextBlockValidators returns next block validators.
func (bc *Blockchain) GetNextBlockValidators() ([]*keys.PublicKey, error) { 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. // GetEnrollments returns all registered validators.
@ -2148,6 +2165,41 @@ func (bc *Blockchain) GetTestVM(t trigger.Type, tx *transaction.Transaction, b *
return systemInterop 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. // Various witness verification errors.
var ( var (
ErrWitnessHashMismatch = errors.New("witness hash mismatch") ErrWitnessHashMismatch = errors.New("witness hash mismatch")

View file

@ -11,7 +11,6 @@ import (
"github.com/nspcc-dev/neo-go/internal/testchain" "github.com/nspcc-dev/neo-go/internal/testchain"
"github.com/nspcc-dev/neo-go/pkg/config" "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/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/state"
"github.com/nspcc-dev/neo-go/pkg/core/storage" "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/core/transaction"
@ -329,7 +328,7 @@ func TestBlockchain_BaseExecFeeBaseStoragePrice_Compat(t *testing.T) {
bc := newTestChain(t) bc := newTestChain(t)
check := func(t *testing.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.GetBaseExecFee(), ic.BaseExecFee())
require.Equal(t, bc.GetStoragePrice(), ic.BaseStorageFee()) require.Equal(t, bc.GetStoragePrice(), ic.BaseStorageFee())
} }

View file

@ -60,6 +60,7 @@ type Blockchainer interface {
GetStateModule() StateRoot GetStateModule() StateRoot
GetStorageItem(id int32, key []byte) state.StorageItem GetStorageItem(id int32, key []byte) state.StorageItem
GetTestVM(t trigger.Type, tx *transaction.Transaction, b *block.Block) *interop.Context 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) GetTransaction(util.Uint256) (*transaction.Transaction, uint32, error)
SetOracle(service services.Oracle) SetOracle(service services.Oracle)
mempool.Feer // fee interface mempool.Feer // fee interface

View file

@ -15,4 +15,5 @@ type StateRoot interface {
GetState(root util.Uint256, key []byte) ([]byte, error) GetState(root util.Uint256, key []byte) ([]byte, error)
GetStateProof(root util.Uint256, key []byte) ([][]byte, error) GetStateProof(root util.Uint256, key []byte) ([][]byte, error)
GetStateRoot(height uint32) (*state.MPTRoot, error) GetStateRoot(height uint32) (*state.MPTRoot, error)
GetLatestStateHeight(root util.Uint256) (uint32, error)
} }

View file

@ -7,6 +7,7 @@ import (
"errors" "errors"
"fmt" "fmt"
iocore "io" iocore "io"
"sync"
"github.com/nspcc-dev/neo-go/pkg/core/block" "github.com/nspcc-dev/neo-go/pkg/core/block"
"github.com/nspcc-dev/neo-go/pkg/core/state" "github.com/nspcc-dev/neo-go/pkg/core/state"
@ -34,11 +35,28 @@ var (
type Simple struct { type Simple struct {
Version Version Version Version
Store *storage.MemCachedStore 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 private bool
keyBuf []byte keyBuf []byte
dataBuf *io.BufBinWriter 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. // NewSimple creates new simple dao using provided backend store.
func NewSimple(backend storage.Store, stateRootInHeader bool, p2pSigExtensions bool) *Simple { func NewSimple(backend storage.Store, stateRootInHeader bool, p2pSigExtensions bool) *Simple {
st := storage.NewMemCachedStore(backend) st := storage.NewMemCachedStore(backend)
@ -52,7 +70,8 @@ func newSimple(st *storage.MemCachedStore, stateRootInHeader bool, p2pSigExtensi
StateRootInHeader: stateRootInHeader, StateRootInHeader: stateRootInHeader,
P2PSigExtensions: p2pSigExtensions, 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 { func (dao *Simple) GetWrapped() *Simple {
d := NewSimple(dao.Store, dao.Version.StateRootInHeader, dao.Version.P2PSigExtensions) d := NewSimple(dao.Store, dao.Version.StateRootInHeader, dao.Version.P2PSigExtensions)
d.Version = dao.Version d.Version = dao.Version
d.nativeCachePS = dao
return d return d
} }
// GetPrivate returns new DAO instance with another layer of private // GetPrivate returns new DAO instance with another layer of private
// MemCachedStore around the current DAO Store. // MemCachedStore around the current DAO Store.
func (dao *Simple) GetPrivate() *Simple { func (dao *Simple) GetPrivate() *Simple {
d := &Simple{} d := &Simple{
*d = *dao // Inherit everything... Version: dao.Version,
keyBuf: dao.keyBuf,
dataBuf: dao.dataBuf,
} // Inherit everything...
d.Store = storage.NewPrivateMemCachedStore(dao.Store) // except storage, wrap another layer. d.Store = storage.NewPrivateMemCachedStore(dao.Store) // except storage, wrap another layer.
d.private = true 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 return d
} }
@ -809,6 +839,17 @@ func (dao *Simple) getDataBuf() *io.BufBinWriter {
// Persist flushes all the changes made into the (supposedly) persistent // Persist flushes all the changes made into the (supposedly) persistent
// underlying store. It doesn't block accesses to DAO from other threads. // underlying store. It doesn't block accesses to DAO from other threads.
func (dao *Simple) Persist() (int, error) { 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() 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 // underlying store. It's a synchronous version of Persist that doesn't allow
// other threads to work with DAO while flushing the Store. // other threads to work with DAO while flushing the Store.
func (dao *Simple) PersistSync() (int, error) { 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() 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
}

View file

@ -13,6 +13,7 @@ import (
"github.com/nspcc-dev/neo-go/pkg/core/dao" "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/interop/interopnames"
"github.com/nspcc-dev/neo-go/pkg/core/state" "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/core/transaction"
"github.com/nspcc-dev/neo-go/pkg/crypto/hash" "github.com/nspcc-dev/neo-go/pkg/crypto/hash"
"github.com/nspcc-dev/neo-go/pkg/io" "github.com/nspcc-dev/neo-go/pkg/io"
@ -339,3 +340,31 @@ func (ic *Context) Exec() error {
defer ic.Finalize() defer ic.Finalize()
return ic.VM.Run() 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
}

View file

@ -18,7 +18,7 @@ import (
) )
type policyChecker interface { type policyChecker interface {
IsBlockedInternal(*dao.Simple, util.Uint160) bool IsBlocked(*dao.Simple, util.Uint160) bool
} }
// LoadToken calls method specified by token id. // 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 { for _, nc := range ic.Natives {
if nc.Metadata().Name == nativenames.Policy { if nc.Metadata().Name == nativenames.Policy {
var pch = nc.(policyChecker) 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()) return fmt.Errorf("contract %s is blocked", cs.Hash.StringLE())
} }
break break

View file

@ -12,7 +12,6 @@ import (
"github.com/nspcc-dev/neo-go/pkg/config" "github.com/nspcc-dev/neo-go/pkg/config"
"github.com/nspcc-dev/neo-go/pkg/config/netmode" "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/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"
"github.com/nspcc-dev/neo-go/pkg/core/interop/contract" "github.com/nspcc-dev/neo-go/pkg/core/interop/contract"
"github.com/nspcc-dev/neo-go/pkg/core/interop/iterator" "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. // 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) chain := newTestChain(t)
context := chain.newInteropContext(trigger.Application, 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() v := context.SpawnVM()
return v, context, chain return v, context, chain
} }
@ -552,10 +551,7 @@ func createVMAndContractState(t testing.TB) (*vm.VM, *state.Contract, *interop.C
}, },
} }
chain := newTestChain(t) v, context, chain := createVM(t)
d := dao.NewSimple(storage.NewMemoryStore(), chain.config.StateRootInHeader, chain.config.P2PSigExtensions)
context := chain.newInteropContext(trigger.Application, d, nil, nil)
v := context.SpawnVM()
return v, contractState, context, chain return v, contractState, context, chain
} }

View file

@ -5,9 +5,7 @@ import (
"runtime" "runtime"
"testing" "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/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/smartcontract/trigger"
"github.com/nspcc-dev/neo-go/pkg/vm" "github.com/nspcc-dev/neo-go/pkg/vm"
"github.com/stretchr/testify/require" "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 := vm.New()
v.Estack().PushVal(value) v.Estack().PushVal(value)
chain := newTestChain(t) chain := newTestChain(t)
d := dao.NewSimple(storage.NewMemoryStore(), chain.config.StateRootInHeader, chain.config.P2PSigExtensions) context := chain.newInteropContext(trigger.Application, chain.dao, nil, nil)
context := chain.newInteropContext(trigger.Application, d, nil, nil)
context.VM = v context.VM = v
require.Error(t, f(context)) require.Error(t, f(context))
} }

View file

@ -205,7 +205,7 @@ func (b *Billet) incrementRefAndStore(h util.Uint256, bs []byte) {
// returned from `process` function. It also replaces all HashNodes to their // returned from `process` function. It also replaces all HashNodes to their
// "unhashed" counterparts until the stop condition is satisfied. // "unhashed" counterparts until the stop condition is satisfied.
func (b *Billet) Traverse(process func(pathToNode []byte, node Node, nodeBytes []byte) bool, ignoreStorageErr bool) error { 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) { if err != nil && !errors.Is(err, errStop) {
return err return err
} }
@ -213,7 +213,7 @@ func (b *Billet) Traverse(process func(pathToNode []byte, node Node, nodeBytes [
return nil 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 { if _, ok := curr.(EmptyNode); ok {
// We're not interested in EmptyNodes, and they do not affect the // We're not interested in EmptyNodes, and they do not affect the
// traversal process, thus remain them untouched. // 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 nil, err
} }
return b.traverse(r, path, from, process, ignoreStorageErr) return b.traverse(r, path, from, process, ignoreStorageErr, backwards)
} }
if len(from) == 0 { if len(from) == 0 {
bytes := slice.Copy(curr.Bytes()) bytes := slice.Copy(curr.Bytes())
@ -242,22 +242,36 @@ func (b *Billet) traverse(curr Node, path, from []byte, process func(pathToNode
var ( var (
startIndex byte startIndex byte
endIndex byte = childrenCount 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 { if len(from) != 0 {
endIndex = lastChild endIndex = lastChild
if backwards {
endIndex = 0
}
startIndex, from = splitPath(from) startIndex, from = splitPath(from)
} }
for i := startIndex; i < endIndex; i++ { for i := int(startIndex); cmp(i); i += step {
var newPath []byte var newPath []byte
if i == lastChild { if i == lastChild {
newPath = path newPath = path
} else { } else {
newPath = append(path, i) newPath = append(path, byte(i))
} }
if i != startIndex { if byte(i) != startIndex {
from = []byte{} 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 err != nil {
if !errors.Is(err, errStop) { if !errors.Is(err, errStop) {
return nil, err return nil, err
@ -276,7 +290,7 @@ func (b *Billet) traverse(curr Node, path, from []byte, process func(pathToNode
} else { } else {
return b.tryCollapseExtension(n), nil 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) { if err != nil && !errors.Is(err, errStop) {
return nil, err return nil, err
} }

View file

@ -625,7 +625,7 @@ func (t *Trie) Find(prefix, from []byte, max int) ([]storage.KeyValue, error) {
} }
return count >= max 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) { if err != nil && !errors.Is(err, errStop) {
return nil, err return nil, err
} }

126
pkg/core/mpt/trie_store.go Normal file
View file

@ -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
}

View file

@ -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)
})
})
})
}

View file

@ -76,7 +76,7 @@ func NewContracts(cfg config.ProtocolConfiguration) *Contracts {
cs.Contracts = append(cs.Contracts, ledger) cs.Contracts = append(cs.Contracts, ledger)
gas := newGAS(int64(cfg.InitialGASSupply), cfg.P2PSigExtensions) gas := newGAS(int64(cfg.InitialGASSupply), cfg.P2PSigExtensions)
neo := newNEO() neo := newNEO(cfg)
policy := newPolicy() policy := newPolicy()
neo.GAS = gas neo.GAS = gas
neo.Policy = policy neo.Policy = policy

View file

@ -3,6 +3,7 @@ package native
import ( import (
"encoding/binary" "encoding/binary"
"errors" "errors"
"fmt"
"math" "math"
"math/big" "math/big"
"sort" "sort"
@ -31,12 +32,6 @@ type Designate struct {
interop.ContractMD interop.ContractMD
NEO *NEO 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 defines whether the P2P signature extensions logic is relevant.
p2pSigExtensionsEnabled bool p2pSigExtensionsEnabled bool
@ -53,6 +48,16 @@ type roleData struct {
height uint32 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 ( const (
designateContractID = -8 designateContractID = -8
@ -73,6 +78,22 @@ var (
ErrNoBlock = errors.New("no persisting block in the context") 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 { func (s *Designate) isValidRole(r noderoles.Role) bool {
return r == noderoles.Oracle || r == noderoles.StateValidator || return r == noderoles.Oracle || r == noderoles.StateValidator ||
r == noderoles.NeoFSAlphabet || (s.p2pSigExtensionsEnabled && r == noderoles.P2PNotary) r == noderoles.NeoFSAlphabet || (s.p2pSigExtensionsEnabled && r == noderoles.P2PNotary)
@ -102,8 +123,30 @@ func newDesignate(p2pSigExtensionsEnabled bool) *Designate {
return s 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 { 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 return nil
} }
@ -114,26 +157,19 @@ func (s *Designate) OnPersist(ic *interop.Context) error {
// PostPersist implements Contract interface. // PostPersist implements Contract interface.
func (s *Designate) PostPersist(ic *interop.Context) error { func (s *Designate) PostPersist(ic *interop.Context) error {
if !s.rolesChanged() { cache := ic.DAO.GetRWCache(s.ID).(*DesignationCache)
if !cache.rolesChangedFlag {
return nil return nil
} }
if err := s.updateCachedRoleData(&s.oracles, ic.DAO, noderoles.Oracle); err != nil { s.notifyRoleChanged(&cache.oracles, noderoles.Oracle)
return err s.notifyRoleChanged(&cache.stateVals, noderoles.StateValidator)
} s.notifyRoleChanged(&cache.neofsAlphabet, noderoles.NeoFSAlphabet)
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
}
if s.p2pSigExtensionsEnabled { if s.p2pSigExtensionsEnabled {
if err := s.updateCachedRoleData(&s.notaries, ic.DAO, noderoles.P2PNotary); err != nil { s.notifyRoleChanged(&cache.notaries, noderoles.P2PNotary)
return err
}
} }
s.rolesChangedFlag.Store(false) cache.rolesChangedFlag = false
return nil return nil
} }
@ -152,7 +188,7 @@ func (s *Designate) getDesignatedByRole(ic *interop.Context, args []stackitem.It
panic(ErrInvalidIndex) panic(ErrInvalidIndex)
} }
index := ind.Uint64() index := ind.Uint64()
if index > uint64(ic.Chain.BlockHeight()+1) { if index > uint64(ic.BlockHeight()+1) {
panic(ErrInvalidIndex) panic(ErrInvalidIndex)
} }
pubs, _, err := s.GetDesignatedByRole(ic.DAO, r, uint32(index)) 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) 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 { func (s *Designate) hashFromNodes(r noderoles.Role, nodes keys.PublicKeys) util.Uint160 {
if len(nodes) == 0 { if len(nodes) == 0 {
return util.Uint160{} return util.Uint160{}
@ -181,47 +212,58 @@ func (s *Designate) hashFromNodes(r noderoles.Role, nodes keys.PublicKeys) util.
return hash.Hash160(script) return hash.Hash160(script)
} }
func (s *Designate) updateCachedRoleData(v *atomic.Value, d *dao.Simple, r noderoles.Role) error { // updateCachedRoleData fetches the most recent role data from the storage and
nodeKeys, height, err := s.GetDesignatedByRole(d, r, math.MaxUint32) // 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 { if err != nil {
return err return err
} }
v.Store(&roleData{ v.nodes = nodeKeys
nodes: nodeKeys, v.addr = s.hashFromNodes(r, nodeKeys)
addr: s.hashFromNodes(r, nodeKeys), v.height = height
height: height, cache.rolesChangedFlag = true
})
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())
}
}
return nil return nil
} }
func (s *Designate) getCachedRoleData(r noderoles.Role) *roleData { func (s *Designate) notifyRoleChanged(v *roleData, r noderoles.Role) {
var val interface{}
switch r { switch r {
case noderoles.Oracle: case noderoles.Oracle:
val = s.oracles.Load() if orc, _ := s.OracleService.Load().(services.Oracle); orc != nil {
case noderoles.StateValidator: orc.UpdateOracleNodes(v.nodes.Copy())
val = s.stateVals.Load() }
case noderoles.NeoFSAlphabet:
val = s.neofsAlphabet.Load()
case noderoles.P2PNotary: 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 return nil
} }
@ -231,17 +273,11 @@ func (s *Designate) GetLastDesignatedHash(d *dao.Simple, r noderoles.Role) (util
if !s.isValidRole(r) { if !s.isValidRole(r) {
return util.Uint160{}, ErrInvalidRole return util.Uint160{}, ErrInvalidRole
} }
if !s.rolesChanged() { cache := d.GetROCache(s.ID).(*DesignationCache)
if val := s.getCachedRoleData(r); val != nil { if val := getCachedRoleData(cache, r); val != nil {
return val.addr, nil return val.addr, nil
}
} }
nodes, _, err := s.GetDesignatedByRole(d, r, math.MaxUint32) return util.Uint160{}, nil
if err != nil {
return util.Uint160{}, err
}
// We only have hashing defined for oracles now.
return s.hashFromNodes(r, nodes), nil
} }
// GetDesignatedByRole returns nodes for role r. // 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) { if !s.isValidRole(r) {
return nil, 0, ErrInvalidRole return nil, 0, ErrInvalidRole
} }
if !s.rolesChanged() { cache := d.GetROCache(s.ID).(*DesignationCache)
if val := s.getCachedRoleData(r); val != nil && val.height <= index { if val := getCachedRoleData(cache, r); val != nil {
if val.height <= index {
return val.nodes.Copy(), val.height, nil 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 ( var (
ns NodeList ns NodeList
bestIndex uint32 bestIndex uint32
@ -310,7 +357,7 @@ func (s *Designate) DesignateAsRole(ic *interop.Context, r noderoles.Role, pubs
if !s.isValidRole(r) { if !s.isValidRole(r) {
return ErrInvalidRole return ErrInvalidRole
} }
h := s.NEO.GetCommitteeAddress() h := s.NEO.GetCommitteeAddress(ic.DAO)
if ok, err := runtime.CheckHashedWitness(ic, h); err != nil || !ok { if ok, err := runtime.CheckHashedWitness(ic, h); err != nil || !ok {
return ErrInvalidWitness return ErrInvalidWitness
} }
@ -327,12 +374,18 @@ func (s *Designate) DesignateAsRole(ic *interop.Context, r noderoles.Role, pubs
} }
sort.Sort(pubs) sort.Sort(pubs)
nl := NodeList(pubs) nl := NodeList(pubs)
s.rolesChangedFlag.Store(true)
err := putConvertibleToDAO(s.ID, ic.DAO, key, &nl) err := putConvertibleToDAO(s.ID, ic.DAO, key, &nl)
if err != nil { if err != nil {
return err 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{ ic.Notifications = append(ic.Notifications, state.NotificationEvent{
ScriptHash: s.Hash, ScriptHash: s.Hash,
Name: DesignationEventName, Name: DesignationEventName,
@ -355,8 +408,3 @@ func (s *Designate) getRole(item stackitem.Item) (noderoles.Role, bool) {
u := bi.Uint64() u := bi.Uint64()
return noderoles.Role(u), u <= math.MaxUint8 && s.isValidRole(noderoles.Role(u)) 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)
}

View file

@ -31,7 +31,7 @@ func Call(ic *interop.Context) error {
if len(history) == 0 { if len(history) == 0 {
return fmt.Errorf("native contract %s is disabled", c.Metadata().Name) 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]) 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()) m, ok := c.Metadata().GetMethodByOffset(ic.VM.Context().IP())

View file

@ -103,19 +103,19 @@ func (l *Ledger) PostPersist(ic *interop.Context) error {
// currentHash implements currentHash SC method. // currentHash implements currentHash SC method.
func (l *Ledger) currentHash(ic *interop.Context, _ []stackitem.Item) stackitem.Item { 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. // currentIndex implements currentIndex SC method.
func (l *Ledger) currentIndex(ic *interop.Context, _ []stackitem.Item) stackitem.Item { 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. // getBlock implements getBlock SC method.
func (l *Ledger) getBlock(ic *interop.Context, params []stackitem.Item) stackitem.Item { func (l *Ledger) getBlock(ic *interop.Context, params []stackitem.Item) stackitem.Item {
hash := getBlockHashFromItem(ic.Chain, params[0]) hash := getBlockHashFromItem(ic, params[0])
block, err := ic.Chain.GetBlock(hash) block, err := ic.GetBlock(hash)
if err != nil || !isTraceableBlock(ic.Chain, block.Index) { if err != nil || !isTraceableBlock(ic, block.Index) {
return stackitem.Null{} return stackitem.Null{}
} }
return BlockToStackItem(block) return BlockToStackItem(block)
@ -124,7 +124,7 @@ func (l *Ledger) getBlock(ic *interop.Context, params []stackitem.Item) stackite
// getTransaction returns transaction to the SC. // getTransaction returns transaction to the SC.
func (l *Ledger) getTransaction(ic *interop.Context, params []stackitem.Item) stackitem.Item { func (l *Ledger) getTransaction(ic *interop.Context, params []stackitem.Item) stackitem.Item {
tx, h, err := getTransactionAndHeight(ic.DAO, params[0]) 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 stackitem.Null{}
} }
return TransactionToStackItem(tx) 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. // getTransactionHeight returns transaction height to the SC.
func (l *Ledger) getTransactionHeight(ic *interop.Context, params []stackitem.Item) stackitem.Item { func (l *Ledger) getTransactionHeight(ic *interop.Context, params []stackitem.Item) stackitem.Item {
_, h, err := getTransactionAndHeight(ic.DAO, params[0]) _, 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(-1)
} }
return stackitem.Make(h) 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 // getTransactionFromBlock returns transaction with the given index from the
// block with height or hash specified. // block with height or hash specified.
func (l *Ledger) getTransactionFromBlock(ic *interop.Context, params []stackitem.Item) stackitem.Item { 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]) index := toUint32(params[1])
block, err := ic.Chain.GetBlock(hash) block, err := ic.GetBlock(hash)
if err != nil || !isTraceableBlock(ic.Chain, block.Index) { if err != nil || !isTraceableBlock(ic, block.Index) {
return stackitem.Null{} return stackitem.Null{}
} }
if index >= uint32(len(block.Transactions)) { 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. // getTransactionSigners returns transaction signers to the SC.
func (l *Ledger) getTransactionSigners(ic *interop.Context, params []stackitem.Item) stackitem.Item { func (l *Ledger) getTransactionSigners(ic *interop.Context, params []stackitem.Item) stackitem.Item {
tx, h, err := getTransactionAndHeight(ic.DAO, params[0]) 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 stackitem.Null{}
} }
return SignersToStackItem(tx.Signers) return SignersToStackItem(tx.Signers)
@ -170,7 +170,7 @@ func (l *Ledger) getTransactionVMState(ic *interop.Context, params []stackitem.I
panic(err) panic(err)
} }
h, _, aer, err := ic.DAO.GetTxExecResult(hash) 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(vm.NoneState)
} }
return stackitem.Make(aer.VMState) 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 // isTraceableBlock defines whether we're able to give information about
// the block with index specified. // the block with index specified.
func isTraceableBlock(bc interop.Ledger, index uint32) bool { func isTraceableBlock(ic *interop.Context, index uint32) bool {
height := bc.BlockHeight() height := ic.BlockHeight()
MaxTraceableBlocks := bc.GetConfig().MaxTraceableBlocks MaxTraceableBlocks := ic.Chain.GetConfig().MaxTraceableBlocks
return index <= height && index+MaxTraceableBlocks > height 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 // Ledger if needed. Interop functions accept both block numbers and
// block hashes as parameters, thus this function is needed. It's supposed to // 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. // 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() bigindex, err := item.TryInteger()
if err == nil && bigindex.IsUint64() { if err == nil && bigindex.IsUint64() {
index := bigindex.Uint64() index := bigindex.Uint64()
if index > math.MaxUint32 { if index > math.MaxUint32 {
panic("bad block index") panic("bad block index")
} }
if uint32(index) > bc.BlockHeight() { if uint32(index) > ic.BlockHeight() {
panic(fmt.Errorf("no block with index %d", index)) panic(fmt.Errorf("no block with index %d", index))
} }
return bc.GetHeaderHash(int(index)) return ic.Chain.GetHeaderHash(int(index))
} }
hash, err := getUint256FromItem(item) hash, err := getUint256FromItem(item)
if err != nil { if err != nil {

View file

@ -6,7 +6,6 @@ import (
"fmt" "fmt"
"math" "math"
"math/big" "math/big"
"sync"
"unicode/utf8" "unicode/utf8"
"github.com/nspcc-dev/neo-go/pkg/core/dao" "github.com/nspcc-dev/neo-go/pkg/core/dao"
@ -30,8 +29,9 @@ import (
type Management struct { type Management struct {
interop.ContractMD interop.ContractMD
NEO *NEO NEO *NEO
}
mtx sync.RWMutex type ManagementCache struct {
contracts map[util.Uint160]*state.Contract contracts map[util.Uint160]*state.Contract
// nep11 is a map of NEP11-compliant contracts which is updated with every PostPersist. // nep11 is a map of NEP11-compliant contracts which is updated with every PostPersist.
nep11 map[util.Uint160]struct{} nep11 map[util.Uint160]struct{}
@ -57,6 +57,33 @@ var (
keyMinimumDeploymentFee = []byte{20} 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. // MakeContractKey creates a key from account script hash.
func MakeContractKey(h util.Uint160) []byte { func MakeContractKey(h util.Uint160) []byte {
return makeUint160Key(prefixContract, h) return makeUint160Key(prefixContract, h)
@ -66,9 +93,6 @@ func MakeContractKey(h util.Uint160) []byte {
func newManagement() *Management { func newManagement() *Management {
var m = &Management{ var m = &Management{
ContractMD: *interop.NewContractMD(nativenames.Management, ManagementContractID), 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() 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. // GetContract returns contract with given hash from given DAO.
func (m *Management) GetContract(d *dao.Simple, hash util.Uint160) (*state.Contract, error) { func (m *Management) GetContract(d *dao.Simple, hash util.Uint160) (*state.Contract, error) {
m.mtx.RLock() cache := d.GetROCache(m.ID).(*ManagementCache)
cs, ok := m.contracts[hash] cs, ok := cache.contracts[hash]
m.mtx.RUnlock()
if !ok { if !ok {
return nil, storage.ErrKeyNotFound return nil, storage.ErrKeyNotFound
} else if cs != nil {
return cs, nil
} }
return m.getContractFromDAO(d, hash) return cs, nil
}
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
} }
func getLimitedSlice(arg stackitem.Item, max int) ([]byte, error) { 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) return contractToStack(newcontract)
} }
func (m *Management) markUpdated(h util.Uint160) { func (m *Management) markUpdated(d *dao.Simple, hash util.Uint160, cs *state.Contract) {
m.mtx.Lock() cache := d.GetRWCache(m.ID).(*ManagementCache)
// Just set it to nil, to refresh cache in `PostPersist`. delete(cache.nep11, hash)
m.contracts[h] = nil delete(cache.nep17, hash)
m.mtx.Unlock() 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. // 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. // 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) { 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) h := state.CreateContractHash(sender, neff.Checksum, manif.Name)
key := MakeContractKey(h) _, err := m.GetContract(d, h)
si := d.GetStorageItem(m.ID, key) if err == nil {
if si != nil {
return nil, errors.New("contract already exists") return nil, errors.New("contract already exists")
} }
id, err := m.getNextContractID(d) 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 { if err != nil {
return nil, err return nil, err
} }
m.markUpdated(newcontract.Hash)
return newcontract, nil 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. contract = *oldcontract // Make a copy, don't ruin (potentially) cached contract.
// if NEF was provided, update the contract script // if NEF was provided, update the contract script
if neff != nil { if neff != nil {
m.markUpdated(hash)
contract.NEF = *neff contract.NEF = *neff
} }
// if manifest was provided, update the contract manifest // 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 { if err != nil {
return nil, fmt.Errorf("invalid manifest: %w", err) return nil, fmt.Errorf("invalid manifest: %w", err)
} }
m.markUpdated(hash)
contract.Manifest = *manif contract.Manifest = *manif
} }
err = checkScriptAndMethods(contract.NEF.Script, contract.Manifest.ABI.Methods) 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) d.DeleteStorageItem(contract.ID, k)
return true return true
}) })
m.markUpdated(hash) m.markUpdated(d, hash, nil)
return 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 // updateContractCache saves contract in the common and NEP-related caches. It's
// an internal method that must be called with m.mtx lock taken. // an internal method that must be called with m.mtx lock taken.
func (m *Management) updateContractCache(cs *state.Contract) { func updateContractCache(cache *ManagementCache, cs *state.Contract) {
m.contracts[cs.Hash] = cs cache.contracts[cs.Hash] = cs
if cs.Manifest.IsStandardSupported(manifest.NEP11StandardName) { if cs.Manifest.IsStandardSupported(manifest.NEP11StandardName) {
m.nep11[cs.Hash] = struct{}{} cache.nep11[cs.Hash] = struct{}{}
} }
if cs.Manifest.IsStandardSupported(manifest.NEP17StandardName) { if cs.Manifest.IsStandardSupported(manifest.NEP17StandardName) {
m.nep17[cs.Hash] = struct{}{} cache.nep17[cs.Hash] = struct{}{}
} }
} }
// OnPersist implements Contract interface. // OnPersist implements Contract interface.
func (m *Management) OnPersist(ic *interop.Context) error { func (m *Management) OnPersist(ic *interop.Context) error {
var cache *ManagementCache
for _, native := range ic.Natives { for _, native := range ic.Natives {
md := native.Metadata() md := native.Metadata()
history := md.UpdateHistory history := md.UpdateHistory
@ -466,16 +478,17 @@ func (m *Management) OnPersist(ic *interop.Context) error {
cs := &state.Contract{ cs := &state.Contract{
ContractBase: md.ContractBase, ContractBase: md.ContractBase,
} }
err := m.PutContractState(ic.DAO, cs)
if err != nil {
return err
}
if err := native.Initialize(ic); err != nil { if err := native.Initialize(ic); err != nil {
return fmt.Errorf("initializing %s native contract: %w", md.Name, err) return fmt.Errorf("initializing %s native contract: %w", md.Name, err)
} }
m.mtx.Lock() err := m.putContractState(ic.DAO, cs, false) // Perform cache update manually.
m.updateContractCache(cs) if err != nil {
m.mtx.Unlock() return err
}
if cache == nil {
cache = ic.DAO.GetRWCache(m.ID).(*ManagementCache)
}
updateContractCache(cache, cs)
} }
return nil 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 // Cache initialisation should be done apart from Initialize because Initialize is
// called only when deploying native contracts. // called only when deploying native contracts.
func (m *Management) InitializeCache(d *dao.Simple) error { func (m *Management) InitializeCache(d *dao.Simple) error {
m.mtx.Lock() cache := &ManagementCache{
defer m.mtx.Unlock() contracts: make(map[util.Uint160]*state.Contract),
nep11: make(map[util.Uint160]struct{}),
nep17: make(map[util.Uint160]struct{}),
}
var initErr error var initErr error
d.Seek(m.ID, storage.SeekRange{Prefix: []byte{prefixContract}}, func(_, v []byte) bool { 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 { if initErr != nil {
return false return false
} }
m.updateContractCache(cs) updateContractCache(cache, cs)
return true return true
}) })
return initErr if initErr != nil {
return initErr
}
d.SetCache(m.ID, cache)
return nil
} }
// PostPersist implements Contract interface. // PostPersist implements Contract interface.
func (m *Management) PostPersist(ic *interop.Context) error { 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 return nil
} }
// GetNEP11Contracts returns hashes of all deployed contracts that support NEP-11 standard. The list // 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 updated every PostPersist, so until PostPersist is called, the result for the previous block
// is returned. // is returned.
func (m *Management) GetNEP11Contracts() []util.Uint160 { func (m *Management) GetNEP11Contracts(d *dao.Simple) []util.Uint160 {
m.mtx.RLock() cache := d.GetROCache(m.ID).(*ManagementCache)
result := make([]util.Uint160, 0, len(m.nep11)) result := make([]util.Uint160, 0, len(cache.nep11))
for h := range m.nep11 { for h := range cache.nep11 {
result = append(result, h) result = append(result, h)
} }
m.mtx.RUnlock()
return result return result
} }
// GetNEP17Contracts returns hashes of all deployed contracts that support NEP-17 standard. The list // 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 updated every PostPersist, so until PostPersist is called, the result for the previous block
// is returned. // is returned.
func (m *Management) GetNEP17Contracts() []util.Uint160 { func (m *Management) GetNEP17Contracts(d *dao.Simple) []util.Uint160 {
m.mtx.RLock() cache := d.GetROCache(m.ID).(*ManagementCache)
result := make([]util.Uint160, 0, len(m.nep17)) result := make([]util.Uint160, 0, len(cache.nep17))
for h := range m.nep17 { for h := range cache.nep17 {
result = append(result, h) result = append(result, h)
} }
m.mtx.RUnlock()
return result return result
} }
@ -552,16 +554,30 @@ func (m *Management) GetNEP17Contracts() []util.Uint160 {
func (m *Management) Initialize(ic *interop.Context) error { func (m *Management) Initialize(ic *interop.Context) error {
setIntWithKey(m.ID, ic.DAO, keyMinimumDeploymentFee, defaultMinimumDeploymentFee) setIntWithKey(m.ID, ic.DAO, keyMinimumDeploymentFee, defaultMinimumDeploymentFee)
setIntWithKey(m.ID, ic.DAO, keyNextAvailableID, 1) 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 return nil
} }
// PutContractState saves given contract state into given DAO. // PutContractState saves given contract state into given DAO.
func (m *Management) PutContractState(d *dao.Simple, cs *state.Contract) error { 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) key := MakeContractKey(cs.Hash)
if err := putConvertibleToDAO(m.ID, d, key, cs); err != nil { if err := putConvertibleToDAO(m.ID, d, key, cs); err != nil {
return err return err
} }
m.markUpdated(cs.Hash) if updateCache {
m.markUpdated(d, cs.Hash, cs)
}
if cs.UpdateCounter != 0 { // Update. if cs.UpdateCounter != 0 { // Update.
return nil return nil
} }

View file

@ -89,8 +89,11 @@ func TestManagement_GetNEP17Contracts(t *testing.T) {
d := dao.NewSimple(storage.NewMemoryStore(), false, false) d := dao.NewSimple(storage.NewMemoryStore(), false, false)
err := mgmt.Initialize(&interop.Context{DAO: d}) err := mgmt.Initialize(&interop.Context{DAO: d})
require.NoError(t, err) 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 // Deploy NEP-17 contract
script := []byte{byte(opcode.RET)} script := []byte{byte(opcode.RET)}
@ -104,29 +107,46 @@ func TestManagement_GetNEP17Contracts(t *testing.T) {
Parameters: []manifest.Parameter{}, Parameters: []manifest.Parameter{},
}) })
manif.SupportedStandards = []string{manifest.NEP17StandardName} 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) require.NoError(t, err)
// PostPersist is not yet called, thus no NEP-17 contracts are expected // c1 contract hash should be returned, as private DAO already contains changed cache.
require.Empty(t, mgmt.GetNEP17Contracts()) require.Equal(t, []util.Uint160{c1.Hash}, mgmt.GetNEP17Contracts(private))
// Call PostPersist, check c1 contract hash is returned // Lower DAO still shouldn't contain c1, as no Persist was called.
require.NoError(t, mgmt.PostPersist(&interop.Context{DAO: d})) require.Empty(t, mgmt.GetNEP17Contracts(d))
require.Equal(t, []util.Uint160{c1.Hash}, mgmt.GetNEP17Contracts())
// 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 // Update contract
private = d.GetPrivate()
manif.ABI.Methods = append(manif.ABI.Methods, manifest.Method{ manif.ABI.Methods = append(manif.ABI.Methods, manifest.Method{
Name: "dummy2", Name: "dummy2",
ReturnType: smartcontract.VoidType, ReturnType: smartcontract.VoidType,
Parameters: []manifest.Parameter{}, 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.NoError(t, err)
require.Equal(t, c1.Hash, c1Updated.Hash)
// No changes expected before PostPersist call. // No changes expected in lower store.
require.Equal(t, []util.Uint160{c1.Hash}, mgmt.GetNEP17Contracts()) 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 // Call Persist, check c1Updated state is returned from lower.
require.NoError(t, mgmt.PostPersist(&interop.Context{DAO: d})) _, err = private.Persist()
require.Equal(t, []util.Uint160{c2.Hash}, mgmt.GetNEP17Contracts()) 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))
} }

View file

@ -108,7 +108,7 @@ func (g *GAS) OnPersist(ic *interop.Context) error {
absAmount := big.NewInt(tx.SystemFee + tx.NetworkFee) absAmount := big.NewInt(tx.SystemFee + tx.NetworkFee)
g.burn(ic, tx.Sender(), absAmount) g.burn(ic, tx.Sender(), absAmount)
} }
validators := g.NEO.GetNextBlockValidatorsInternal() validators := g.NEO.GetNextBlockValidatorsInternal(ic.DAO)
primary := validators[ic.Block.PrimaryIndex].GetScriptHash() primary := validators[ic.Block.PrimaryIndex].GetScriptHash()
var netFee int64 var netFee int64
for _, tx := range ic.Block.Transactions { for _, tx := range ic.Block.Transactions {

View file

@ -8,7 +8,6 @@ import (
"math/big" "math/big"
"sort" "sort"
"strings" "strings"
"sync/atomic"
"github.com/nspcc-dev/neo-go/pkg/config" "github.com/nspcc-dev/neo-go/pkg/config"
"github.com/nspcc-dev/neo-go/pkg/core/dao" "github.com/nspcc-dev/neo-go/pkg/core/dao"
@ -35,33 +34,33 @@ type NEO struct {
GAS *GAS GAS *GAS
Policy *Policy Policy *Policy
// gasPerBlock represents current value of generated gas per block. // Configuration and standby keys are set in constructor and then
// It is append-only and doesn't need to be copied when used. // only read from.
gasPerBlock atomic.Value cfg config.ProtocolConfiguration
gasPerBlockChanged atomic.Value standbyKeys keys.PublicKeys
}
registerPrice atomic.Value type NeoCache struct {
registerPriceChanged atomic.Value // gasPerBlock represents the history of generated gas per block.
gasPerBlock gasRecord
votesChanged atomic.Value registerPrice int64
nextValidators atomic.Value
validators atomic.Value votesChanged bool
nextValidators keys.PublicKeys
validators keys.PublicKeys
// committee contains cached committee members and their votes. // committee contains cached committee members and their votes.
// It is updated once in a while depending on committee size // It is updated once in a while depending on committee size
// (every 28 blocks for mainnet). It's value // (every 28 blocks for mainnet). It's value
// is always equal to value stored by `prefixCommittee`. // is always equal to value stored by `prefixCommittee`.
committee atomic.Value committee keysWithVotes
// committeeHash contains script hash of the committee. // 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. // 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 // It is set in state-modifying methods only and read in `PostPersist` thus is not protected
// by any mutex. // by any mutex.
gasPerVoteCache map[string]big.Int 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 ( const (
@ -105,6 +104,41 @@ var (
big100 = big.NewInt(100) 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. // makeValidatorKey creates a key from account script hash.
func makeValidatorKey(key *keys.PublicKey) []byte { func makeValidatorKey(key *keys.PublicKey) []byte {
b := key.Bytes() b := key.Bytes()
@ -116,7 +150,7 @@ func makeValidatorKey(key *keys.PublicKey) []byte {
} }
// newNEO returns NEO native contract. // newNEO returns NEO native contract.
func newNEO() *NEO { func newNEO(cfg config.ProtocolConfiguration) *NEO {
n := &NEO{} n := &NEO{}
defer n.UpdateHash() defer n.UpdateHash()
@ -128,13 +162,11 @@ func newNEO() *NEO {
nep17.balFromBytes = n.balanceFromBytes nep17.balFromBytes = n.balanceFromBytes
n.nep17TokenNative = *nep17 n.nep17TokenNative = *nep17
n.votesChanged.Store(true)
n.nextValidators.Store(keys.PublicKeys(nil)) err := n.initConfigCache(cfg)
n.validators.Store(keys.PublicKeys(nil)) if err != nil {
n.committee.Store(keysWithVotes(nil)) panic(fmt.Errorf("failed to initialize NEO config cache: %w", err))
n.committeeHash.Store(util.Uint160{}) }
n.registerPriceChanged.Store(true)
n.gasPerVoteCache = make(map[string]big.Int)
desc := newDescriptor("unclaimedGas", smartcontract.IntegerType, desc := newDescriptor("unclaimedGas", smartcontract.IntegerType,
manifest.NewParameter("account", smartcontract.Hash160Type), manifest.NewParameter("account", smartcontract.Hash160Type),
@ -198,10 +230,6 @@ func newNEO() *NEO {
// Initialize initializes NEO contract. // Initialize initializes NEO contract.
func (n *NEO) Initialize(ic *interop.Context) error { 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 { if err := n.nep17TokenNative.Initialize(ic); err != nil {
return err return err
} }
@ -211,9 +239,17 @@ func (n *NEO) Initialize(ic *interop.Context) error {
return errors.New("already initialized") 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)] committee0 := n.standbyKeys[:n.cfg.GetCommitteeSize(ic.Block.Index)]
cvs := toKeysWithVotes(committee0) cvs := toKeysWithVotes(committee0)
err = n.updateCache(cvs, ic.Chain) err := n.updateCache(cache, cvs, ic.BlockHeight())
if err != nil { if err != nil {
return err return err
} }
@ -231,80 +267,79 @@ func (n *NEO) Initialize(ic *interop.Context) error {
n.putGASRecord(ic.DAO, index, value) n.putGASRecord(ic.DAO, index, value)
gr := &gasRecord{{Index: index, GASPerBlock: *value}} gr := &gasRecord{{Index: index, GASPerBlock: *value}}
n.gasPerBlock.Store(*gr) cache.gasPerBlock = *gr
n.gasPerBlockChanged.Store(false)
ic.DAO.PutStorageItem(n.ID, []byte{prefixVotersCount}, state.StorageItem{}) ic.DAO.PutStorageItem(n.ID, []byte{prefixVotersCount}, state.StorageItem{})
setIntWithKey(n.ID, ic.DAO, []byte{prefixRegisterPrice}, DefaultRegisterPrice) setIntWithKey(n.ID, ic.DAO, []byte{prefixRegisterPrice}, DefaultRegisterPrice)
n.registerPrice.Store(int64(DefaultRegisterPrice)) cache.registerPrice = int64(DefaultRegisterPrice)
n.registerPriceChanged.Store(false)
return nil return nil
} }
// InitializeCache initializes all NEO cache with the proper values from storage. // InitializeCache initializes all NEO cache with the proper values from storage.
// Cache initialisation should be done apart from Initialize because Initialize is // Cache initialisation should be done apart from Initialize because Initialize is
// called only when deploying native contracts. // called only when deploying native contracts.
func (n *NEO) InitializeCache(bc interop.Ledger, d *dao.Simple) error { func (n *NEO) InitializeCache(blockHeight uint32, d *dao.Simple) error {
err := n.initConfigCache(bc) cache := &NeoCache{
if err != nil { gasPerVoteCache: make(map[string]big.Int),
return nil votesChanged: true,
} }
var committee = keysWithVotes{} var committee = keysWithVotes{}
si := d.GetStorageItem(n.ID, prefixCommittee) si := d.GetStorageItem(n.ID, prefixCommittee)
if err := committee.DecodeBytes(si); err != nil { 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 { if err := n.updateCache(cache, committee, blockHeight); err != nil {
return err return fmt.Errorf("failed to update cache: %w", err)
} }
n.gasPerBlock.Store(n.getSortedGASRecordFromDAO(d)) cache.gasPerBlock = n.getSortedGASRecordFromDAO(d)
n.gasPerBlockChanged.Store(false) cache.registerPrice = getIntWithKey(n.ID, d, []byte{prefixRegisterPrice})
d.SetCache(n.ID, cache)
return nil return nil
} }
func (n *NEO) initConfigCache(bc interop.Ledger) error { func (n *NEO) initConfigCache(cfg config.ProtocolConfiguration) error {
var err error var err error
n.cfg = bc.GetConfig() n.cfg = cfg
n.standbyKeys, err = keys.NewPublicKeysFromStrings(n.cfg.StandbyCommittee) n.standbyKeys, err = keys.NewPublicKeysFromStrings(n.cfg.StandbyCommittee)
return err return err
} }
func (n *NEO) updateCache(cvs keysWithVotes, bc interop.Ledger) error { func (n *NEO) updateCache(cache *NeoCache, cvs keysWithVotes, blockHeight uint32) error {
n.committee.Store(cvs) cache.committee = cvs
var committee = n.GetCommitteeMembers() var committee = getCommitteeMembers(cache)
script, err := smartcontract.CreateMajorityMultiSigRedeemScript(committee.Copy()) script, err := smartcontract.CreateMajorityMultiSigRedeemScript(committee.Copy())
if err != nil { if err != nil {
return err 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) sort.Sort(nextVals)
n.nextValidators.Store(nextVals) cache.nextValidators = nextVals
return nil return nil
} }
func (n *NEO) updateCommittee(ic *interop.Context) error { func (n *NEO) updateCommittee(cache *NeoCache, ic *interop.Context) error {
votesChanged := n.votesChanged.Load().(bool) if !cache.votesChanged {
if !votesChanged {
// We need to put in storage anyway, as it affects dumps // We need to put in storage anyway, as it affects dumps
committee := n.committee.Load().(keysWithVotes) ic.DAO.PutStorageItem(n.ID, prefixCommittee, cache.committee.Bytes())
ic.DAO.PutStorageItem(n.ID, prefixCommittee, committee.Bytes())
return nil return nil
} }
_, cvs, err := n.computeCommitteeMembers(ic.Chain, ic.DAO) _, cvs, err := n.computeCommitteeMembers(ic.BlockHeight(), ic.DAO)
if err != nil { if err != nil {
return err return err
} }
if err := n.updateCache(cvs, ic.Chain); err != nil { if err := n.updateCache(cache, cvs, ic.BlockHeight()); err != nil {
return err return err
} }
n.votesChanged.Store(false) cache.votesChanged = false
ic.DAO.PutStorageItem(n.ID, prefixCommittee, cvs.Bytes()) ic.DAO.PutStorageItem(n.ID, prefixCommittee, cvs.Bytes())
return nil return nil
} }
@ -312,13 +347,14 @@ func (n *NEO) updateCommittee(ic *interop.Context) error {
// OnPersist implements Contract interface. // OnPersist implements Contract interface.
func (n *NEO) OnPersist(ic *interop.Context) error { func (n *NEO) OnPersist(ic *interop.Context) error {
if n.cfg.ShouldUpdateCommitteeAt(ic.Block.Index) { if n.cfg.ShouldUpdateCommitteeAt(ic.Block.Index) {
oldKeys := n.nextValidators.Load().(keys.PublicKeys) cache := ic.DAO.GetRWCache(n.ID).(*NeoCache)
oldCom := n.committee.Load().(keysWithVotes) oldKeys := cache.nextValidators
oldCom := cache.committee
if n.cfg.GetNumOfCNs(ic.Block.Index) != len(oldKeys) || if n.cfg.GetNumOfCNs(ic.Block.Index) != len(oldKeys) ||
n.cfg.GetCommitteeSize(ic.Block.Index) != len(oldCom) { 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 return err
} }
} }
@ -328,7 +364,8 @@ func (n *NEO) OnPersist(ic *interop.Context) error {
// PostPersist implements Contract interface. // PostPersist implements Contract interface.
func (n *NEO) PostPersist(ic *interop.Context) error { func (n *NEO) PostPersist(ic *interop.Context) error {
gas := n.GetGASPerBlock(ic.DAO, ic.Block.Index) 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) committeeSize := n.cfg.GetCommitteeSize(ic.Block.Index)
index := int(ic.Block.Index) % committeeSize index := int(ic.Block.Index) % committeeSize
committeeReward := new(big.Int).Mul(gas, bigCommitteeRewardRatio) 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, big.NewInt(int64(committeeSize+validatorsCount)))
voterReward.Div(voterReward, big100) voterReward.Div(voterReward, big100)
var cs = n.committee.Load().(keysWithVotes) var (
var key = make([]byte, 38) cs = cache.committee
isCacheRW bool
key = make([]byte, 38)
)
for i := range cs { for i := range cs {
if cs[i].Votes.Sign() > 0 { if cs[i].Votes.Sign() > 0 {
var tmp = new(big.Int) var tmp = new(big.Int)
@ -358,7 +398,7 @@ func (n *NEO) PostPersist(ic *interop.Context) error {
key = makeVoterKey([]byte(cs[i].Key), key) key = makeVoterKey([]byte(cs[i].Key), key)
var r *big.Int var r *big.Int
if g, ok := n.gasPerVoteCache[cs[i].Key]; ok { if g, ok := cache.gasPerVoteCache[cs[i].Key]; ok {
r = &g r = &g
} else { } else {
reward := n.getGASPerVote(ic.DAO, key[:34], []uint32{ic.Block.Index + 1}) 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) tmp.Add(tmp, r)
binary.BigEndian.PutUint32(key[34:], ic.Block.Index+1) 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)) 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 return nil
} }
@ -504,28 +538,25 @@ func (n *NEO) getSortedGASRecordFromDAO(d *dao.Simple) gasRecord {
// GetGASPerBlock returns gas generated for block with provided index. // GetGASPerBlock returns gas generated for block with provided index.
func (n *NEO) GetGASPerBlock(d *dao.Simple, index uint32) *big.Int { func (n *NEO) GetGASPerBlock(d *dao.Simple, index uint32) *big.Int {
var gr gasRecord cache := d.GetROCache(n.ID).(*NeoCache)
if n.gasPerBlockChanged.Load().(bool) { gr := cache.gasPerBlock
gr = n.getSortedGASRecordFromDAO(d)
} else {
gr = n.gasPerBlock.Load().(gasRecord)
}
for i := len(gr) - 1; i >= 0; i-- { for i := len(gr) - 1; i >= 0; i-- {
if gr[i].Index <= index { if gr[i].Index <= index {
g := gr[i].GASPerBlock g := gr[i].GASPerBlock
return &g return &g
} }
} }
panic("contract not initialized") panic("NEO cache not initialized")
} }
// GetCommitteeAddress returns address of the committee. // GetCommitteeAddress returns address of the committee.
func (n *NEO) GetCommitteeAddress() util.Uint160 { func (n *NEO) GetCommitteeAddress(d *dao.Simple) util.Uint160 {
return n.committeeHash.Load().(util.Uint160) cache := d.GetROCache(n.ID).(*NeoCache)
return cache.committeeHash
} }
func (n *NEO) checkCommittee(ic *interop.Context) bool { 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 { if err != nil {
panic(err) panic(err)
} }
@ -549,8 +580,12 @@ func (n *NEO) SetGASPerBlock(ic *interop.Context, index uint32, gas *big.Int) er
if !n.checkCommittee(ic) { if !n.checkCommittee(ic) {
return errors.New("invalid committee signature") return errors.New("invalid committee signature")
} }
n.gasPerBlockChanged.Store(true)
n.putGASRecord(ic.DAO, index, gas) 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 return nil
} }
@ -559,10 +594,8 @@ func (n *NEO) getRegisterPrice(ic *interop.Context, _ []stackitem.Item) stackite
} }
func (n *NEO) getRegisterPriceInternal(d *dao.Simple) int64 { func (n *NEO) getRegisterPriceInternal(d *dao.Simple) int64 {
if !n.registerPriceChanged.Load().(bool) { cache := d.GetROCache(n.ID).(*NeoCache)
return n.registerPrice.Load().(int64) return cache.registerPrice
}
return getIntWithKey(n.ID, d, []byte{prefixRegisterPrice})
} }
func (n *NEO) setRegisterPrice(ic *interop.Context, args []stackitem.Item) stackitem.Item { 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()) 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{} 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 { if c.Registered || c.Votes.Sign() != 0 {
return false, nil return false
} }
d.DeleteStorageItem(n.ID, makeValidatorKey(pub)) 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. d.DeleteStorageItem(n.ID, append(voterKey, k...)) // d.Seek cuts prefix, thus need to append it again.
return true return true
}) })
delete(n.gasPerVoteCache, string(voterKey)) delete(cache.gasPerVoteCache, string(voterKey))
return true, nil return true
} }
func makeVoterKey(pub []byte, prealloc ...[]byte) []byte { 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 { } else if value.Sign() < 0 {
return nil, errors.New("negative value") return nil, errors.New("negative value")
} }
var gr gasRecord cache := d.GetROCache(n.ID).(*NeoCache)
if !n.gasPerBlockChanged.Load().(bool) { gr := cache.gasPerBlock
gr = n.gasPerBlock.Load().(gasRecord)
} else {
gr = n.getSortedGASRecordFromDAO(d)
}
var sum, tmp big.Int var sum, tmp big.Int
for i := len(gr) - 1; i >= 0; i-- { for i := len(gr) - 1; i >= 0; i-- {
if gr[i].Index >= end { if gr[i].Index >= end {
@ -719,12 +749,13 @@ func (n *NEO) UnregisterCandidateInternal(ic *interop.Context, pub *keys.PublicK
if si == nil { if si == nil {
return 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 := new(candidate).FromBytes(si)
c.Registered = false c.Registered = false
ok, err := n.dropCandidateIfZero(ic.DAO, pub, c) ok := n.dropCandidateIfZero(ic.DAO, cache, pub, c)
if ok { if ok {
return err return nil
} }
return putConvertibleToDAO(n.ID, ic.DAO, key, c) 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). // 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). // 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 { 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 { if acc.VoteTo != nil {
key := makeValidatorKey(acc.VoteTo) key := makeValidatorKey(acc.VoteTo)
si := d.GetStorageItem(n.ID, key) 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 := new(candidate).FromBytes(si)
cd.Votes.Add(&cd.Votes, value) cd.Votes.Add(&cd.Votes, value)
if !isNewVote { if !isNewVote {
ok, err := n.dropCandidateIfZero(d, acc.VoteTo, cd) ok := n.dropCandidateIfZero(d, cache, acc.VoteTo, cd)
if ok { if ok {
return err return nil
} }
} }
n.validators.Store(keys.PublicKeys(nil)) cache.validators = nil
return putConvertibleToDAO(n.ID, d, key, cd) return putConvertibleToDAO(n.ID, d, key, cd)
} }
return nil 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 { d.Seek(n.ID, storage.SeekRange{Prefix: []byte{prefixCandidate}}, func(k, v []byte) bool {
c := new(candidate).FromBytes(v) c := new(candidate).FromBytes(v)
emit.CheckSig(buf.BinWriter, k) 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}) arr = append(arr, keyWithVotes{Key: string(k), Votes: &c.Votes})
} }
buf.Reset() 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. // ComputeNextBlockValidators returns an actual list of current validators.
func (n *NEO) ComputeNextBlockValidators(bc interop.Ledger, d *dao.Simple) (keys.PublicKeys, error) { func (n *NEO) ComputeNextBlockValidators(blockHeight uint32, d *dao.Simple) (keys.PublicKeys, error) {
numOfCNs := n.cfg.GetNumOfCNs(bc.BlockHeight() + 1) numOfCNs := n.cfg.GetNumOfCNs(blockHeight + 1)
if vals := n.validators.Load().(keys.PublicKeys); vals != nil && numOfCNs == len(vals) { // 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 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 { if err != nil {
return nil, err return nil, err
} }
result = result[:numOfCNs] result = result[:numOfCNs]
sort.Sort(result) sort.Sort(result)
n.validators.Store(result) cache.validators = result
return result, nil return result, nil
} }
func (n *NEO) getCommittee(ic *interop.Context, _ []stackitem.Item) stackitem.Item { func (n *NEO) getCommittee(ic *interop.Context, _ []stackitem.Item) stackitem.Item {
pubs := n.GetCommitteeMembers() pubs := n.GetCommitteeMembers(ic.DAO)
sort.Sort(pubs) sort.Sort(pubs)
return pubsToArray(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. // GetCommitteeMembers returns public keys of nodes in committee using cached value.
func (n *NEO) GetCommitteeMembers() keys.PublicKeys { func (n *NEO) GetCommitteeMembers(d *dao.Simple) keys.PublicKeys {
var cvs = n.committee.Load().(keysWithVotes) 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 committee = make(keys.PublicKeys, len(cvs))
var err error var err error
for i := range committee { for i := range committee {
@ -965,7 +1006,7 @@ func toKeysWithVotes(pubs keys.PublicKeys) keysWithVotes {
} }
// computeCommitteeMembers returns public keys of nodes in committee. // 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} key := []byte{prefixVotersCount}
si := d.GetStorageItem(n.ID, key) si := d.GetStorageItem(n.ID, key)
if si == nil { if si == nil {
@ -977,7 +1018,7 @@ func (n *NEO) computeCommitteeMembers(bc interop.Ledger, d *dao.Simple) (keys.Pu
_, totalSupply := n.getTotalSupply(d) _, totalSupply := n.getTotalSupply(d)
voterTurnout := votersCount.Div(votersCount, totalSupply) 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. // Can be sorted and/or returned to outside users, thus needs to be copied.
sbVals := keys.PublicKeys(n.standbyKeys[:count]).Copy() sbVals := keys.PublicKeys(n.standbyKeys[:count]).Copy()
cs, err := n.getCandidates(d, false) 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 { func (n *NEO) getNextBlockValidators(ic *interop.Context, _ []stackitem.Item) stackitem.Item {
result := n.GetNextBlockValidatorsInternal() result := n.GetNextBlockValidatorsInternal(ic.DAO)
return pubsToArray(result) return pubsToArray(result)
} }
// GetNextBlockValidatorsInternal returns next block validators. // GetNextBlockValidatorsInternal returns next block validators.
func (n *NEO) GetNextBlockValidatorsInternal() keys.PublicKeys { func (n *NEO) GetNextBlockValidatorsInternal(d *dao.Simple) keys.PublicKeys {
return n.nextValidators.Load().(keys.PublicKeys).Copy() cache := d.GetROCache(n.ID).(*NeoCache)
return cache.nextValidators.Copy()
} }
// BalanceOf returns native NEO token balance for the acc. // BalanceOf returns native NEO token balance for the acc.

View file

@ -8,9 +8,12 @@ import (
"github.com/nspcc-dev/neo-go/pkg/core/native/noderoles" "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/core/state"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys" "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"
"github.com/nspcc-dev/neo-go/pkg/neotest/chain" "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/nspcc-dev/neo-go/pkg/vm/stackitem"
"github.com/stretchr/testify/require" "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) { func setNodesByRole(t *testing.T, designateInvoker *neotest.ContractInvoker, ok bool, r noderoles.Role, nodes keys.PublicKeys) {
pubs := make([]interface{}, len(nodes)) pubs := make([]interface{}, len(nodes))
for i := range nodes { for i := range nodes {

View file

@ -5,8 +5,15 @@ import (
"github.com/nspcc-dev/neo-go/pkg/core/native/nativenames" "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/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/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"
"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" "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) 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)
}

View file

@ -20,6 +20,8 @@ import (
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag" "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/manifest"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/nef" "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/emit"
"github.com/nspcc-dev/neo-go/pkg/vm/opcode" "github.com/nspcc-dev/neo-go/pkg/vm/opcode"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem" "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) 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) { func TestManagement_ContractDeploy(t *testing.T) {
c := newManagementClient(t) c := newManagementClient(t)
managementInvoker := c.WithSigners(c.Committee) managementInvoker := c.WithSigners(c.Committee)

View file

@ -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) 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) { func TestNEO_RegisterPrice(t *testing.T) {
testGetSet(t, newNeoCommitteeClient(t, 100_0000_0000), "RegisterPrice", native.DefaultRegisterPrice, 1, math.MaxInt64) 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) { func TestNEO_Vote(t *testing.T) {
neoCommitteeInvoker := newNeoCommitteeClient(t, 100_0000_0000) neoCommitteeInvoker := newNeoCommitteeClient(t, 100_0000_0000)
neoValidatorsInvoker := neoCommitteeInvoker.WithSigners(neoCommitteeInvoker.Validator) neoValidatorsInvoker := neoCommitteeInvoker.WithSigners(neoCommitteeInvoker.Validator)

View file

@ -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)) 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) { func TestNotary_NotaryServiceFeePerKey(t *testing.T) {
c := newNotaryClient(t) c := newNotaryClient(t)
testGetSet(t, c, "NotaryServiceFeePerKey", 1000_0000, 0, 0) 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) { func TestNotary_Pipeline(t *testing.T) {
notaryCommitteeInvoker := newNotaryClient(t) notaryCommitteeInvoker := newNotaryClient(t)
e := notaryCommitteeInvoker.Executor e := notaryCommitteeInvoker.Executor

View file

@ -28,10 +28,14 @@ func newOracleClient(t *testing.T) *neotest.ContractInvoker {
return newNativeClient(t, nativenames.Oracle) 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) 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, func putOracleRequest(t *testing.T, oracleInvoker *neotest.ContractInvoker,
url string, filter *string, cb string, userData []byte, gas int64, errStr ...string) { url string, filter *string, cb string, userData []byte, gas int64, errStr ...string) {
var filtItem interface{} var filtItem interface{}

View file

@ -19,14 +19,26 @@ func TestPolicy_FeePerByte(t *testing.T) {
testGetSet(t, newPolicyClient(t), "FeePerByte", 1000, 0, 100_000_000) 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) { func TestPolicy_ExecFeeFactor(t *testing.T) {
testGetSet(t, newPolicyClient(t), "ExecFeeFactor", interop.DefaultBaseExecFee, 1, 1000) 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) { func TestPolicy_StoragePrice(t *testing.T) {
testGetSet(t, newPolicyClient(t), "StoragePrice", native.DefaultStoragePrice, 1, 10000000) 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) { func TestPolicy_BlockedAccounts(t *testing.T) {
c := newPolicyClient(t) c := newPolicyClient(t)
e := c.Executor e := c.Executor

View file

@ -5,7 +5,6 @@ import (
"fmt" "fmt"
"math" "math"
"math/big" "math/big"
"sync"
"github.com/nspcc-dev/neo-go/pkg/core/dao" "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"
@ -32,12 +31,9 @@ type Notary struct {
GAS *GAS GAS *GAS
NEO *NEO NEO *NEO
Desig *Designate Desig *Designate
}
lock sync.RWMutex type NotaryCache struct {
// 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
maxNotValidBeforeDelta uint32 maxNotValidBeforeDelta uint32
notaryServiceFeePerKey int64 notaryServiceFeePerKey int64
} }
@ -56,6 +52,22 @@ var (
notaryServiceFeeKey = []byte{5} 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. // newNotary returns Notary native contract.
func newNotary() *Notary { func newNotary() *Notary {
n := &Notary{ContractMD: *interop.NewContractMD(nativenames.Notary, notaryContractID)} 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 { func (n *Notary) Initialize(ic *interop.Context) error {
setIntWithKey(n.ID, ic.DAO, maxNotValidBeforeDeltaKey, defaultMaxNotValidBeforeDelta) setIntWithKey(n.ID, ic.DAO, maxNotValidBeforeDeltaKey, defaultMaxNotValidBeforeDelta)
setIntWithKey(n.ID, ic.DAO, notaryServiceFeeKey, defaultNotaryServiceFeePerKey) setIntWithKey(n.ID, ic.DAO, notaryServiceFeeKey, defaultNotaryServiceFeePerKey)
n.isValid = true
n.maxNotValidBeforeDelta = defaultMaxNotValidBeforeDelta cache := &NotaryCache{
n.notaryServiceFeePerKey = defaultNotaryServiceFeePerKey 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 return nil
} }
@ -176,15 +201,6 @@ func (n *Notary) OnPersist(ic *interop.Context) error {
// PostPersist implements Contract interface. // PostPersist implements Contract interface.
func (n *Notary) PostPersist(ic *interop.Context) error { 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 return nil
} }
@ -207,7 +223,7 @@ func (n *Notary) onPayment(ic *interop.Context, args []stackitem.Item) stackitem
} }
allowedChangeTill := ic.Tx.Sender() == to allowedChangeTill := ic.Tx.Sender() == to
currentHeight := ic.Chain.BlockHeight() currentHeight := ic.BlockHeight()
deposit := n.GetDepositFor(ic.DAO, to) deposit := n.GetDepositFor(ic.DAO, to)
till := toUint32(additionalParams[1]) till := toUint32(additionalParams[1])
if till < currentHeight { if till < currentHeight {
@ -250,7 +266,7 @@ func (n *Notary) lockDepositUntil(ic *interop.Context, args []stackitem.Item) st
return stackitem.NewBool(false) return stackitem.NewBool(false)
} }
till := toUint32(args[1]) till := toUint32(args[1])
if till < ic.Chain.BlockHeight() { if till < ic.BlockHeight() {
return stackitem.NewBool(false) return stackitem.NewBool(false)
} }
deposit := n.GetDepositFor(ic.DAO, addr) deposit := n.GetDepositFor(ic.DAO, addr)
@ -286,7 +302,7 @@ func (n *Notary) withdraw(ic *interop.Context, args []stackitem.Item) stackitem.
if deposit == nil { if deposit == nil {
return stackitem.NewBool(false) return stackitem.NewBool(false)
} }
if ic.Chain.BlockHeight() < deposit.Till { if ic.BlockHeight() < deposit.Till {
return stackitem.NewBool(false) return stackitem.NewBool(false)
} }
cs, err := ic.GetContract(n.GAS.Hash) 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. // GetMaxNotValidBeforeDelta is an internal representation of Notary getMaxNotValidBeforeDelta method.
func (n *Notary) GetMaxNotValidBeforeDelta(dao *dao.Simple) uint32 { func (n *Notary) GetMaxNotValidBeforeDelta(dao *dao.Simple) uint32 {
n.lock.RLock() cache := dao.GetROCache(n.ID).(*NotaryCache)
defer n.lock.RUnlock() return cache.maxNotValidBeforeDelta
if n.isValid {
return n.maxNotValidBeforeDelta
}
return uint32(getIntWithKey(n.ID, dao, maxNotValidBeforeDeltaKey))
} }
// setMaxNotValidBeforeDelta is Notary contract method and sets the maximum NotValidBefore delta. // 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]) value := toUint32(args[0])
cfg := ic.Chain.GetConfig() cfg := ic.Chain.GetConfig()
maxInc := cfg.MaxValidUntilBlockIncrement maxInc := cfg.MaxValidUntilBlockIncrement
if value > maxInc/2 || value < uint32(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.Chain.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) { if !n.NEO.checkCommittee(ic) {
panic("invalid committee signature") panic("invalid committee signature")
} }
n.lock.Lock()
defer n.lock.Unlock()
setIntWithKey(n.ID, ic.DAO, maxNotValidBeforeDeltaKey, int64(value)) setIntWithKey(n.ID, ic.DAO, maxNotValidBeforeDeltaKey, int64(value))
n.isValid = false cache := ic.DAO.GetRWCache(n.ID).(*NotaryCache)
cache.maxNotValidBeforeDelta = value
return stackitem.Null{} 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. // GetNotaryServiceFeePerKey is an internal representation of Notary getNotaryServiceFeePerKey method.
func (n *Notary) GetNotaryServiceFeePerKey(dao *dao.Simple) int64 { func (n *Notary) GetNotaryServiceFeePerKey(dao *dao.Simple) int64 {
n.lock.RLock() cache := dao.GetROCache(n.ID).(*NotaryCache)
defer n.lock.RUnlock() return cache.notaryServiceFeePerKey
if n.isValid {
return n.notaryServiceFeePerKey
}
return getIntWithKey(n.ID, dao, notaryServiceFeeKey)
} }
// setNotaryServiceFeePerKey is Notary contract method and sets a reward per notary request key for notary nodes. // 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) { if !n.NEO.checkCommittee(ic) {
panic("invalid committee signature") panic("invalid committee signature")
} }
n.lock.Lock()
defer n.lock.Unlock()
setIntWithKey(n.ID, ic.DAO, notaryServiceFeeKey, int64(value)) setIntWithKey(n.ID, ic.DAO, notaryServiceFeeKey, int64(value))
n.isValid = false cache := ic.DAO.GetRWCache(n.ID).(*NotaryCache)
cache.notaryServiceFeePerKey = value
return stackitem.Null{} return stackitem.Null{}
} }

View file

@ -40,15 +40,16 @@ type Oracle struct {
Desig *Designate Desig *Designate
oracleScript []byte oracleScript []byte
requestPrice atomic.Value
requestPriceChanged atomic.Value
// Module is an oracle module capable of talking with the external world. // Module is an oracle module capable of talking with the external world.
Module atomic.Value Module atomic.Value
// newRequests contains new requests created during current block. // newRequests contains new requests created during current block.
newRequests map[uint64]*state.OracleRequest newRequests map[uint64]*state.OracleRequest
} }
type OracleCache struct {
requestPrice int64
}
const ( const (
oracleContractID = -9 oracleContractID = -9
maxURLLength = 256 maxURLLength = 256
@ -82,6 +83,22 @@ var (
ErrResponseNotFound = errors.New("oracle response not found") 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 { func newOracle() *Oracle {
o := &Oracle{ContractMD: *interop.NewContractMD(nativenames.Oracle, oracleContractID)} o := &Oracle{ContractMD: *interop.NewContractMD(nativenames.Oracle, oracleContractID)}
defer o.UpdateHash() defer o.UpdateHash()
@ -121,8 +138,6 @@ func newOracle() *Oracle {
md = newMethodAndPrice(o.setPrice, 1<<15, callflag.States) md = newMethodAndPrice(o.setPrice, 1<<15, callflag.States)
o.AddMethod(md, desc) o.AddMethod(md, desc)
o.requestPriceChanged.Store(true)
return o return o
} }
@ -143,10 +158,6 @@ func (o *Oracle) OnPersist(ic *interop.Context) error {
// PostPersist represents `postPersist` method. // PostPersist represents `postPersist` method.
func (o *Oracle) PostPersist(ic *interop.Context) error { func (o *Oracle) PostPersist(ic *interop.Context) error {
p := o.getPriceInternal(ic.DAO) p := o.getPriceInternal(ic.DAO)
if o.requestPriceChanged.Load().(bool) {
o.requestPrice.Store(p)
o.requestPriceChanged.Store(false)
}
var nodes keys.PublicKeys var nodes keys.PublicKeys
var reward []big.Int var reward []big.Int
@ -220,11 +231,20 @@ func (o *Oracle) Metadata() *interop.ContractMD {
func (o *Oracle) Initialize(ic *interop.Context) error { func (o *Oracle) Initialize(ic *interop.Context) error {
setIntWithKey(o.ID, ic.DAO, prefixRequestID, 0) setIntWithKey(o.ID, ic.DAO, prefixRequestID, 0)
setIntWithKey(o.ID, ic.DAO, prefixRequestPrice, DefaultOracleRequestPrice) 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 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 { func getResponse(tx *transaction.Transaction) *transaction.OracleResponse {
for i := range tx.Attributes { for i := range tx.Attributes {
if tx.Attributes[i].Type == transaction.OracleResponseT { 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 { func (o *Oracle) getPriceInternal(d *dao.Simple) int64 {
if !o.requestPriceChanged.Load().(bool) { cache := d.GetROCache(o.ID).(*OracleCache)
return o.requestPrice.Load().(int64) return cache.requestPrice
}
return getIntWithKey(o.ID, d, prefixRequestPrice)
} }
func (o *Oracle) setPrice(ic *interop.Context, args []stackitem.Item) stackitem.Item { 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") panic("invalid committee signature")
} }
setIntWithKey(o.ID, ic.DAO, prefixRequestPrice, price.Int64()) 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{} return stackitem.Null{}
} }

View file

@ -4,7 +4,6 @@ import (
"fmt" "fmt"
"math/big" "math/big"
"sort" "sort"
"sync"
"github.com/nspcc-dev/neo-go/pkg/core/dao" "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"
@ -52,12 +51,10 @@ var (
// Policy represents Policy native contract. // Policy represents Policy native contract.
type Policy struct { type Policy struct {
interop.ContractMD interop.ContractMD
NEO *NEO 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 type PolicyCache struct {
// blockchain DAO persisting. If true, we can safely use cached values.
isValid bool
execFeeFactor uint32 execFeeFactor uint32
feePerByte int64 feePerByte int64
maxVerificationGas int64 maxVerificationGas int64
@ -65,7 +62,23 @@ type Policy struct {
blockedAccounts []util.Uint160 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. // newPolicy returns Policy native contract.
func newPolicy() *Policy { 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, execFeeFactorKey, defaultExecFeeFactor)
setIntWithKey(p.ID, ic.DAO, storagePriceKey, DefaultStoragePrice) setIntWithKey(p.ID, ic.DAO, storagePriceKey, DefaultStoragePrice)
p.isValid = true cache := &PolicyCache{
p.execFeeFactor = defaultExecFeeFactor execFeeFactor: defaultExecFeeFactor,
p.feePerByte = defaultFeePerByte feePerByte: defaultFeePerByte,
p.maxVerificationGas = defaultMaxVerificationGas maxVerificationGas: defaultMaxVerificationGas,
p.storagePrice = DefaultStoragePrice storagePrice: DefaultStoragePrice,
p.blockedAccounts = make([]util.Uint160, 0) blockedAccounts: make([]util.Uint160, 0),
}
ic.DAO.SetCache(p.ID, cache)
return nil 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. // OnPersist implements Contract interface.
func (p *Policy) OnPersist(ic *interop.Context) error { func (p *Policy) OnPersist(ic *interop.Context) error {
return nil return nil
@ -145,32 +193,7 @@ func (p *Policy) OnPersist(ic *interop.Context) error {
// PostPersist implements Contract interface. // PostPersist implements Contract interface.
func (p *Policy) PostPersist(ic *interop.Context) error { func (p *Policy) PostPersist(ic *interop.Context) error {
p.lock.Lock() return nil
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
} }
// getFeePerByte is Policy contract method and returns required transaction's fee // 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. // GetFeePerByteInternal returns required transaction's fee per byte.
func (p *Policy) GetFeePerByteInternal(dao *dao.Simple) int64 { func (p *Policy) GetFeePerByteInternal(dao *dao.Simple) int64 {
p.lock.RLock() cache := dao.GetROCache(p.ID).(*PolicyCache)
defer p.lock.RUnlock() return cache.feePerByte
if p.isValid {
return p.feePerByte
}
return getIntWithKey(p.ID, dao, feePerByteKey)
} }
// GetMaxVerificationGas returns maximum gas allowed to be burned during verificaion. // GetMaxVerificationGas returns maximum gas allowed to be burned during verificaion.
func (p *Policy) GetMaxVerificationGas(_ *dao.Simple) int64 { func (p *Policy) GetMaxVerificationGas(dao *dao.Simple) int64 {
if p.isValid { cache := dao.GetROCache(p.ID).(*PolicyCache)
return p.maxVerificationGas return cache.maxVerificationGas
}
return defaultMaxVerificationGas
} }
func (p *Policy) getExecFeeFactor(ic *interop.Context, _ []stackitem.Item) stackitem.Item { 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. // GetExecFeeFactorInternal returns current execution fee factor.
func (p *Policy) GetExecFeeFactorInternal(d *dao.Simple) int64 { func (p *Policy) GetExecFeeFactorInternal(d *dao.Simple) int64 {
p.lock.RLock() cache := d.GetROCache(p.ID).(*PolicyCache)
defer p.lock.RUnlock() return int64(cache.execFeeFactor)
if p.isValid {
return int64(p.execFeeFactor)
}
return getIntWithKey(p.ID, d, execFeeFactorKey)
} }
func (p *Policy) setExecFeeFactor(ic *interop.Context, args []stackitem.Item) stackitem.Item { 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) { if !p.NEO.checkCommittee(ic) {
panic("invalid committee signature") panic("invalid committee signature")
} }
p.lock.Lock()
defer p.lock.Unlock()
setIntWithKey(p.ID, ic.DAO, execFeeFactorKey, int64(value)) setIntWithKey(p.ID, ic.DAO, execFeeFactorKey, int64(value))
p.isValid = false cache := ic.DAO.GetRWCache(p.ID).(*PolicyCache)
cache.execFeeFactor = value
return stackitem.Null{} return stackitem.Null{}
} }
// isBlocked is Policy contract method and checks whether provided account is blocked. // isBlocked is Policy contract method and checks whether provided account is blocked.
func (p *Policy) isBlocked(ic *interop.Context, args []stackitem.Item) stackitem.Item { func (p *Policy) isBlocked(ic *interop.Context, args []stackitem.Item) stackitem.Item {
hash := toUint160(args[0]) 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. // IsBlocked checks whether provided account is blocked.
func (p *Policy) IsBlockedInternal(dao *dao.Simple, hash util.Uint160) bool { func (p *Policy) IsBlocked(dao *dao.Simple, hash util.Uint160) bool {
p.lock.RLock() _, isBlocked := p.isBlockedInternal(dao, hash)
defer p.lock.RUnlock() return isBlocked
if p.isValid { }
length := len(p.blockedAccounts)
i := sort.Search(length, func(i int) bool { // isBlockedInternal checks whether provided account is blocked. It returns position
return !p.blockedAccounts[i].Less(hash) // of the blocked account in the blocked accounts list (or the position it should be
}) // put at).
if length != 0 && i != length && p.blockedAccounts[i].Equals(hash) { func (p *Policy) isBlockedInternal(dao *dao.Simple, hash util.Uint160) (int, bool) {
return true cache := dao.GetROCache(p.ID).(*PolicyCache)
} length := len(cache.blockedAccounts)
return false 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 i, false
return dao.GetStorageItem(p.ID, key) != nil
} }
func (p *Policy) getStoragePrice(ic *interop.Context, _ []stackitem.Item) stackitem.Item { 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. // GetStoragePriceInternal returns current execution fee factor.
func (p *Policy) GetStoragePriceInternal(d *dao.Simple) int64 { func (p *Policy) GetStoragePriceInternal(d *dao.Simple) int64 {
p.lock.RLock() cache := d.GetROCache(p.ID).(*PolicyCache)
defer p.lock.RUnlock() return int64(cache.storagePrice)
if p.isValid {
return int64(p.storagePrice)
}
return getIntWithKey(p.ID, d, storagePriceKey)
} }
func (p *Policy) setStoragePrice(ic *interop.Context, args []stackitem.Item) stackitem.Item { 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) { if !p.NEO.checkCommittee(ic) {
panic("invalid committee signature") panic("invalid committee signature")
} }
p.lock.Lock()
defer p.lock.Unlock()
setIntWithKey(p.ID, ic.DAO, storagePriceKey, int64(value)) setIntWithKey(p.ID, ic.DAO, storagePriceKey, int64(value))
p.isValid = false cache := ic.DAO.GetRWCache(p.ID).(*PolicyCache)
cache.storagePrice = value
return stackitem.Null{} return stackitem.Null{}
} }
@ -288,10 +299,9 @@ func (p *Policy) setFeePerByte(ic *interop.Context, args []stackitem.Item) stack
if !p.NEO.checkCommittee(ic) { if !p.NEO.checkCommittee(ic) {
panic("invalid committee signature") panic("invalid committee signature")
} }
p.lock.Lock()
defer p.lock.Unlock()
setIntWithKey(p.ID, ic.DAO, feePerByteKey, value) setIntWithKey(p.ID, ic.DAO, feePerByteKey, value)
p.isValid = false cache := ic.DAO.GetRWCache(p.ID).(*PolicyCache)
cache.feePerByte = value
return stackitem.Null{} return stackitem.Null{}
} }
@ -307,14 +317,19 @@ func (p *Policy) blockAccount(ic *interop.Context, args []stackitem.Item) stacki
panic("cannot block native contract") panic("cannot block native contract")
} }
} }
if p.IsBlockedInternal(ic.DAO, hash) { i, blocked := p.isBlockedInternal(ic.DAO, hash)
if blocked {
return stackitem.NewBool(false) return stackitem.NewBool(false)
} }
key := append([]byte{blockedAccountPrefix}, hash.BytesBE()...) key := append([]byte{blockedAccountPrefix}, hash.BytesBE()...)
p.lock.Lock()
defer p.lock.Unlock()
ic.DAO.PutStorageItem(p.ID, key, state.StorageItem{}) 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) return stackitem.NewBool(true)
} }
@ -325,14 +340,14 @@ func (p *Policy) unblockAccount(ic *interop.Context, args []stackitem.Item) stac
panic("invalid committee signature") panic("invalid committee signature")
} }
hash := toUint160(args[0]) hash := toUint160(args[0])
if !p.IsBlockedInternal(ic.DAO, hash) { i, blocked := p.isBlockedInternal(ic.DAO, hash)
if !blocked {
return stackitem.NewBool(false) return stackitem.NewBool(false)
} }
key := append([]byte{blockedAccountPrefix}, hash.BytesBE()...) key := append([]byte{blockedAccountPrefix}, hash.BytesBE()...)
p.lock.Lock()
defer p.lock.Unlock()
ic.DAO.DeleteStorageItem(p.ID, key) 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) return stackitem.NewBool(true)
} }
@ -341,7 +356,7 @@ func (p *Policy) unblockAccount(ic *interop.Context, args []stackitem.Item) stac
// fee limit. // fee limit.
func (p *Policy) CheckPolicy(d *dao.Simple, tx *transaction.Transaction) error { func (p *Policy) CheckPolicy(d *dao.Simple, tx *transaction.Transaction) error {
for _, signer := range tx.Signers { 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()) return fmt.Errorf("account %s is blocked", signer.Account.StringLE())
} }
} }

View file

@ -1,6 +1,7 @@
package stateroot package stateroot
import ( import (
"bytes"
"encoding/binary" "encoding/binary"
"errors" "errors"
"fmt" "fmt"
@ -101,6 +102,34 @@ func (s *Module) GetStateRoot(height uint32) (*state.MPTRoot, error) {
return s.getStateRoot(makeStateRootKey(height)) 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. // CurrentLocalStateRoot returns hash of the local state root.
func (s *Module) CurrentLocalStateRoot() util.Uint256 { func (s *Module) CurrentLocalStateRoot() util.Uint256 {
return s.currentLocal.Load().(util.Uint256) return s.currentLocal.Load().(util.Uint256)

View file

@ -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, len(pubs) > int(valIndex))
require.True(t, pubs[valIndex].VerifyHashable(vote.Signature, uint32(netmode.UnitTestNet), r)) 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)
}
}

View file

@ -365,7 +365,6 @@ func (s *MemCachedStore) persist(isSync bool) (int, error) {
if !isSync { if !isSync {
s.mut.Unlock() s.mut.Unlock()
} }
err = tempstore.ps.PutChangeSet(tempstore.mem, tempstore.stor) err = tempstore.ps.PutChangeSet(tempstore.mem, tempstore.stor)
if !isSync { if !isSync {

View file

@ -588,6 +588,33 @@ func (c *Client) InvokeScript(script []byte, signers []transaction.Signer) (*res
return c.invokeSomething("invokescript", p, signers) 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 // InvokeFunction returns the results after calling the smart contract scripthash
// with the given operation and parameters. // with the given operation and parameters.
// NOTE: this is test invoke and will not affect the blockchain. // 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) 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 // InvokeContractVerify returns the results after calling `verify` method of the smart contract
// with the given parameters under verification trigger type. // with the given parameters under verification trigger type.
// NOTE: this is test invoke and will not affect the blockchain. // 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...) 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. // 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) { func (c *Client) invokeSomething(method string, p request.RawParams, signers []transaction.Signer, witnesses ...transaction.Witness) (*result.Invoke, error) {
var resp = new(result.Invoke) var resp = new(result.Invoke)

View file

@ -4,9 +4,11 @@ import (
"context" "context"
"encoding/base64" "encoding/base64"
"encoding/hex" "encoding/hex"
"strings"
"testing" "testing"
"github.com/nspcc-dev/neo-go/internal/testchain" "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/fee"
"github.com/nspcc-dev/neo-go/pkg/core/native/nativenames" "github.com/nspcc-dev/neo-go/pkg/core/native/nativenames"
"github.com/nspcc-dev/neo-go/pkg/core/transaction" "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)) 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) { 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)}}) 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) require.NoError(t, err)

View file

@ -24,6 +24,7 @@ import (
"github.com/nspcc-dev/neo-go/pkg/core/block" "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/blockchainer"
"github.com/nspcc-dev/neo-go/pkg/core/fee" "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/interop/iterator"
"github.com/nspcc-dev/neo-go/pkg/core/mempoolevent" "github.com/nspcc-dev/neo-go/pkg/core/mempoolevent"
"github.com/nspcc-dev/neo-go/pkg/core/mpt" "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){ var rpcHandlers = map[string]func(*Server, request.Params) (interface{}, *response.Error){
"calculatenetworkfee": (*Server).calculateNetworkFee, "calculatenetworkfee": (*Server).calculateNetworkFee,
"findstates": (*Server).findStates, "findstates": (*Server).findStates,
"getapplicationlog": (*Server).getApplicationLog, "getapplicationlog": (*Server).getApplicationLog,
"getbestblockhash": (*Server).getBestBlockHash, "getbestblockhash": (*Server).getBestBlockHash,
"getblock": (*Server).getBlock, "getblock": (*Server).getBlock,
"getblockcount": (*Server).getBlockCount, "getblockcount": (*Server).getBlockCount,
"getblockhash": (*Server).getBlockHash, "getblockhash": (*Server).getBlockHash,
"getblockheader": (*Server).getBlockHeader, "getblockheader": (*Server).getBlockHeader,
"getblockheadercount": (*Server).getBlockHeaderCount, "getblockheadercount": (*Server).getBlockHeaderCount,
"getblocksysfee": (*Server).getBlockSysFee, "getblocksysfee": (*Server).getBlockSysFee,
"getcommittee": (*Server).getCommittee, "getcommittee": (*Server).getCommittee,
"getconnectioncount": (*Server).getConnectionCount, "getconnectioncount": (*Server).getConnectionCount,
"getcontractstate": (*Server).getContractState, "getcontractstate": (*Server).getContractState,
"getnativecontracts": (*Server).getNativeContracts, "getnativecontracts": (*Server).getNativeContracts,
"getnep11balances": (*Server).getNEP11Balances, "getnep11balances": (*Server).getNEP11Balances,
"getnep11properties": (*Server).getNEP11Properties, "getnep11properties": (*Server).getNEP11Properties,
"getnep11transfers": (*Server).getNEP11Transfers, "getnep11transfers": (*Server).getNEP11Transfers,
"getnep17balances": (*Server).getNEP17Balances, "getnep17balances": (*Server).getNEP17Balances,
"getnep17transfers": (*Server).getNEP17Transfers, "getnep17transfers": (*Server).getNEP17Transfers,
"getpeers": (*Server).getPeers, "getpeers": (*Server).getPeers,
"getproof": (*Server).getProof, "getproof": (*Server).getProof,
"getrawmempool": (*Server).getRawMempool, "getrawmempool": (*Server).getRawMempool,
"getrawtransaction": (*Server).getrawtransaction, "getrawtransaction": (*Server).getrawtransaction,
"getstate": (*Server).getState, "getstate": (*Server).getState,
"getstateheight": (*Server).getStateHeight, "getstateheight": (*Server).getStateHeight,
"getstateroot": (*Server).getStateRoot, "getstateroot": (*Server).getStateRoot,
"getstorage": (*Server).getStorage, "getstorage": (*Server).getStorage,
"gettransactionheight": (*Server).getTransactionHeight, "gettransactionheight": (*Server).getTransactionHeight,
"getunclaimedgas": (*Server).getUnclaimedGas, "getunclaimedgas": (*Server).getUnclaimedGas,
"getnextblockvalidators": (*Server).getNextBlockValidators, "getnextblockvalidators": (*Server).getNextBlockValidators,
"getversion": (*Server).getVersion, "getversion": (*Server).getVersion,
"invokefunction": (*Server).invokeFunction, "invokefunction": (*Server).invokeFunction,
"invokescript": (*Server).invokescript, "invokefunctionhistoric": (*Server).invokeFunctionHistoric,
"invokecontractverify": (*Server).invokeContractVerify, "invokescript": (*Server).invokescript,
"sendrawtransaction": (*Server).sendrawtransaction, "invokescripthistoric": (*Server).invokescripthistoric,
"submitblock": (*Server).submitBlock, "invokecontractverify": (*Server).invokeContractVerify,
"submitnotaryrequest": (*Server).submitNotaryRequest, "invokecontractverifyhistoric": (*Server).invokeContractVerifyHistoric,
"submitoracleresponse": (*Server).submitOracleResponse, "sendrawtransaction": (*Server).sendrawtransaction,
"validateaddress": (*Server).validateAddress, "submitblock": (*Server).submitBlock,
"verifyproof": (*Server).verifyProof, "submitnotaryrequest": (*Server).submitNotaryRequest,
"submitoracleresponse": (*Server).submitOracleResponse,
"validateaddress": (*Server).validateAddress,
"verifyproof": (*Server).verifyProof,
} }
var rpcWsHandlers = map[string]func(*Server, request.Params, *subscriber) (interface{}, *response.Error){ 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() script := bw.Bytes()
tx := &transaction.Transaction{Script: script} tx := &transaction.Transaction{Script: script}
b, err := s.getFakeNextBlock() b, err := s.getFakeNextBlock(s.chain.BlockHeight() + 1)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@ -1571,16 +1575,40 @@ func (s *Server) getCommittee(_ request.Params) (interface{}, *response.Error) {
// invokeFunction implements the `invokeFunction` RPC call. // invokeFunction implements the `invokeFunction` RPC call.
func (s *Server) invokeFunction(reqParams request.Params) (interface{}, *response.Error) { 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 { if len(reqParams) < 2 {
return nil, response.ErrInvalidParams 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)) scriptHash, responseErr := s.contractScriptHashFromParam(reqParams.Value(0))
if responseErr != nil { if responseErr != nil {
return nil, responseErr return nil, false, responseErr
} }
method, err := reqParams[1].GetString() method, err := reqParams[1].GetString()
if err != nil { if err != nil {
return nil, response.ErrInvalidParams return nil, false, response.ErrInvalidParams
} }
var params *request.Param var params *request.Param
if len(reqParams) > 2 { if len(reqParams) > 2 {
@ -1590,7 +1618,7 @@ func (s *Server) invokeFunction(reqParams request.Params) (interface{}, *respons
if len(reqParams) > 3 { if len(reqParams) > 3 {
signers, _, err := reqParams[3].GetSignersWithWitnesses() signers, _, err := reqParams[3].GetSignersWithWitnesses()
if err != nil { if err != nil {
return nil, response.ErrInvalidParams return nil, false, response.ErrInvalidParams
} }
tx.Signers = signers tx.Signers = signers
} }
@ -1598,7 +1626,7 @@ func (s *Server) invokeFunction(reqParams request.Params) (interface{}, *respons
if len(reqParams) > 4 { if len(reqParams) > 4 {
verbose, err = reqParams[4].GetBoolean() verbose, err = reqParams[4].GetBoolean()
if err != nil { if err != nil {
return nil, response.ErrInvalidParams return nil, false, response.ErrInvalidParams
} }
} }
if len(tx.Signers) == 0 { 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) script, err := request.CreateFunctionInvocationScript(scriptHash, method, params)
if err != nil { 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 tx.Script = script
return s.runScriptInVM(trigger.Application, script, util.Uint160{}, tx, verbose) return tx, verbose, nil
} }
// invokescript implements the `invokescript` RPC call. // invokescript implements the `invokescript` RPC call.
func (s *Server) invokescript(reqParams request.Params) (interface{}, *response.Error) { 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 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 { if err != nil {
return nil, response.ErrInvalidParams return nil, false, response.ErrInvalidParams
} }
tx := &transaction.Transaction{} tx := &transaction.Transaction{}
if len(reqParams) > 1 { if len(reqParams) > 1 {
signers, witnesses, err := reqParams[1].GetSignersWithWitnesses() signers, witnesses, err := reqParams[1].GetSignersWithWitnesses()
if err != nil { if err != nil {
return nil, response.ErrInvalidParams return nil, false, response.ErrInvalidParams
} }
tx.Signers = signers tx.Signers = signers
tx.Scripts = witnesses tx.Scripts = witnesses
@ -1636,33 +1684,57 @@ func (s *Server) invokescript(reqParams request.Params) (interface{}, *response.
if len(reqParams) > 2 { if len(reqParams) > 2 {
verbose, err = reqParams[2].GetBoolean() verbose, err = reqParams[2].GetBoolean()
if err != nil { if err != nil {
return nil, response.ErrInvalidParams return nil, false, response.ErrInvalidParams
} }
} }
if len(tx.Signers) == 0 { if len(tx.Signers) == 0 {
tx.Signers = []transaction.Signer{{Account: util.Uint160{}, Scopes: transaction.None}} tx.Signers = []transaction.Signer{{Account: util.Uint160{}, Scopes: transaction.None}}
} }
tx.Script = script tx.Script = script
return s.runScriptInVM(trigger.Application, script, util.Uint160{}, tx, verbose) return tx, verbose, nil
} }
// invokeContractVerify implements the `invokecontractverify` RPC call. // invokeContractVerify implements the `invokecontractverify` RPC call.
func (s *Server) invokeContractVerify(reqParams request.Params) (interface{}, *response.Error) { 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)) scriptHash, responseErr := s.contractScriptHashFromParam(reqParams.Value(0))
if responseErr != nil { if responseErr != nil {
return nil, responseErr return util.Uint160{}, nil, nil, responseErr
} }
bw := io.NewBufBinWriter() bw := io.NewBufBinWriter()
if len(reqParams) > 1 { if len(reqParams) > 1 {
args, err := reqParams[1].GetArray() // second `invokecontractverify` parameter is an array of arguments for `verify` method args, err := reqParams[1].GetArray() // second `invokecontractverify` parameter is an array of arguments for `verify` method
if err != nil { if err != nil {
return nil, response.WrapErrorWithData(response.ErrInvalidParams, err) return util.Uint160{}, nil, nil, response.WrapErrorWithData(response.ErrInvalidParams, err)
} }
if len(args) > 0 { if len(args) > 0 {
err := request.ExpandArrayIntoScript(bw.BinWriter, args) err := request.ExpandArrayIntoScript(bw.BinWriter, args)
if err != nil { 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 { if len(reqParams) > 2 {
signers, witnesses, err := reqParams[2].GetSignersWithWitnesses() signers, witnesses, err := reqParams[2].GetSignersWithWitnesses()
if err != nil { if err != nil {
return nil, response.ErrInvalidParams return util.Uint160{}, nil, nil, response.ErrInvalidParams
} }
tx.Signers = signers tx.Signers = signers
tx.Scripts = witnesses tx.Scripts = witnesses
@ -1680,16 +1752,51 @@ func (s *Server) invokeContractVerify(reqParams request.Params) (interface{}, *r
tx.Signers = []transaction.Signer{{Account: scriptHash}} tx.Signers = []transaction.Signer{{Account: scriptHash}}
tx.Scripts = []transaction.Witness{{InvocationScript: invocationScript, VerificationScript: []byte{}}} 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, // When transferring funds, script execution does no auto GAS claim,
// because it depends on persisting tx height. // because it depends on persisting tx height.
// This is why we provide block here. // This is why we provide block here.
b := block.New(s.stateRootEnabled) b := block.New(s.stateRootEnabled)
b.Index = s.chain.BlockHeight() + 1 b.Index = nextBlockHeight
hdr, err := s.chain.GetHeader(s.chain.GetHeaderHash(int(s.chain.BlockHeight()))) hdr, err := s.chain.GetHeader(s.chain.GetHeaderHash(int(nextBlockHeight - 1)))
if err != nil { if err != nil {
return nil, err 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` // witness invocation script in case of `verification` trigger (it pushes `verify`
// arguments on stack before verification). In case of contract verification // arguments on stack before verification). In case of contract verification
// contractScriptHash should be specified. // 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) { func (s *Server) runScriptInVM(t trigger.Type, script []byte, contractScriptHash util.Uint160, tx *transaction.Transaction, b *block.Block, verbose bool) (*result.Invoke, *response.Error) {
b, err := s.getFakeNextBlock() var (
if err != nil { err error
return nil, response.NewInternalServerError("can't create fake block", err) 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 { if verbose {
ic.VM.EnableInvocationTree() ic.VM.EnableInvocationTree()
} }
@ -1720,9 +1838,9 @@ func (s *Server) runScriptInVM(t trigger.Type, script []byte, contractScriptHash
ic.VM.GasLimit = gasPolicy 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 { 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 { } else {
ic.VM.LoadScriptWithFlags(script, callflag.All) ic.VM.LoadScriptWithFlags(script, callflag.All)

View file

@ -75,6 +75,7 @@ const (
nfsoContractHash = "5f9ebd6b001b54c7bc70f96e0412fcf415dfe09f" nfsoContractHash = "5f9ebd6b001b54c7bc70f96e0412fcf415dfe09f"
nfsoToken1ID = "7e244ffd6aa85fb1579d2ed22e9b761ab62e3486" nfsoToken1ID = "7e244ffd6aa85fb1579d2ed22e9b761ab62e3486"
invokescriptContractAVM = "VwIADBQBDAMOBQYMDQIODw0DDgcJAAAAAErZMCQE2zBwaEH4J+yMqiYEEUAMFA0PAwIJAAIBAwcDBAUCAQAOBgwJStkwJATbMHFpQfgn7IyqJgQSQBNA" invokescriptContractAVM = "VwIADBQBDAMOBQYMDQIODw0DDgcJAAAAAErZMCQE2zBwaEH4J+yMqiYEEUAMFA0PAwIJAAIBAwcDBAUCAQAOBgwJStkwJATbMHFpQfgn7IyqJgQSQBNA"
block20StateRootLE = "19ec3c3d01afe5274e8bb4a393c97da708c5608c5b0ad116c16108b6a04fb08e"
) )
var ( var (
@ -999,6 +1000,134 @@ var rpcTestCases = map[string][]rpcTestCase{
fail: true, 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": { "invokescript": {
{ {
name: "positive", name: "positive",
@ -1098,6 +1227,132 @@ var rpcTestCases = map[string][]rpcTestCase{
fail: true, 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": { "invokecontractverify": {
{ {
name: "positive", name: "positive",
@ -1203,6 +1458,129 @@ var rpcTestCases = map[string][]rpcTestCase{
fail: true, 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": { "sendrawtransaction": {
{ {
name: "positive", name: "positive",