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
burned).
#### `invokecontractverifyhistoric`, `invokefunctionhistoric` and `invokescripthistoric` calls
These methods provide the ability of *historical* calls and accept block hash or
block index or stateroot hash as the first parameter and the list of parameters
that is the same as of `invokecontractverify`, `invokefunction` and
`invokescript` correspondingly. The historical call assumes that the contracts'
storage state has all its values got from MPT with the specified stateroot and
the transaction will be invoked using interop context with block of the specified
height. This allows to perform test invocation using the specified past chain
state. These methods may be useful for debugging purposes.
Behavior note: any historical RPC call need the historical chain state to be
presented in the node storage, thus if the node keeps only latest MPT state
the historical call can not be handled properly.The historical calls only
guaranteed to correctly work on archival node that stores all MPT data. If a
node keeps the number of latest states and has the GC on (this setting
corresponds to the `RemoveUntraceableBlocks` set to `true`), then the behaviour
of historical RPC call is undefined. GC can always kick some data out of the
storage while the historical call is executing, thus keep in mind that the call
can be processed with `RemoveUntraceableBlocks` only with limitations on
available data.
#### `submitnotaryrequest` call
This method can be used on P2P Notary enabled networks to submit new notary

View file

@ -201,7 +201,8 @@ func TestAppCall(t *testing.T) {
}
fc := fakechain.NewFakeChain()
ic := interop.NewContext(trigger.Application, fc, dao.NewSimple(storage.NewMemoryStore(), false, false), interop.DefaultBaseExecFee, native.DefaultStoragePrice, contractGetter, nil, nil, nil, zaptest.NewLogger(t))
ic := interop.NewContext(trigger.Application, fc, dao.NewSimple(storage.NewMemoryStore(), false, false),
interop.DefaultBaseExecFee, native.DefaultStoragePrice, contractGetter, nil, nil, nil, zaptest.NewLogger(t))
t.Run("valid script", func(t *testing.T) {
src := getAppCallScript(fmt.Sprintf("%#v", ih.BytesBE()))

View file

@ -76,7 +76,7 @@ func TestCreateBasicChain(t *testing.T) {
func initBasicChain(t *testing.T, e *neotest.Executor) {
if !e.Chain.GetConfig().P2PSigExtensions {
t.Fatal("P2PSitExtensions should be enabled to init basic chain")
t.Fatal("P2PSigExtensions should be enabled to init basic chain")
}
const neoAmount = 99999000

View file

@ -433,14 +433,9 @@ func (bc *Blockchain) init() error {
return fmt.Errorf("can't init MPT at height %d: %w", bHeight, err)
}
err = bc.contracts.NEO.InitializeCache(bc, bc.dao)
err = bc.initializeNativeCache(bc.blockHeight, bc.dao)
if err != nil {
return fmt.Errorf("can't init cache for NEO native contract: %w", err)
}
err = bc.contracts.Management.InitializeCache(bc.dao)
if err != nil {
return fmt.Errorf("can't init cache for Management native contract: %w", err)
return fmt.Errorf("can't init natives cache: %w", err)
}
// Check autogenerated native contracts' manifests and NEFs against the stored ones.
@ -575,15 +570,10 @@ func (bc *Blockchain) jumpToStateInternal(p uint32, stage stateJumpStage) error
Root: block.PrevStateRoot,
})
err = bc.contracts.NEO.InitializeCache(bc, bc.dao)
err = bc.initializeNativeCache(block.Index, bc.dao)
if err != nil {
return fmt.Errorf("can't init cache for NEO native contract: %w", err)
return fmt.Errorf("failed to initialize natives cache: %w", err)
}
err = bc.contracts.Management.InitializeCache(bc.dao)
if err != nil {
return fmt.Errorf("can't init cache for Management native contract: %w", err)
}
bc.contracts.Designate.InitializeCache()
if err := bc.updateExtensibleWhitelist(p); err != nil {
return fmt.Errorf("failed to update extensible whitelist: %w", err)
@ -595,6 +585,33 @@ func (bc *Blockchain) jumpToStateInternal(p uint32, stage stateJumpStage) error
return nil
}
func (bc *Blockchain) initializeNativeCache(blockHeight uint32, d *dao.Simple) error {
err := bc.contracts.NEO.InitializeCache(blockHeight, d)
if err != nil {
return fmt.Errorf("can't init cache for NEO native contract: %w", err)
}
err = bc.contracts.Management.InitializeCache(d)
if err != nil {
return fmt.Errorf("can't init cache for Management native contract: %w", err)
}
err = bc.contracts.Designate.InitializeCache(d)
if err != nil {
return fmt.Errorf("can't init cache for Designation native contract: %w", err)
}
bc.contracts.Oracle.InitializeCache(d)
if bc.P2PSigExtensionsEnabled() {
err = bc.contracts.Notary.InitializeCache(d)
if err != nil {
return fmt.Errorf("can't init cache for Notary native contract: %w", err)
}
}
err = bc.contracts.Policy.InitializeCache(d)
if err != nil {
return fmt.Errorf("can't init cache for Policy native contract: %w", err)
}
return nil
}
// Run runs chain loop, it needs to be run as goroutine and executing it is
// critical for correct Blockchain operation.
func (bc *Blockchain) Run() {
@ -1220,14 +1237,14 @@ func (bc *Blockchain) updateExtensibleWhitelist(height uint32) error {
return nil
}
newList := []util.Uint160{bc.contracts.NEO.GetCommitteeAddress()}
nextVals := bc.contracts.NEO.GetNextBlockValidatorsInternal()
newList := []util.Uint160{bc.contracts.NEO.GetCommitteeAddress(bc.dao)}
nextVals := bc.contracts.NEO.GetNextBlockValidatorsInternal(bc.dao)
script, err := smartcontract.CreateDefaultMultiSigRedeemScript(nextVals)
if err != nil {
return err
}
newList = append(newList, hash.Hash160(script))
bc.updateExtensibleList(&newList, bc.contracts.NEO.GetNextBlockValidatorsInternal())
bc.updateExtensibleList(&newList, bc.contracts.NEO.GetNextBlockValidatorsInternal(bc.dao))
if len(stateVals) > 0 {
h, err := bc.contracts.Designate.GetLastDesignatedHash(bc.dao, noderoles.StateValidator)
@ -1451,12 +1468,12 @@ func (bc *Blockchain) ForEachNEP11Transfer(acc util.Uint160, newestTimestamp uin
// GetNEP17Contracts returns the list of deployed NEP-17 contracts.
func (bc *Blockchain) GetNEP17Contracts() []util.Uint160 {
return bc.contracts.Management.GetNEP17Contracts()
return bc.contracts.Management.GetNEP17Contracts(bc.dao)
}
// GetNEP11Contracts returns the list of deployed NEP-11 contracts.
func (bc *Blockchain) GetNEP11Contracts() []util.Uint160 {
return bc.contracts.Management.GetNEP11Contracts()
return bc.contracts.Management.GetNEP11Contracts(bc.dao)
}
// GetTokenLastUpdated returns a set of contract ids with the corresponding last updated
@ -1823,7 +1840,7 @@ func (bc *Blockchain) ApplyPolicyToTxSet(txes []*transaction.Transaction) []*tra
curVC := bc.config.GetNumOfCNs(bc.BlockHeight() + 1)
if oldVC == nil || oldVC != curVC {
m := smartcontract.GetDefaultHonestNodeCount(curVC)
verification, _ := smartcontract.CreateDefaultMultiSigRedeemScript(bc.contracts.NEO.GetNextBlockValidatorsInternal())
verification, _ := smartcontract.CreateDefaultMultiSigRedeemScript(bc.contracts.NEO.GetNextBlockValidatorsInternal(bc.dao))
defaultWitness = transaction.Witness{
InvocationScript: make([]byte, 66*m),
VerificationScript: verification,
@ -1939,7 +1956,7 @@ func (bc *Blockchain) verifyAndPoolTx(t *transaction.Transaction, pool *mempool.
if err != nil {
return err
}
if err := bc.verifyTxAttributes(t, isPartialTx); err != nil {
if err := bc.verifyTxAttributes(bc.dao, t, isPartialTx); err != nil {
return err
}
err = pool.Add(t, feer, data...)
@ -1963,11 +1980,11 @@ func (bc *Blockchain) verifyAndPoolTx(t *transaction.Transaction, pool *mempool.
return nil
}
func (bc *Blockchain) verifyTxAttributes(tx *transaction.Transaction, isPartialTx bool) error {
func (bc *Blockchain) verifyTxAttributes(d *dao.Simple, tx *transaction.Transaction, isPartialTx bool) error {
for i := range tx.Attributes {
switch attrType := tx.Attributes[i].Type; attrType {
case transaction.HighPriority:
h := bc.contracts.NEO.GetCommitteeAddress()
h := bc.contracts.NEO.GetCommitteeAddress(d)
if !tx.HasSigner(h) {
return fmt.Errorf("%w: high priority tx is not signed by committee", ErrInvalidAttribute)
}
@ -2061,7 +2078,7 @@ func (bc *Blockchain) IsTxStillRelevant(t *transaction.Transaction, txpool *memp
} else if txpool.HasConflicts(t, bc) {
return false
}
if err := bc.verifyTxAttributes(t, isPartialTx); err != nil {
if err := bc.verifyTxAttributes(bc.dao, t, isPartialTx); err != nil {
return false
}
for i := range t.Scripts {
@ -2119,19 +2136,19 @@ func (bc *Blockchain) PoolTxWithData(t *transaction.Transaction, data interface{
// GetCommittee returns the sorted list of public keys of nodes in committee.
func (bc *Blockchain) GetCommittee() (keys.PublicKeys, error) {
pubs := bc.contracts.NEO.GetCommitteeMembers()
pubs := bc.contracts.NEO.GetCommitteeMembers(bc.dao)
sort.Sort(pubs)
return pubs, nil
}
// GetValidators returns current validators.
func (bc *Blockchain) GetValidators() ([]*keys.PublicKey, error) {
return bc.contracts.NEO.ComputeNextBlockValidators(bc, bc.dao)
return bc.contracts.NEO.ComputeNextBlockValidators(bc.blockHeight, bc.dao)
}
// GetNextBlockValidators returns next block validators.
func (bc *Blockchain) GetNextBlockValidators() ([]*keys.PublicKey, error) {
return bc.contracts.NEO.GetNextBlockValidatorsInternal(), nil
return bc.contracts.NEO.GetNextBlockValidatorsInternal(bc.dao), nil
}
// GetEnrollments returns all registered validators.
@ -2148,6 +2165,41 @@ func (bc *Blockchain) GetTestVM(t trigger.Type, tx *transaction.Transaction, b *
return systemInterop
}
// GetTestHistoricVM returns an interop context with VM set up for a test run.
func (bc *Blockchain) GetTestHistoricVM(t trigger.Type, tx *transaction.Transaction, b *block.Block) (*interop.Context, error) {
if bc.config.KeepOnlyLatestState {
return nil, errors.New("only latest state is supported")
}
if b == nil {
return nil, errors.New("block is mandatory to produce test historic VM")
}
var mode = mpt.ModeAll
if bc.config.RemoveUntraceableBlocks {
if b.Index < bc.BlockHeight()-bc.config.MaxTraceableBlocks {
return nil, fmt.Errorf("state for height %d is outdated and removed from the storage", b.Index)
}
mode |= mpt.ModeGCFlag
}
sr, err := bc.stateRoot.GetStateRoot(b.Index)
if err != nil {
return nil, fmt.Errorf("failed to retrieve stateroot for height %d: %w", b.Index, err)
}
s := mpt.NewTrieStore(sr.Root, mode, storage.NewPrivateMemCachedStore(bc.dao.Store))
dTrie := dao.NewSimple(s, bc.config.StateRootInHeader, bc.config.P2PSigExtensions)
dTrie.Version = bc.dao.Version
// Initialize native cache before passing DAO to interop context constructor, because
// the constructor will call BaseExecFee/StoragePrice policy methods on the passed DAO.
err = bc.initializeNativeCache(b.Index, dTrie)
if err != nil {
return nil, fmt.Errorf("failed to initialize native cache backed by historic DAO: %w", err)
}
systemInterop := bc.newInteropContext(t, dTrie, b, tx)
vm := systemInterop.SpawnVM()
vm.SetPriceGetter(systemInterop.GetPrice)
vm.LoadToken = contract.LoadToken(systemInterop)
return systemInterop, nil
}
// Various witness verification errors.
var (
ErrWitnessHashMismatch = errors.New("witness hash mismatch")

View file

@ -11,7 +11,6 @@ import (
"github.com/nspcc-dev/neo-go/internal/testchain"
"github.com/nspcc-dev/neo-go/pkg/config"
"github.com/nspcc-dev/neo-go/pkg/core/block"
"github.com/nspcc-dev/neo-go/pkg/core/dao"
"github.com/nspcc-dev/neo-go/pkg/core/state"
"github.com/nspcc-dev/neo-go/pkg/core/storage"
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
@ -329,7 +328,7 @@ func TestBlockchain_BaseExecFeeBaseStoragePrice_Compat(t *testing.T) {
bc := newTestChain(t)
check := func(t *testing.T) {
ic := bc.newInteropContext(trigger.Application, dao.NewSimple(storage.NewMemoryStore(), bc.config.StateRootInHeader, bc.config.P2PSigExtensions), bc.topBlock.Load().(*block.Block), nil)
ic := bc.newInteropContext(trigger.Application, bc.dao, bc.topBlock.Load().(*block.Block), nil)
require.Equal(t, bc.GetBaseExecFee(), ic.BaseExecFee())
require.Equal(t, bc.GetStoragePrice(), ic.BaseStorageFee())
}

View file

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

View file

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

View file

@ -7,6 +7,7 @@ import (
"errors"
"fmt"
iocore "io"
"sync"
"github.com/nspcc-dev/neo-go/pkg/core/block"
"github.com/nspcc-dev/neo-go/pkg/core/state"
@ -34,11 +35,28 @@ var (
type Simple struct {
Version Version
Store *storage.MemCachedStore
nativeCacheLock sync.RWMutex
nativeCache map[int32]NativeContractCache
// nativeCachePS is the backend store that provides functionality to store
// and retrieve multi-tier native contract cache. The lowest Simple has its
// nativeCachePS set to nil.
nativeCachePS *Simple
private bool
keyBuf []byte
dataBuf *io.BufBinWriter
}
// NativeContractCache is an interface representing cache for a native contract.
// Cache can be copied to create a wrapper around current DAO layer. Wrapped cache
// can be persisted to the underlying DAO native cache.
type NativeContractCache interface {
// Copy returns a copy of native cache item that can safely be changed within
// the subsequent DAO operations.
Copy() NativeContractCache
}
// NewSimple creates new simple dao using provided backend store.
func NewSimple(backend storage.Store, stateRootInHeader bool, p2pSigExtensions bool) *Simple {
st := storage.NewMemCachedStore(backend)
@ -52,7 +70,8 @@ func newSimple(st *storage.MemCachedStore, stateRootInHeader bool, p2pSigExtensi
StateRootInHeader: stateRootInHeader,
P2PSigExtensions: p2pSigExtensions,
},
Store: st,
Store: st,
nativeCache: make(map[int32]NativeContractCache),
}
}
@ -66,16 +85,27 @@ func (dao *Simple) GetBatch() *storage.MemBatch {
func (dao *Simple) GetWrapped() *Simple {
d := NewSimple(dao.Store, dao.Version.StateRootInHeader, dao.Version.P2PSigExtensions)
d.Version = dao.Version
d.nativeCachePS = dao
return d
}
// GetPrivate returns new DAO instance with another layer of private
// MemCachedStore around the current DAO Store.
func (dao *Simple) GetPrivate() *Simple {
d := &Simple{}
*d = *dao // Inherit everything...
d := &Simple{
Version: dao.Version,
keyBuf: dao.keyBuf,
dataBuf: dao.dataBuf,
} // Inherit everything...
d.Store = storage.NewPrivateMemCachedStore(dao.Store) // except storage, wrap another layer.
d.private = true
d.nativeCachePS = dao
// Do not inherit cache from nativeCachePS; instead should create clear map:
// GetRWCache and GetROCache will retrieve cache from the underlying
// nativeCache if requested. The lowest underlying DAO MUST have its native
// cache initialized before access it, otherwise GetROCache and GetRWCache
// won't work properly.
d.nativeCache = make(map[int32]NativeContractCache)
return d
}
@ -809,6 +839,17 @@ func (dao *Simple) getDataBuf() *io.BufBinWriter {
// Persist flushes all the changes made into the (supposedly) persistent
// underlying store. It doesn't block accesses to DAO from other threads.
func (dao *Simple) Persist() (int, error) {
if dao.nativeCachePS != nil {
if !dao.private {
dao.nativeCacheLock.Lock()
defer dao.nativeCacheLock.Unlock()
}
if !dao.nativeCachePS.private {
dao.nativeCachePS.nativeCacheLock.Lock()
defer dao.nativeCachePS.nativeCacheLock.Unlock()
}
dao.persistNativeCache()
}
return dao.Store.Persist()
}
@ -816,5 +857,77 @@ func (dao *Simple) Persist() (int, error) {
// underlying store. It's a synchronous version of Persist that doesn't allow
// other threads to work with DAO while flushing the Store.
func (dao *Simple) PersistSync() (int, error) {
if dao.nativeCachePS != nil {
dao.nativeCacheLock.Lock()
dao.nativeCachePS.nativeCacheLock.Lock()
defer func() {
dao.nativeCachePS.nativeCacheLock.Unlock()
dao.nativeCacheLock.Unlock()
}()
dao.persistNativeCache()
}
return dao.Store.PersistSync()
}
// persistNativeCache is internal unprotected method for native cache persisting.
// It does NO checks for nativeCachePS is not nil.
func (dao *Simple) persistNativeCache() {
lower := dao.nativeCachePS
for id, nativeCache := range dao.nativeCache {
lower.nativeCache[id] = nativeCache
}
dao.nativeCache = nil
}
// GetROCache returns native contact cache. The cache CAN NOT be modified by
// the caller. It's the caller's duty to keep it unmodified.
func (dao *Simple) GetROCache(id int32) NativeContractCache {
if !dao.private {
dao.nativeCacheLock.RLock()
defer dao.nativeCacheLock.RUnlock()
}
return dao.getCache(id, true)
}
// GetRWCache returns native contact cache. The cache CAN BE safely modified
// by the caller.
func (dao *Simple) GetRWCache(id int32) NativeContractCache {
if !dao.private {
dao.nativeCacheLock.Lock()
defer dao.nativeCacheLock.Unlock()
}
return dao.getCache(id, false)
}
// getCache is an internal unlocked representation of GetROCache and GetRWCache.
func (dao *Simple) getCache(k int32, ro bool) NativeContractCache {
if itm, ok := dao.nativeCache[k]; ok {
// Don't need to create itm copy, because its value was already copied
// the first time it was retrieved from loser ps.
return itm
}
if dao.nativeCachePS != nil {
if ro {
return dao.nativeCachePS.GetROCache(k)
}
v := dao.nativeCachePS.GetRWCache(k)
if v != nil {
// Create a copy here in order not to modify the existing cache.
cp := v.Copy()
dao.nativeCache[k] = cp
return cp
}
}
return nil
}
// SetCache adds native contract cache to the cache map.
func (dao *Simple) SetCache(id int32, v NativeContractCache) {
dao.nativeCacheLock.Lock()
defer dao.nativeCacheLock.Unlock()
dao.nativeCache[id] = v
}

View file

@ -13,6 +13,7 @@ import (
"github.com/nspcc-dev/neo-go/pkg/core/dao"
"github.com/nspcc-dev/neo-go/pkg/core/interop/interopnames"
"github.com/nspcc-dev/neo-go/pkg/core/state"
"github.com/nspcc-dev/neo-go/pkg/core/storage"
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
"github.com/nspcc-dev/neo-go/pkg/crypto/hash"
"github.com/nspcc-dev/neo-go/pkg/io"
@ -339,3 +340,31 @@ func (ic *Context) Exec() error {
defer ic.Finalize()
return ic.VM.Run()
}
// BlockHeight returns current block height got from Context's block if it's set.
func (ic *Context) BlockHeight() uint32 {
if ic.Block != nil {
return ic.Block.Index - 1 // Persisting block is not yet stored.
}
return ic.Chain.BlockHeight()
}
// CurrentBlockHash returns current block hash got from Context's block if it's set.
func (ic *Context) CurrentBlockHash() util.Uint256 {
if ic.Block != nil {
return ic.Chain.GetHeaderHash(int(ic.Block.Index - 1)) // Persisting block is not yet stored.
}
return ic.Chain.CurrentBlockHash()
}
// GetBlock returns block if it exists and available at the current Context's height.
func (ic *Context) GetBlock(hash util.Uint256) (*block.Block, error) {
block, err := ic.Chain.GetBlock(hash)
if err != nil {
return nil, err
}
if block.Index > ic.BlockHeight() {
return nil, storage.ErrKeyNotFound
}
return block, nil
}

View file

@ -18,7 +18,7 @@ import (
)
type policyChecker interface {
IsBlockedInternal(*dao.Simple, util.Uint160) bool
IsBlocked(*dao.Simple, util.Uint160) bool
}
// LoadToken calls method specified by token id.
@ -97,7 +97,7 @@ func callExFromNative(ic *interop.Context, caller util.Uint160, cs *state.Contra
for _, nc := range ic.Natives {
if nc.Metadata().Name == nativenames.Policy {
var pch = nc.(policyChecker)
if pch.IsBlockedInternal(ic.DAO, cs.Hash) {
if pch.IsBlocked(ic.DAO, cs.Hash) {
return fmt.Errorf("contract %s is blocked", cs.Hash.StringLE())
}
break

View file

@ -12,7 +12,6 @@ import (
"github.com/nspcc-dev/neo-go/pkg/config"
"github.com/nspcc-dev/neo-go/pkg/config/netmode"
"github.com/nspcc-dev/neo-go/pkg/core/block"
"github.com/nspcc-dev/neo-go/pkg/core/dao"
"github.com/nspcc-dev/neo-go/pkg/core/interop"
"github.com/nspcc-dev/neo-go/pkg/core/interop/contract"
"github.com/nspcc-dev/neo-go/pkg/core/interop/iterator"
@ -530,10 +529,10 @@ func TestStorageFind(t *testing.T) {
// Helper functions to create VM, InteropContext, TX, Account, Contract.
func createVM(t *testing.T) (*vm.VM, *interop.Context, *Blockchain) {
func createVM(t testing.TB) (*vm.VM, *interop.Context, *Blockchain) {
chain := newTestChain(t)
context := chain.newInteropContext(trigger.Application,
dao.NewSimple(storage.NewMemoryStore(), chain.config.StateRootInHeader, chain.config.P2PSigExtensions), nil, nil)
chain.dao.GetWrapped(), nil, nil)
v := context.SpawnVM()
return v, context, chain
}
@ -552,10 +551,7 @@ func createVMAndContractState(t testing.TB) (*vm.VM, *state.Contract, *interop.C
},
}
chain := newTestChain(t)
d := dao.NewSimple(storage.NewMemoryStore(), chain.config.StateRootInHeader, chain.config.P2PSigExtensions)
context := chain.newInteropContext(trigger.Application, d, nil, nil)
v := context.SpawnVM()
v, context, chain := createVM(t)
return v, contractState, context, chain
}

View file

@ -5,9 +5,7 @@ import (
"runtime"
"testing"
"github.com/nspcc-dev/neo-go/pkg/core/dao"
"github.com/nspcc-dev/neo-go/pkg/core/interop"
"github.com/nspcc-dev/neo-go/pkg/core/storage"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger"
"github.com/nspcc-dev/neo-go/pkg/vm"
"github.com/stretchr/testify/require"
@ -17,8 +15,7 @@ func testNonInterop(t *testing.T, value interface{}, f func(*interop.Context) er
v := vm.New()
v.Estack().PushVal(value)
chain := newTestChain(t)
d := dao.NewSimple(storage.NewMemoryStore(), chain.config.StateRootInHeader, chain.config.P2PSigExtensions)
context := chain.newInteropContext(trigger.Application, d, nil, nil)
context := chain.newInteropContext(trigger.Application, chain.dao, nil, nil)
context.VM = v
require.Error(t, f(context))
}

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

View file

@ -625,7 +625,7 @@ func (t *Trie) Find(prefix, from []byte, max int) ([]storage.KeyValue, error) {
}
return count >= max
}
_, err = b.traverse(start, path, fromP, process, false)
_, err = b.traverse(start, path, fromP, process, false, false)
if err != nil && !errors.Is(err, errStop) {
return nil, err
}

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)
gas := newGAS(int64(cfg.InitialGASSupply), cfg.P2PSigExtensions)
neo := newNEO()
neo := newNEO(cfg)
policy := newPolicy()
neo.GAS = gas
neo.Policy = policy

View file

@ -3,6 +3,7 @@ package native
import (
"encoding/binary"
"errors"
"fmt"
"math"
"math/big"
"sort"
@ -31,12 +32,6 @@ type Designate struct {
interop.ContractMD
NEO *NEO
rolesChangedFlag atomic.Value
oracles atomic.Value
stateVals atomic.Value
neofsAlphabet atomic.Value
notaries atomic.Value
// p2pSigExtensionsEnabled defines whether the P2P signature extensions logic is relevant.
p2pSigExtensionsEnabled bool
@ -53,6 +48,16 @@ type roleData struct {
height uint32
}
type DesignationCache struct {
// rolesChangedFlag shows whether any of designated nodes were changed within the current block.
// It is used to notify dependant services about updated node roles during PostPersist.
rolesChangedFlag bool
oracles roleData
stateVals roleData
neofsAlphabet roleData
notaries roleData
}
const (
designateContractID = -8
@ -73,6 +78,22 @@ var (
ErrNoBlock = errors.New("no persisting block in the context")
)
var (
_ interop.Contract = (*Designate)(nil)
_ dao.NativeContractCache = (*DesignationCache)(nil)
)
// Copy implements NativeContractCache interface.
func (c *DesignationCache) Copy() dao.NativeContractCache {
cp := &DesignationCache{}
copyDesignationCache(c, cp)
return cp
}
func copyDesignationCache(src, dst *DesignationCache) {
*dst = *src
}
func (s *Designate) isValidRole(r noderoles.Role) bool {
return r == noderoles.Oracle || r == noderoles.StateValidator ||
r == noderoles.NeoFSAlphabet || (s.p2pSigExtensionsEnabled && r == noderoles.P2PNotary)
@ -102,8 +123,30 @@ func newDesignate(p2pSigExtensionsEnabled bool) *Designate {
return s
}
// Initialize initializes Oracle contract.
// Initialize initializes Designation contract. It is called once at native Management's OnPersist
// at the genesis block, and we can't properly fill the cache at this point, as there are no roles
// data in the storage.
func (s *Designate) Initialize(ic *interop.Context) error {
cache := &DesignationCache{}
ic.DAO.SetCache(s.ID, cache)
return nil
}
// InitializeCache fills native Designate cache from DAO. It is called at non-zero height, thus
// we can fetch the roles data right from the storage.
func (s *Designate) InitializeCache(d *dao.Simple) error {
cache := &DesignationCache{}
roles := []noderoles.Role{noderoles.Oracle, noderoles.NeoFSAlphabet, noderoles.StateValidator}
if s.p2pSigExtensionsEnabled {
roles = append(roles, noderoles.P2PNotary)
}
for _, r := range roles {
err := s.updateCachedRoleData(cache, d, r)
if err != nil {
return fmt.Errorf("failed to get nodes from storage for %d role: %w", r, err)
}
}
d.SetCache(s.ID, cache)
return nil
}
@ -114,26 +157,19 @@ func (s *Designate) OnPersist(ic *interop.Context) error {
// PostPersist implements Contract interface.
func (s *Designate) PostPersist(ic *interop.Context) error {
if !s.rolesChanged() {
cache := ic.DAO.GetRWCache(s.ID).(*DesignationCache)
if !cache.rolesChangedFlag {
return nil
}
if err := s.updateCachedRoleData(&s.oracles, ic.DAO, noderoles.Oracle); err != nil {
return err
}
if err := s.updateCachedRoleData(&s.stateVals, ic.DAO, noderoles.StateValidator); err != nil {
return err
}
if err := s.updateCachedRoleData(&s.neofsAlphabet, ic.DAO, noderoles.NeoFSAlphabet); err != nil {
return err
}
s.notifyRoleChanged(&cache.oracles, noderoles.Oracle)
s.notifyRoleChanged(&cache.stateVals, noderoles.StateValidator)
s.notifyRoleChanged(&cache.neofsAlphabet, noderoles.NeoFSAlphabet)
if s.p2pSigExtensionsEnabled {
if err := s.updateCachedRoleData(&s.notaries, ic.DAO, noderoles.P2PNotary); err != nil {
return err
}
s.notifyRoleChanged(&cache.notaries, noderoles.P2PNotary)
}
s.rolesChangedFlag.Store(false)
cache.rolesChangedFlag = false
return nil
}
@ -152,7 +188,7 @@ func (s *Designate) getDesignatedByRole(ic *interop.Context, args []stackitem.It
panic(ErrInvalidIndex)
}
index := ind.Uint64()
if index > uint64(ic.Chain.BlockHeight()+1) {
if index > uint64(ic.BlockHeight()+1) {
panic(ErrInvalidIndex)
}
pubs, _, err := s.GetDesignatedByRole(ic.DAO, r, uint32(index))
@ -162,11 +198,6 @@ func (s *Designate) getDesignatedByRole(ic *interop.Context, args []stackitem.It
return pubsToArray(pubs)
}
func (s *Designate) rolesChanged() bool {
rc := s.rolesChangedFlag.Load()
return rc == nil || rc.(bool)
}
func (s *Designate) hashFromNodes(r noderoles.Role, nodes keys.PublicKeys) util.Uint160 {
if len(nodes) == 0 {
return util.Uint160{}
@ -181,47 +212,58 @@ func (s *Designate) hashFromNodes(r noderoles.Role, nodes keys.PublicKeys) util.
return hash.Hash160(script)
}
func (s *Designate) updateCachedRoleData(v *atomic.Value, d *dao.Simple, r noderoles.Role) error {
nodeKeys, height, err := s.GetDesignatedByRole(d, r, math.MaxUint32)
// updateCachedRoleData fetches the most recent role data from the storage and
// updates the given cache.
func (s *Designate) updateCachedRoleData(cache *DesignationCache, d *dao.Simple, r noderoles.Role) error {
var v *roleData
switch r {
case noderoles.Oracle:
v = &cache.oracles
case noderoles.StateValidator:
v = &cache.stateVals
case noderoles.NeoFSAlphabet:
v = &cache.neofsAlphabet
case noderoles.P2PNotary:
v = &cache.notaries
}
nodeKeys, height, err := s.getDesignatedByRoleFromStorage(d, r, math.MaxUint32)
if err != nil {
return err
}
v.Store(&roleData{
nodes: nodeKeys,
addr: s.hashFromNodes(r, nodeKeys),
height: height,
})
switch r {
case noderoles.Oracle:
if orc, _ := s.OracleService.Load().(services.Oracle); orc != nil {
orc.UpdateOracleNodes(nodeKeys.Copy())
}
case noderoles.P2PNotary:
if ntr, _ := s.NotaryService.Load().(services.Notary); ntr != nil {
ntr.UpdateNotaryNodes(nodeKeys.Copy())
}
case noderoles.StateValidator:
if s.StateRootService != nil {
s.StateRootService.UpdateStateValidators(height, nodeKeys.Copy())
}
}
v.nodes = nodeKeys
v.addr = s.hashFromNodes(r, nodeKeys)
v.height = height
cache.rolesChangedFlag = true
return nil
}
func (s *Designate) getCachedRoleData(r noderoles.Role) *roleData {
var val interface{}
func (s *Designate) notifyRoleChanged(v *roleData, r noderoles.Role) {
switch r {
case noderoles.Oracle:
val = s.oracles.Load()
case noderoles.StateValidator:
val = s.stateVals.Load()
case noderoles.NeoFSAlphabet:
val = s.neofsAlphabet.Load()
if orc, _ := s.OracleService.Load().(services.Oracle); orc != nil {
orc.UpdateOracleNodes(v.nodes.Copy())
}
case noderoles.P2PNotary:
val = s.notaries.Load()
if ntr, _ := s.NotaryService.Load().(services.Notary); ntr != nil {
ntr.UpdateNotaryNodes(v.nodes.Copy())
}
case noderoles.StateValidator:
if s.StateRootService != nil {
s.StateRootService.UpdateStateValidators(v.height, v.nodes.Copy())
}
}
if val != nil {
return val.(*roleData)
}
func getCachedRoleData(cache *DesignationCache, r noderoles.Role) *roleData {
switch r {
case noderoles.Oracle:
return &cache.oracles
case noderoles.StateValidator:
return &cache.stateVals
case noderoles.NeoFSAlphabet:
return &cache.neofsAlphabet
case noderoles.P2PNotary:
return &cache.notaries
}
return nil
}
@ -231,17 +273,11 @@ func (s *Designate) GetLastDesignatedHash(d *dao.Simple, r noderoles.Role) (util
if !s.isValidRole(r) {
return util.Uint160{}, ErrInvalidRole
}
if !s.rolesChanged() {
if val := s.getCachedRoleData(r); val != nil {
return val.addr, nil
}
cache := d.GetROCache(s.ID).(*DesignationCache)
if val := getCachedRoleData(cache, r); val != nil {
return val.addr, nil
}
nodes, _, err := s.GetDesignatedByRole(d, r, math.MaxUint32)
if err != nil {
return util.Uint160{}, err
}
// We only have hashing defined for oracles now.
return s.hashFromNodes(r, nodes), nil
return util.Uint160{}, nil
}
// GetDesignatedByRole returns nodes for role r.
@ -249,11 +285,22 @@ func (s *Designate) GetDesignatedByRole(d *dao.Simple, r noderoles.Role, index u
if !s.isValidRole(r) {
return nil, 0, ErrInvalidRole
}
if !s.rolesChanged() {
if val := s.getCachedRoleData(r); val != nil && val.height <= index {
cache := d.GetROCache(s.ID).(*DesignationCache)
if val := getCachedRoleData(cache, r); val != nil {
if val.height <= index {
return val.nodes.Copy(), val.height, nil
}
} else {
// Cache is always valid, thus if there's no cache then there's no designated nodes for this role.
return nil, 0, nil
}
// Cache stores only latest designated nodes, so if the old info is requested, then we still need
// to search in the storage.
return s.getDesignatedByRoleFromStorage(d, r, index)
}
// getDesignatedByRoleFromStorage returns nodes for role r from the storage.
func (s *Designate) getDesignatedByRoleFromStorage(d *dao.Simple, r noderoles.Role, index uint32) (keys.PublicKeys, uint32, error) {
var (
ns NodeList
bestIndex uint32
@ -310,7 +357,7 @@ func (s *Designate) DesignateAsRole(ic *interop.Context, r noderoles.Role, pubs
if !s.isValidRole(r) {
return ErrInvalidRole
}
h := s.NEO.GetCommitteeAddress()
h := s.NEO.GetCommitteeAddress(ic.DAO)
if ok, err := runtime.CheckHashedWitness(ic, h); err != nil || !ok {
return ErrInvalidWitness
}
@ -327,12 +374,18 @@ func (s *Designate) DesignateAsRole(ic *interop.Context, r noderoles.Role, pubs
}
sort.Sort(pubs)
nl := NodeList(pubs)
s.rolesChangedFlag.Store(true)
err := putConvertibleToDAO(s.ID, ic.DAO, key, &nl)
if err != nil {
return err
}
cache := ic.DAO.GetRWCache(s.ID).(*DesignationCache)
err = s.updateCachedRoleData(cache, ic.DAO, r)
if err != nil {
return fmt.Errorf("failed to update Designation role data cache: %w", err)
}
ic.Notifications = append(ic.Notifications, state.NotificationEvent{
ScriptHash: s.Hash,
Name: DesignationEventName,
@ -355,8 +408,3 @@ func (s *Designate) getRole(item stackitem.Item) (noderoles.Role, bool) {
u := bi.Uint64()
return noderoles.Role(u), u <= math.MaxUint8 && s.isValidRole(noderoles.Role(u))
}
// InitializeCache invalidates native Designate cache.
func (s *Designate) InitializeCache() {
s.rolesChangedFlag.Store(true)
}

View file

@ -31,7 +31,7 @@ func Call(ic *interop.Context) error {
if len(history) == 0 {
return fmt.Errorf("native contract %s is disabled", c.Metadata().Name)
}
if history[0] > ic.Chain.BlockHeight() {
if history[0] > ic.BlockHeight() {
return fmt.Errorf("native contract %s is active after height = %d", c.Metadata().Name, history[0])
}
m, ok := c.Metadata().GetMethodByOffset(ic.VM.Context().IP())

View file

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

View file

@ -6,7 +6,6 @@ import (
"fmt"
"math"
"math/big"
"sync"
"unicode/utf8"
"github.com/nspcc-dev/neo-go/pkg/core/dao"
@ -30,8 +29,9 @@ import (
type Management struct {
interop.ContractMD
NEO *NEO
}
mtx sync.RWMutex
type ManagementCache struct {
contracts map[util.Uint160]*state.Contract
// nep11 is a map of NEP11-compliant contracts which is updated with every PostPersist.
nep11 map[util.Uint160]struct{}
@ -57,6 +57,33 @@ var (
keyMinimumDeploymentFee = []byte{20}
)
var (
_ interop.Contract = (*Management)(nil)
_ dao.NativeContractCache = (*ManagementCache)(nil)
)
// Copy implements NativeContractCache interface.
func (c *ManagementCache) Copy() dao.NativeContractCache {
cp := &ManagementCache{
contracts: make(map[util.Uint160]*state.Contract),
nep11: make(map[util.Uint160]struct{}),
nep17: make(map[util.Uint160]struct{}),
}
// Copy the whole set of contracts is too expensive. We will create a separate map
// holding the same set of pointers to contracts, and in case if some contract is
// supposed to be changed, Management will create the copy in-place.
for hash, ctr := range c.contracts {
cp.contracts[hash] = ctr
}
for hash := range c.nep17 {
cp.nep17[hash] = struct{}{}
}
for hash := range c.nep11 {
cp.nep11[hash] = struct{}{}
}
return cp
}
// MakeContractKey creates a key from account script hash.
func MakeContractKey(h util.Uint160) []byte {
return makeUint160Key(prefixContract, h)
@ -66,9 +93,6 @@ func MakeContractKey(h util.Uint160) []byte {
func newManagement() *Management {
var m = &Management{
ContractMD: *interop.NewContractMD(nativenames.Management, ManagementContractID),
contracts: make(map[util.Uint160]*state.Contract),
nep11: make(map[util.Uint160]struct{}),
nep17: make(map[util.Uint160]struct{}),
}
defer m.UpdateHash()
@ -146,25 +170,12 @@ func (m *Management) getContract(ic *interop.Context, args []stackitem.Item) sta
// GetContract returns contract with given hash from given DAO.
func (m *Management) GetContract(d *dao.Simple, hash util.Uint160) (*state.Contract, error) {
m.mtx.RLock()
cs, ok := m.contracts[hash]
m.mtx.RUnlock()
cache := d.GetROCache(m.ID).(*ManagementCache)
cs, ok := cache.contracts[hash]
if !ok {
return nil, storage.ErrKeyNotFound
} else if cs != nil {
return cs, nil
}
return m.getContractFromDAO(d, hash)
}
func (m *Management) getContractFromDAO(d *dao.Simple, hash util.Uint160) (*state.Contract, error) {
contract := new(state.Contract)
key := MakeContractKey(hash)
err := getConvertibleFromDAO(m.ID, d, key, contract)
if err != nil {
return nil, err
}
return contract, nil
return cs, nil
}
func getLimitedSlice(arg stackitem.Item, max int) ([]byte, error) {
@ -260,20 +271,23 @@ func (m *Management) deployWithData(ic *interop.Context, args []stackitem.Item)
return contractToStack(newcontract)
}
func (m *Management) markUpdated(h util.Uint160) {
m.mtx.Lock()
// Just set it to nil, to refresh cache in `PostPersist`.
m.contracts[h] = nil
m.mtx.Unlock()
func (m *Management) markUpdated(d *dao.Simple, hash util.Uint160, cs *state.Contract) {
cache := d.GetRWCache(m.ID).(*ManagementCache)
delete(cache.nep11, hash)
delete(cache.nep17, hash)
if cs == nil {
delete(cache.contracts, hash)
return
}
updateContractCache(cache, cs)
}
// Deploy creates contract's hash/ID and saves new contract into the given DAO.
// It doesn't run _deploy method and doesn't emit notification.
func (m *Management) Deploy(d *dao.Simple, sender util.Uint160, neff *nef.File, manif *manifest.Manifest) (*state.Contract, error) {
h := state.CreateContractHash(sender, neff.Checksum, manif.Name)
key := MakeContractKey(h)
si := d.GetStorageItem(m.ID, key)
if si != nil {
_, err := m.GetContract(d, h)
if err == nil {
return nil, errors.New("contract already exists")
}
id, err := m.getNextContractID(d)
@ -300,7 +314,6 @@ func (m *Management) Deploy(d *dao.Simple, sender util.Uint160, neff *nef.File,
if err != nil {
return nil, err
}
m.markUpdated(newcontract.Hash)
return newcontract, nil
}
@ -340,7 +353,6 @@ func (m *Management) Update(d *dao.Simple, hash util.Uint160, neff *nef.File, ma
contract = *oldcontract // Make a copy, don't ruin (potentially) cached contract.
// if NEF was provided, update the contract script
if neff != nil {
m.markUpdated(hash)
contract.NEF = *neff
}
// if manifest was provided, update the contract manifest
@ -352,7 +364,6 @@ func (m *Management) Update(d *dao.Simple, hash util.Uint160, neff *nef.File, ma
if err != nil {
return nil, fmt.Errorf("invalid manifest: %w", err)
}
m.markUpdated(hash)
contract.Manifest = *manif
}
err = checkScriptAndMethods(contract.NEF.Script, contract.Manifest.ABI.Methods)
@ -393,7 +404,7 @@ func (m *Management) Destroy(d *dao.Simple, hash util.Uint160) error {
d.DeleteStorageItem(contract.ID, k)
return true
})
m.markUpdated(hash)
m.markUpdated(d, hash, nil)
return nil
}
@ -444,18 +455,19 @@ func (m *Management) Metadata() *interop.ContractMD {
// updateContractCache saves contract in the common and NEP-related caches. It's
// an internal method that must be called with m.mtx lock taken.
func (m *Management) updateContractCache(cs *state.Contract) {
m.contracts[cs.Hash] = cs
func updateContractCache(cache *ManagementCache, cs *state.Contract) {
cache.contracts[cs.Hash] = cs
if cs.Manifest.IsStandardSupported(manifest.NEP11StandardName) {
m.nep11[cs.Hash] = struct{}{}
cache.nep11[cs.Hash] = struct{}{}
}
if cs.Manifest.IsStandardSupported(manifest.NEP17StandardName) {
m.nep17[cs.Hash] = struct{}{}
cache.nep17[cs.Hash] = struct{}{}
}
}
// OnPersist implements Contract interface.
func (m *Management) OnPersist(ic *interop.Context) error {
var cache *ManagementCache
for _, native := range ic.Natives {
md := native.Metadata()
history := md.UpdateHistory
@ -466,16 +478,17 @@ func (m *Management) OnPersist(ic *interop.Context) error {
cs := &state.Contract{
ContractBase: md.ContractBase,
}
err := m.PutContractState(ic.DAO, cs)
if err != nil {
return err
}
if err := native.Initialize(ic); err != nil {
return fmt.Errorf("initializing %s native contract: %w", md.Name, err)
}
m.mtx.Lock()
m.updateContractCache(cs)
m.mtx.Unlock()
err := m.putContractState(ic.DAO, cs, false) // Perform cache update manually.
if err != nil {
return err
}
if cache == nil {
cache = ic.DAO.GetRWCache(m.ID).(*ManagementCache)
}
updateContractCache(cache, cs)
}
return nil
@ -485,8 +498,11 @@ func (m *Management) OnPersist(ic *interop.Context) error {
// Cache initialisation should be done apart from Initialize because Initialize is
// called only when deploying native contracts.
func (m *Management) InitializeCache(d *dao.Simple) error {
m.mtx.Lock()
defer m.mtx.Unlock()
cache := &ManagementCache{
contracts: make(map[util.Uint160]*state.Contract),
nep11: make(map[util.Uint160]struct{}),
nep17: make(map[util.Uint160]struct{}),
}
var initErr error
d.Seek(m.ID, storage.SeekRange{Prefix: []byte{prefixContract}}, func(_, v []byte) bool {
@ -495,56 +511,42 @@ func (m *Management) InitializeCache(d *dao.Simple) error {
if initErr != nil {
return false
}
m.updateContractCache(cs)
updateContractCache(cache, cs)
return true
})
return initErr
if initErr != nil {
return initErr
}
d.SetCache(m.ID, cache)
return nil
}
// PostPersist implements Contract interface.
func (m *Management) PostPersist(ic *interop.Context) error {
m.mtx.Lock()
for h, cs := range m.contracts {
if cs != nil {
continue
}
delete(m.nep11, h)
delete(m.nep17, h)
newCs, err := m.getContractFromDAO(ic.DAO, h)
if err != nil {
// Contract was destroyed.
delete(m.contracts, h)
continue
}
m.updateContractCache(newCs)
}
m.mtx.Unlock()
return nil
}
// GetNEP11Contracts returns hashes of all deployed contracts that support NEP-11 standard. The list
// is updated every PostPersist, so until PostPersist is called, the result for the previous block
// is returned.
func (m *Management) GetNEP11Contracts() []util.Uint160 {
m.mtx.RLock()
result := make([]util.Uint160, 0, len(m.nep11))
for h := range m.nep11 {
func (m *Management) GetNEP11Contracts(d *dao.Simple) []util.Uint160 {
cache := d.GetROCache(m.ID).(*ManagementCache)
result := make([]util.Uint160, 0, len(cache.nep11))
for h := range cache.nep11 {
result = append(result, h)
}
m.mtx.RUnlock()
return result
}
// GetNEP17Contracts returns hashes of all deployed contracts that support NEP-17 standard. The list
// is updated every PostPersist, so until PostPersist is called, the result for the previous block
// is returned.
func (m *Management) GetNEP17Contracts() []util.Uint160 {
m.mtx.RLock()
result := make([]util.Uint160, 0, len(m.nep17))
for h := range m.nep17 {
func (m *Management) GetNEP17Contracts(d *dao.Simple) []util.Uint160 {
cache := d.GetROCache(m.ID).(*ManagementCache)
result := make([]util.Uint160, 0, len(cache.nep17))
for h := range cache.nep17 {
result = append(result, h)
}
m.mtx.RUnlock()
return result
}
@ -552,16 +554,30 @@ func (m *Management) GetNEP17Contracts() []util.Uint160 {
func (m *Management) Initialize(ic *interop.Context) error {
setIntWithKey(m.ID, ic.DAO, keyMinimumDeploymentFee, defaultMinimumDeploymentFee)
setIntWithKey(m.ID, ic.DAO, keyNextAvailableID, 1)
cache := &ManagementCache{
contracts: make(map[util.Uint160]*state.Contract),
nep11: make(map[util.Uint160]struct{}),
nep17: make(map[util.Uint160]struct{}),
}
ic.DAO.SetCache(m.ID, cache)
return nil
}
// PutContractState saves given contract state into given DAO.
func (m *Management) PutContractState(d *dao.Simple, cs *state.Contract) error {
return m.putContractState(d, cs, true)
}
// putContractState is an internal PutContractState representation.
func (m *Management) putContractState(d *dao.Simple, cs *state.Contract, updateCache bool) error {
key := MakeContractKey(cs.Hash)
if err := putConvertibleToDAO(m.ID, d, key, cs); err != nil {
return err
}
m.markUpdated(cs.Hash)
if updateCache {
m.markUpdated(d, cs.Hash, cs)
}
if cs.UpdateCounter != 0 { // Update.
return nil
}

View file

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

View file

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

View file

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

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/state"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
"github.com/nspcc-dev/neo-go/pkg/io"
"github.com/nspcc-dev/neo-go/pkg/neotest"
"github.com/nspcc-dev/neo-go/pkg/neotest/chain"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
"github.com/nspcc-dev/neo-go/pkg/vm/emit"
"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
"github.com/stretchr/testify/require"
)
@ -77,6 +80,38 @@ func testGetSet(t *testing.T, c *neotest.ContractInvoker, name string, defaultVa
})
}
func testGetSetCache(t *testing.T, c *neotest.ContractInvoker, name string, defaultValue int64) {
getName := "get" + name
setName := "set" + name
committeeInvoker := c.WithSigners(c.Committee)
newVal := defaultValue - 1
// Change fee, abort the transaction and check that contract cache wasn't persisted
// for FAULTed tx at the same block.
w := io.NewBufBinWriter()
emit.AppCall(w.BinWriter, committeeInvoker.Hash, setName, callflag.All, newVal)
emit.Opcodes(w.BinWriter, opcode.ABORT)
tx1 := committeeInvoker.PrepareInvocation(t, w.Bytes(), committeeInvoker.Signers)
tx2 := committeeInvoker.PrepareInvoke(t, getName)
committeeInvoker.AddNewBlock(t, tx1, tx2)
committeeInvoker.CheckFault(t, tx1.Hash(), "ABORT")
committeeInvoker.CheckHalt(t, tx2.Hash(), stackitem.Make(defaultValue))
// Change fee and check that change is available for the next tx.
tx1 = committeeInvoker.PrepareInvoke(t, setName, newVal)
tx2 = committeeInvoker.PrepareInvoke(t, getName)
committeeInvoker.AddNewBlock(t, tx1, tx2)
committeeInvoker.CheckHalt(t, tx1.Hash())
if name != "GasPerBlock" {
committeeInvoker.CheckHalt(t, tx2.Hash(), stackitem.Make(newVal))
} else {
committeeInvoker.CheckHalt(t, tx2.Hash(), stackitem.Make(defaultValue))
committeeInvoker.Invoke(t, newVal, getName)
}
}
func setNodesByRole(t *testing.T, designateInvoker *neotest.ContractInvoker, ok bool, r noderoles.Role, nodes keys.PublicKeys) {
pubs := make([]interface{}, len(nodes))
for i := range nodes {

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/noderoles"
"github.com/nspcc-dev/neo-go/pkg/core/state"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
"github.com/nspcc-dev/neo-go/pkg/io"
"github.com/nspcc-dev/neo-go/pkg/neotest"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm/emit"
"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
"github.com/stretchr/testify/require"
)
@ -45,3 +52,88 @@ func TestDesignate_DesignateAsRole(t *testing.T) {
checkNodeRoles(t, designateInvoker, true, noderoles.NeoFSAlphabet, e.Chain.BlockHeight()+1, pubs)
})
}
type dummyOracle struct {
updateNodes func(k keys.PublicKeys)
}
// AddRequests processes new requests.
func (o *dummyOracle) AddRequests(map[uint64]*state.OracleRequest) {
}
// RemoveRequests removes already processed requests.
func (o *dummyOracle) RemoveRequests([]uint64) {
panic("TODO")
}
// UpdateOracleNodes updates oracle nodes.
func (o *dummyOracle) UpdateOracleNodes(k keys.PublicKeys) {
if o.updateNodes != nil {
o.updateNodes(k)
return
}
panic("TODO")
}
// UpdateNativeContract updates oracle contract native script and hash.
func (o *dummyOracle) UpdateNativeContract([]byte, []byte, util.Uint160, int) {
}
// Start runs oracle module.
func (o *dummyOracle) Start() {
panic("TODO")
}
// Shutdown shutdowns oracle module.
func (o *dummyOracle) Shutdown() {
panic("TODO")
}
func TestDesignate_Cache(t *testing.T) {
c := newDesignateClient(t)
e := c.Executor
designateInvoker := c.WithSigners(c.Committee)
r := int64(noderoles.Oracle)
var (
updatedNodes keys.PublicKeys
updateCalled bool
)
oracleServ := &dummyOracle{
updateNodes: func(k keys.PublicKeys) {
updatedNodes = k
updateCalled = true
},
}
privGood, err := keys.NewPrivateKey()
require.NoError(t, err)
pubsGood := []interface{}{privGood.PublicKey().Bytes()}
privBad, err := keys.NewPrivateKey()
require.NoError(t, err)
pubsBad := []interface{}{privBad.PublicKey().Bytes()}
// Firstly, designate good Oracle node and check that OracleService callback was called during PostPersist.
e.Chain.SetOracle(oracleServ)
txDesignateGood := designateInvoker.PrepareInvoke(t, "designateAsRole", r, pubsGood)
e.AddNewBlock(t, txDesignateGood)
e.CheckHalt(t, txDesignateGood.Hash(), stackitem.Null{})
require.True(t, updateCalled)
require.Equal(t, keys.PublicKeys{privGood.PublicKey()}, updatedNodes)
updatedNodes = nil
updateCalled = false
// Check designated node in a separate block.
checkNodeRoles(t, designateInvoker, true, noderoles.Oracle, e.Chain.BlockHeight()+1, keys.PublicKeys{privGood.PublicKey()})
// Designate privBad as oracle node and abort the transaction. Designation cache changes
// shouldn't be persisted to the contract and no notification should be sent.
w := io.NewBufBinWriter()
emit.AppCall(w.BinWriter, designateInvoker.Hash, "designateAsRole", callflag.All, int64(r), pubsBad)
emit.Opcodes(w.BinWriter, opcode.ABORT)
require.NoError(t, w.Err)
script := w.Bytes()
designateInvoker.InvokeScriptCheckFAULT(t, script, designateInvoker.Signers, "ABORT")
require.Nil(t, updatedNodes)
require.False(t, updateCalled)
}

View file

@ -20,6 +20,8 @@ import (
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/nef"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger"
"github.com/nspcc-dev/neo-go/pkg/vm"
"github.com/nspcc-dev/neo-go/pkg/vm/emit"
"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
@ -34,6 +36,43 @@ func TestManagement_MinimumDeploymentFee(t *testing.T) {
testGetSet(t, newManagementClient(t), "MinimumDeploymentFee", 10_00000000, 0, 0)
}
func TestManagement_MinimumDeploymentFeeCache(t *testing.T) {
c := newManagementClient(t)
testGetSetCache(t, c, "MinimumDeploymentFee", 10_00000000)
}
func TestManagement_ContractCache(t *testing.T) {
c := newManagementClient(t)
managementInvoker := c.WithSigners(c.Committee)
cs1, _ := contracts.GetTestContractState(t, pathToInternalContracts, 1, 2, c.Committee.ScriptHash())
manifestBytes, err := json.Marshal(cs1.Manifest)
require.NoError(t, err)
nefBytes, err := cs1.NEF.Bytes()
require.NoError(t, err)
// Deploy contract, abort the transaction and check that Management cache wasn't persisted
// for FAULTed tx at the same block.
w := io.NewBufBinWriter()
emit.AppCall(w.BinWriter, managementInvoker.Hash, "deploy", callflag.All, nefBytes, manifestBytes)
emit.Opcodes(w.BinWriter, opcode.ABORT)
tx1 := managementInvoker.PrepareInvocation(t, w.Bytes(), managementInvoker.Signers)
tx2 := managementInvoker.PrepareInvoke(t, "getContract", cs1.Hash.BytesBE())
managementInvoker.AddNewBlock(t, tx1, tx2)
managementInvoker.CheckFault(t, tx1.Hash(), "ABORT")
managementInvoker.CheckHalt(t, tx2.Hash(), stackitem.Null{})
// Deploy the contract and check that cache was persisted for HALTed transaction at the same block.
tx1 = managementInvoker.PrepareInvoke(t, "deploy", nefBytes, manifestBytes)
tx2 = managementInvoker.PrepareInvoke(t, "getContract", cs1.Hash.BytesBE())
managementInvoker.AddNewBlock(t, tx1, tx2)
managementInvoker.CheckHalt(t, tx1.Hash())
aer, err := managementInvoker.Chain.GetAppExecResults(tx2.Hash(), trigger.Application)
require.NoError(t, err)
require.Equal(t, vm.HaltState, aer[0].VMState, aer[0].FaultException)
require.NotEqual(t, stackitem.Null{}, aer[0].Stack)
}
func TestManagement_ContractDeploy(t *testing.T) {
c := newManagementClient(t)
managementInvoker := c.WithSigners(c.Committee)

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

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

View file

@ -28,10 +28,14 @@ func newOracleClient(t *testing.T) *neotest.ContractInvoker {
return newNativeClient(t, nativenames.Oracle)
}
func TestGetSetPrice(t *testing.T) {
func TestOracle_GetSetPrice(t *testing.T) {
testGetSet(t, newOracleClient(t), "Price", native.DefaultOracleRequestPrice, 1, math.MaxInt64)
}
func TestOracle_GetSetPriceCache(t *testing.T) {
testGetSetCache(t, newOracleClient(t), "Price", native.DefaultOracleRequestPrice)
}
func putOracleRequest(t *testing.T, oracleInvoker *neotest.ContractInvoker,
url string, filter *string, cb string, userData []byte, gas int64, errStr ...string) {
var filtItem interface{}

View file

@ -19,14 +19,26 @@ func TestPolicy_FeePerByte(t *testing.T) {
testGetSet(t, newPolicyClient(t), "FeePerByte", 1000, 0, 100_000_000)
}
func TestPolicy_FeePerByteCache(t *testing.T) {
testGetSetCache(t, newPolicyClient(t), "FeePerByte", 1000)
}
func TestPolicy_ExecFeeFactor(t *testing.T) {
testGetSet(t, newPolicyClient(t), "ExecFeeFactor", interop.DefaultBaseExecFee, 1, 1000)
}
func TestPolicy_ExecFeeFactorCache(t *testing.T) {
testGetSetCache(t, newPolicyClient(t), "ExecFeeFactor", interop.DefaultBaseExecFee)
}
func TestPolicy_StoragePrice(t *testing.T) {
testGetSet(t, newPolicyClient(t), "StoragePrice", native.DefaultStoragePrice, 1, 10000000)
}
func TestPolicy_StoragePriceCache(t *testing.T) {
testGetSetCache(t, newPolicyClient(t), "StoragePrice", native.DefaultStoragePrice)
}
func TestPolicy_BlockedAccounts(t *testing.T) {
c := newPolicyClient(t)
e := c.Executor

View file

@ -5,7 +5,6 @@ import (
"fmt"
"math"
"math/big"
"sync"
"github.com/nspcc-dev/neo-go/pkg/core/dao"
"github.com/nspcc-dev/neo-go/pkg/core/interop"
@ -32,12 +31,9 @@ type Notary struct {
GAS *GAS
NEO *NEO
Desig *Designate
}
lock sync.RWMutex
// isValid defies whether cached values were changed during the current
// consensus iteration. If false, these values will be updated after
// blockchain DAO persisting. If true, we can safely use cached values.
isValid bool
type NotaryCache struct {
maxNotValidBeforeDelta uint32
notaryServiceFeePerKey int64
}
@ -56,6 +52,22 @@ var (
notaryServiceFeeKey = []byte{5}
)
var (
_ interop.Contract = (*Notary)(nil)
_ dao.NativeContractCache = (*NotaryCache)(nil)
)
// Copy implements NativeContractCache interface.
func (c *NotaryCache) Copy() dao.NativeContractCache {
cp := &NotaryCache{}
copyNotaryCache(c, cp)
return cp
}
func copyNotaryCache(src, dst *NotaryCache) {
*dst = *src
}
// newNotary returns Notary native contract.
func newNotary() *Notary {
n := &Notary{ContractMD: *interop.NewContractMD(nativenames.Notary, notaryContractID)}
@ -125,9 +137,22 @@ func (n *Notary) Metadata() *interop.ContractMD {
func (n *Notary) Initialize(ic *interop.Context) error {
setIntWithKey(n.ID, ic.DAO, maxNotValidBeforeDeltaKey, defaultMaxNotValidBeforeDelta)
setIntWithKey(n.ID, ic.DAO, notaryServiceFeeKey, defaultNotaryServiceFeePerKey)
n.isValid = true
n.maxNotValidBeforeDelta = defaultMaxNotValidBeforeDelta
n.notaryServiceFeePerKey = defaultNotaryServiceFeePerKey
cache := &NotaryCache{
maxNotValidBeforeDelta: defaultMaxNotValidBeforeDelta,
notaryServiceFeePerKey: defaultNotaryServiceFeePerKey,
}
ic.DAO.SetCache(n.ID, cache)
return nil
}
func (n *Notary) InitializeCache(d *dao.Simple) error {
cache := &NotaryCache{
maxNotValidBeforeDelta: uint32(getIntWithKey(n.ID, d, maxNotValidBeforeDeltaKey)),
notaryServiceFeePerKey: getIntWithKey(n.ID, d, notaryServiceFeeKey),
}
d.SetCache(n.ID, cache)
return nil
}
@ -176,15 +201,6 @@ func (n *Notary) OnPersist(ic *interop.Context) error {
// PostPersist implements Contract interface.
func (n *Notary) PostPersist(ic *interop.Context) error {
n.lock.Lock()
defer n.lock.Unlock()
if n.isValid {
return nil
}
n.maxNotValidBeforeDelta = uint32(getIntWithKey(n.ID, ic.DAO, maxNotValidBeforeDeltaKey))
n.notaryServiceFeePerKey = getIntWithKey(n.ID, ic.DAO, notaryServiceFeeKey)
n.isValid = true
return nil
}
@ -207,7 +223,7 @@ func (n *Notary) onPayment(ic *interop.Context, args []stackitem.Item) stackitem
}
allowedChangeTill := ic.Tx.Sender() == to
currentHeight := ic.Chain.BlockHeight()
currentHeight := ic.BlockHeight()
deposit := n.GetDepositFor(ic.DAO, to)
till := toUint32(additionalParams[1])
if till < currentHeight {
@ -250,7 +266,7 @@ func (n *Notary) lockDepositUntil(ic *interop.Context, args []stackitem.Item) st
return stackitem.NewBool(false)
}
till := toUint32(args[1])
if till < ic.Chain.BlockHeight() {
if till < ic.BlockHeight() {
return stackitem.NewBool(false)
}
deposit := n.GetDepositFor(ic.DAO, addr)
@ -286,7 +302,7 @@ func (n *Notary) withdraw(ic *interop.Context, args []stackitem.Item) stackitem.
if deposit == nil {
return stackitem.NewBool(false)
}
if ic.Chain.BlockHeight() < deposit.Till {
if ic.BlockHeight() < deposit.Till {
return stackitem.NewBool(false)
}
cs, err := ic.GetContract(n.GAS.Hash)
@ -391,12 +407,8 @@ func (n *Notary) getMaxNotValidBeforeDelta(ic *interop.Context, _ []stackitem.It
// GetMaxNotValidBeforeDelta is an internal representation of Notary getMaxNotValidBeforeDelta method.
func (n *Notary) GetMaxNotValidBeforeDelta(dao *dao.Simple) uint32 {
n.lock.RLock()
defer n.lock.RUnlock()
if n.isValid {
return n.maxNotValidBeforeDelta
}
return uint32(getIntWithKey(n.ID, dao, maxNotValidBeforeDeltaKey))
cache := dao.GetROCache(n.ID).(*NotaryCache)
return cache.maxNotValidBeforeDelta
}
// setMaxNotValidBeforeDelta is Notary contract method and sets the maximum NotValidBefore delta.
@ -404,16 +416,15 @@ func (n *Notary) setMaxNotValidBeforeDelta(ic *interop.Context, args []stackitem
value := toUint32(args[0])
cfg := ic.Chain.GetConfig()
maxInc := cfg.MaxValidUntilBlockIncrement
if value > maxInc/2 || value < uint32(cfg.GetNumOfCNs(ic.Chain.BlockHeight())) {
panic(fmt.Errorf("MaxNotValidBeforeDelta cannot be more than %d or less than %d", maxInc/2, cfg.GetNumOfCNs(ic.Chain.BlockHeight())))
if value > maxInc/2 || value < uint32(cfg.GetNumOfCNs(ic.BlockHeight())) {
panic(fmt.Errorf("MaxNotValidBeforeDelta cannot be more than %d or less than %d", maxInc/2, cfg.GetNumOfCNs(ic.BlockHeight())))
}
if !n.NEO.checkCommittee(ic) {
panic("invalid committee signature")
}
n.lock.Lock()
defer n.lock.Unlock()
setIntWithKey(n.ID, ic.DAO, maxNotValidBeforeDeltaKey, int64(value))
n.isValid = false
cache := ic.DAO.GetRWCache(n.ID).(*NotaryCache)
cache.maxNotValidBeforeDelta = value
return stackitem.Null{}
}
@ -424,12 +435,8 @@ func (n *Notary) getNotaryServiceFeePerKey(ic *interop.Context, _ []stackitem.It
// GetNotaryServiceFeePerKey is an internal representation of Notary getNotaryServiceFeePerKey method.
func (n *Notary) GetNotaryServiceFeePerKey(dao *dao.Simple) int64 {
n.lock.RLock()
defer n.lock.RUnlock()
if n.isValid {
return n.notaryServiceFeePerKey
}
return getIntWithKey(n.ID, dao, notaryServiceFeeKey)
cache := dao.GetROCache(n.ID).(*NotaryCache)
return cache.notaryServiceFeePerKey
}
// setNotaryServiceFeePerKey is Notary contract method and sets a reward per notary request key for notary nodes.
@ -441,10 +448,9 @@ func (n *Notary) setNotaryServiceFeePerKey(ic *interop.Context, args []stackitem
if !n.NEO.checkCommittee(ic) {
panic("invalid committee signature")
}
n.lock.Lock()
defer n.lock.Unlock()
setIntWithKey(n.ID, ic.DAO, notaryServiceFeeKey, int64(value))
n.isValid = false
cache := ic.DAO.GetRWCache(n.ID).(*NotaryCache)
cache.notaryServiceFeePerKey = value
return stackitem.Null{}
}

View file

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

View file

@ -4,7 +4,6 @@ import (
"fmt"
"math/big"
"sort"
"sync"
"github.com/nspcc-dev/neo-go/pkg/core/dao"
"github.com/nspcc-dev/neo-go/pkg/core/interop"
@ -52,12 +51,10 @@ var (
// Policy represents Policy native contract.
type Policy struct {
interop.ContractMD
NEO *NEO
lock sync.RWMutex
// isValid defies whether cached values were changed during the current
// consensus iteration. If false, these values will be updated after
// blockchain DAO persisting. If true, we can safely use cached values.
isValid bool
NEO *NEO
}
type PolicyCache struct {
execFeeFactor uint32
feePerByte int64
maxVerificationGas int64
@ -65,7 +62,23 @@ type Policy struct {
blockedAccounts []util.Uint160
}
var _ interop.Contract = (*Policy)(nil)
var (
_ interop.Contract = (*Policy)(nil)
_ dao.NativeContractCache = (*PolicyCache)(nil)
)
// Copy implements NativeContractCache interface.
func (c *PolicyCache) Copy() dao.NativeContractCache {
cp := &PolicyCache{}
copyPolicyCache(c, cp)
return cp
}
func copyPolicyCache(src, dst *PolicyCache) {
*dst = *src
dst.blockedAccounts = make([]util.Uint160, len(src.blockedAccounts))
copy(dst.blockedAccounts, src.blockedAccounts)
}
// newPolicy returns Policy native contract.
func newPolicy() *Policy {
@ -128,16 +141,51 @@ func (p *Policy) Initialize(ic *interop.Context) error {
setIntWithKey(p.ID, ic.DAO, execFeeFactorKey, defaultExecFeeFactor)
setIntWithKey(p.ID, ic.DAO, storagePriceKey, DefaultStoragePrice)
p.isValid = true
p.execFeeFactor = defaultExecFeeFactor
p.feePerByte = defaultFeePerByte
p.maxVerificationGas = defaultMaxVerificationGas
p.storagePrice = DefaultStoragePrice
p.blockedAccounts = make([]util.Uint160, 0)
cache := &PolicyCache{
execFeeFactor: defaultExecFeeFactor,
feePerByte: defaultFeePerByte,
maxVerificationGas: defaultMaxVerificationGas,
storagePrice: DefaultStoragePrice,
blockedAccounts: make([]util.Uint160, 0),
}
ic.DAO.SetCache(p.ID, cache)
return nil
}
func (p *Policy) InitializeCache(d *dao.Simple) error {
cache := &PolicyCache{}
err := p.fillCacheFromDAO(cache, d)
if err != nil {
return err
}
d.SetCache(p.ID, cache)
return nil
}
func (p *Policy) fillCacheFromDAO(cache *PolicyCache, d *dao.Simple) error {
cache.execFeeFactor = uint32(getIntWithKey(p.ID, d, execFeeFactorKey))
cache.feePerByte = getIntWithKey(p.ID, d, feePerByteKey)
cache.maxVerificationGas = defaultMaxVerificationGas
cache.storagePrice = uint32(getIntWithKey(p.ID, d, storagePriceKey))
cache.blockedAccounts = make([]util.Uint160, 0)
var fErr error
d.Seek(p.ID, storage.SeekRange{Prefix: []byte{blockedAccountPrefix}}, func(k, _ []byte) bool {
hash, err := util.Uint160DecodeBytesBE(k)
if err != nil {
fErr = fmt.Errorf("failed to decode blocked account hash: %w", err)
return false
}
cache.blockedAccounts = append(cache.blockedAccounts, hash)
return true
})
if fErr != nil {
return fmt.Errorf("failed to initialize blocked accounts: %w", fErr)
}
return nil
}
// OnPersist implements Contract interface.
func (p *Policy) OnPersist(ic *interop.Context) error {
return nil
@ -145,32 +193,7 @@ func (p *Policy) OnPersist(ic *interop.Context) error {
// PostPersist implements Contract interface.
func (p *Policy) PostPersist(ic *interop.Context) error {
p.lock.Lock()
defer p.lock.Unlock()
if p.isValid {
return nil
}
p.execFeeFactor = uint32(getIntWithKey(p.ID, ic.DAO, execFeeFactorKey))
p.feePerByte = getIntWithKey(p.ID, ic.DAO, feePerByteKey)
p.maxVerificationGas = defaultMaxVerificationGas
p.storagePrice = uint32(getIntWithKey(p.ID, ic.DAO, storagePriceKey))
p.blockedAccounts = make([]util.Uint160, 0)
var fErr error
ic.DAO.Seek(p.ID, storage.SeekRange{Prefix: []byte{blockedAccountPrefix}}, func(k, _ []byte) bool {
hash, err := util.Uint160DecodeBytesBE(k)
if err != nil {
fErr = fmt.Errorf("failed to decode blocked account hash: %w", err)
return false
}
p.blockedAccounts = append(p.blockedAccounts, hash)
return true
})
if fErr == nil {
p.isValid = true
}
return fErr
return nil
}
// getFeePerByte is Policy contract method and returns required transaction's fee
@ -181,20 +204,14 @@ func (p *Policy) getFeePerByte(ic *interop.Context, _ []stackitem.Item) stackite
// GetFeePerByteInternal returns required transaction's fee per byte.
func (p *Policy) GetFeePerByteInternal(dao *dao.Simple) int64 {
p.lock.RLock()
defer p.lock.RUnlock()
if p.isValid {
return p.feePerByte
}
return getIntWithKey(p.ID, dao, feePerByteKey)
cache := dao.GetROCache(p.ID).(*PolicyCache)
return cache.feePerByte
}
// GetMaxVerificationGas returns maximum gas allowed to be burned during verificaion.
func (p *Policy) GetMaxVerificationGas(_ *dao.Simple) int64 {
if p.isValid {
return p.maxVerificationGas
}
return defaultMaxVerificationGas
func (p *Policy) GetMaxVerificationGas(dao *dao.Simple) int64 {
cache := dao.GetROCache(p.ID).(*PolicyCache)
return cache.maxVerificationGas
}
func (p *Policy) getExecFeeFactor(ic *interop.Context, _ []stackitem.Item) stackitem.Item {
@ -203,12 +220,8 @@ func (p *Policy) getExecFeeFactor(ic *interop.Context, _ []stackitem.Item) stack
// GetExecFeeFactorInternal returns current execution fee factor.
func (p *Policy) GetExecFeeFactorInternal(d *dao.Simple) int64 {
p.lock.RLock()
defer p.lock.RUnlock()
if p.isValid {
return int64(p.execFeeFactor)
}
return getIntWithKey(p.ID, d, execFeeFactorKey)
cache := d.GetROCache(p.ID).(*PolicyCache)
return int64(cache.execFeeFactor)
}
func (p *Policy) setExecFeeFactor(ic *interop.Context, args []stackitem.Item) stackitem.Item {
@ -219,35 +232,38 @@ func (p *Policy) setExecFeeFactor(ic *interop.Context, args []stackitem.Item) st
if !p.NEO.checkCommittee(ic) {
panic("invalid committee signature")
}
p.lock.Lock()
defer p.lock.Unlock()
setIntWithKey(p.ID, ic.DAO, execFeeFactorKey, int64(value))
p.isValid = false
cache := ic.DAO.GetRWCache(p.ID).(*PolicyCache)
cache.execFeeFactor = value
return stackitem.Null{}
}
// isBlocked is Policy contract method and checks whether provided account is blocked.
func (p *Policy) isBlocked(ic *interop.Context, args []stackitem.Item) stackitem.Item {
hash := toUint160(args[0])
return stackitem.NewBool(p.IsBlockedInternal(ic.DAO, hash))
_, blocked := p.isBlockedInternal(ic.DAO, hash)
return stackitem.NewBool(blocked)
}
// IsBlockedInternal checks whether provided account is blocked.
func (p *Policy) IsBlockedInternal(dao *dao.Simple, hash util.Uint160) bool {
p.lock.RLock()
defer p.lock.RUnlock()
if p.isValid {
length := len(p.blockedAccounts)
i := sort.Search(length, func(i int) bool {
return !p.blockedAccounts[i].Less(hash)
})
if length != 0 && i != length && p.blockedAccounts[i].Equals(hash) {
return true
}
return false
// IsBlocked checks whether provided account is blocked.
func (p *Policy) IsBlocked(dao *dao.Simple, hash util.Uint160) bool {
_, isBlocked := p.isBlockedInternal(dao, hash)
return isBlocked
}
// isBlockedInternal checks whether provided account is blocked. It returns position
// of the blocked account in the blocked accounts list (or the position it should be
// put at).
func (p *Policy) isBlockedInternal(dao *dao.Simple, hash util.Uint160) (int, bool) {
cache := dao.GetROCache(p.ID).(*PolicyCache)
length := len(cache.blockedAccounts)
i := sort.Search(length, func(i int) bool {
return !cache.blockedAccounts[i].Less(hash)
})
if length != 0 && i != length && cache.blockedAccounts[i].Equals(hash) {
return i, true
}
key := append([]byte{blockedAccountPrefix}, hash.BytesBE()...)
return dao.GetStorageItem(p.ID, key) != nil
return i, false
}
func (p *Policy) getStoragePrice(ic *interop.Context, _ []stackitem.Item) stackitem.Item {
@ -256,12 +272,8 @@ func (p *Policy) getStoragePrice(ic *interop.Context, _ []stackitem.Item) stacki
// GetStoragePriceInternal returns current execution fee factor.
func (p *Policy) GetStoragePriceInternal(d *dao.Simple) int64 {
p.lock.RLock()
defer p.lock.RUnlock()
if p.isValid {
return int64(p.storagePrice)
}
return getIntWithKey(p.ID, d, storagePriceKey)
cache := d.GetROCache(p.ID).(*PolicyCache)
return int64(cache.storagePrice)
}
func (p *Policy) setStoragePrice(ic *interop.Context, args []stackitem.Item) stackitem.Item {
@ -272,10 +284,9 @@ func (p *Policy) setStoragePrice(ic *interop.Context, args []stackitem.Item) sta
if !p.NEO.checkCommittee(ic) {
panic("invalid committee signature")
}
p.lock.Lock()
defer p.lock.Unlock()
setIntWithKey(p.ID, ic.DAO, storagePriceKey, int64(value))
p.isValid = false
cache := ic.DAO.GetRWCache(p.ID).(*PolicyCache)
cache.storagePrice = value
return stackitem.Null{}
}
@ -288,10 +299,9 @@ func (p *Policy) setFeePerByte(ic *interop.Context, args []stackitem.Item) stack
if !p.NEO.checkCommittee(ic) {
panic("invalid committee signature")
}
p.lock.Lock()
defer p.lock.Unlock()
setIntWithKey(p.ID, ic.DAO, feePerByteKey, value)
p.isValid = false
cache := ic.DAO.GetRWCache(p.ID).(*PolicyCache)
cache.feePerByte = value
return stackitem.Null{}
}
@ -307,14 +317,19 @@ func (p *Policy) blockAccount(ic *interop.Context, args []stackitem.Item) stacki
panic("cannot block native contract")
}
}
if p.IsBlockedInternal(ic.DAO, hash) {
i, blocked := p.isBlockedInternal(ic.DAO, hash)
if blocked {
return stackitem.NewBool(false)
}
key := append([]byte{blockedAccountPrefix}, hash.BytesBE()...)
p.lock.Lock()
defer p.lock.Unlock()
ic.DAO.PutStorageItem(p.ID, key, state.StorageItem{})
p.isValid = false
cache := ic.DAO.GetRWCache(p.ID).(*PolicyCache)
if len(cache.blockedAccounts) == i {
cache.blockedAccounts = append(cache.blockedAccounts, hash)
} else {
cache.blockedAccounts = append(cache.blockedAccounts[:i+1], cache.blockedAccounts[i:]...)
cache.blockedAccounts[i] = hash
}
return stackitem.NewBool(true)
}
@ -325,14 +340,14 @@ func (p *Policy) unblockAccount(ic *interop.Context, args []stackitem.Item) stac
panic("invalid committee signature")
}
hash := toUint160(args[0])
if !p.IsBlockedInternal(ic.DAO, hash) {
i, blocked := p.isBlockedInternal(ic.DAO, hash)
if !blocked {
return stackitem.NewBool(false)
}
key := append([]byte{blockedAccountPrefix}, hash.BytesBE()...)
p.lock.Lock()
defer p.lock.Unlock()
ic.DAO.DeleteStorageItem(p.ID, key)
p.isValid = false
cache := ic.DAO.GetRWCache(p.ID).(*PolicyCache)
cache.blockedAccounts = append(cache.blockedAccounts[:i], cache.blockedAccounts[i+1:]...)
return stackitem.NewBool(true)
}
@ -341,7 +356,7 @@ func (p *Policy) unblockAccount(ic *interop.Context, args []stackitem.Item) stac
// fee limit.
func (p *Policy) CheckPolicy(d *dao.Simple, tx *transaction.Transaction) error {
for _, signer := range tx.Signers {
if p.IsBlockedInternal(d, signer.Account) {
if _, isBlocked := p.isBlockedInternal(d, signer.Account); isBlocked {
return fmt.Errorf("account %s is blocked", signer.Account.StringLE())
}
}

View file

@ -1,6 +1,7 @@
package stateroot
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
@ -101,6 +102,34 @@ func (s *Module) GetStateRoot(height uint32) (*state.MPTRoot, error) {
return s.getStateRoot(makeStateRootKey(height))
}
// GetLatestStateHeight returns the latest blockchain height by the given stateroot.
func (s *Module) GetLatestStateHeight(root util.Uint256) (uint32, error) {
rootBytes := root.BytesBE()
rootStartOffset := 1 + 4 // stateroot version (1 byte) + stateroot index (4 bytes)
rootEndOffset := rootStartOffset + util.Uint256Size
var (
h uint32
found bool
rootKey = makeStateRootKey(s.localHeight.Load())
)
s.Store.Seek(storage.SeekRange{
Prefix: []byte{rootKey[0]}, // DataMPTAux
Start: rootKey[1:], // Start is a value that should be appended to the Prefix
Backwards: true,
}, func(k, v []byte) bool {
if len(k) == 5 && bytes.Equal(v[rootStartOffset:rootEndOffset], rootBytes) {
h = binary.BigEndian.Uint32(k[1:]) // cut prefix DataMPTAux
found = true
return false
}
return true
})
if found {
return h, nil
}
return h, storage.ErrKeyNotFound
}
// CurrentLocalStateRoot returns hash of the local state root.
func (s *Module) CurrentLocalStateRoot() util.Uint256 {
return s.currentLocal.Load().(util.Uint256)

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, 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 {
s.mut.Unlock()
}
err = tempstore.ps.PutChangeSet(tempstore.mem, tempstore.stor)
if !isSync {

View file

@ -588,6 +588,33 @@ func (c *Client) InvokeScript(script []byte, signers []transaction.Signer) (*res
return c.invokeSomething("invokescript", p, signers)
}
// InvokeScriptAtHeight returns the result of the given script after running it
// true the VM using the provided chain state retrieved from the specified chain
// height.
// NOTE: This is a test invoke and will not affect the blockchain.
func (c *Client) InvokeScriptAtHeight(height uint32, script []byte, signers []transaction.Signer) (*result.Invoke, error) {
var p = request.NewRawParams(height, script)
return c.invokeSomething("invokescripthistoric", p, signers)
}
// InvokeScriptAtBlock returns the result of the given script after running it
// true the VM using the provided chain state retrieved from the specified block
// hash.
// NOTE: This is a test invoke and will not affect the blockchain.
func (c *Client) InvokeScriptAtBlock(blockHash util.Uint256, script []byte, signers []transaction.Signer) (*result.Invoke, error) {
var p = request.NewRawParams(blockHash.StringLE(), script)
return c.invokeSomething("invokescripthistoric", p, signers)
}
// InvokeScriptWithState returns the result of the given script after running it
// true the VM using the provided chain state retrieved from the specified
// stateroot hash.
// NOTE: This is a test invoke and will not affect the blockchain.
func (c *Client) InvokeScriptWithState(stateroot util.Uint256, script []byte, signers []transaction.Signer) (*result.Invoke, error) {
var p = request.NewRawParams(stateroot.StringLE(), script)
return c.invokeSomething("invokescripthistoric", p, signers)
}
// InvokeFunction returns the results after calling the smart contract scripthash
// with the given operation and parameters.
// NOTE: this is test invoke and will not affect the blockchain.
@ -596,6 +623,33 @@ func (c *Client) InvokeFunction(contract util.Uint160, operation string, params
return c.invokeSomething("invokefunction", p, signers)
}
// InvokeFunctionAtHeight returns the results after calling the smart contract
// with the given operation and parameters at the given blockchain state
// specified by the blockchain height.
// NOTE: this is test invoke and will not affect the blockchain.
func (c *Client) InvokeFunctionAtHeight(height uint32, contract util.Uint160, operation string, params []smartcontract.Parameter, signers []transaction.Signer) (*result.Invoke, error) {
var p = request.NewRawParams(height, contract.StringLE(), operation, params)
return c.invokeSomething("invokefunctionhistoric", p, signers)
}
// InvokeFunctionAtBlock returns the results after calling the smart contract
// with the given operation and parameters at given the blockchain state
// specified by the block hash.
// NOTE: this is test invoke and will not affect the blockchain.
func (c *Client) InvokeFunctionAtBlock(blockHash util.Uint256, contract util.Uint160, operation string, params []smartcontract.Parameter, signers []transaction.Signer) (*result.Invoke, error) {
var p = request.NewRawParams(blockHash.StringLE(), contract.StringLE(), operation, params)
return c.invokeSomething("invokefunctionhistoric", p, signers)
}
// InvokeFunctionWithState returns the results after calling the smart contract
// with the given operation and parameters at the given blockchain state defined
// by the specified stateroot hash.
// NOTE: this is test invoke and will not affect the blockchain.
func (c *Client) InvokeFunctionWithState(stateroot util.Uint256, contract util.Uint160, operation string, params []smartcontract.Parameter, signers []transaction.Signer) (*result.Invoke, error) {
var p = request.NewRawParams(stateroot.StringLE(), contract.StringLE(), operation, params)
return c.invokeSomething("invokefunctionhistoric", p, signers)
}
// InvokeContractVerify returns the results after calling `verify` method of the smart contract
// with the given parameters under verification trigger type.
// NOTE: this is test invoke and will not affect the blockchain.
@ -604,6 +658,33 @@ func (c *Client) InvokeContractVerify(contract util.Uint160, params []smartcontr
return c.invokeSomething("invokecontractverify", p, signers, witnesses...)
}
// InvokeContractVerifyAtHeight returns the results after calling `verify` method
// of the smart contract with the given parameters under verification trigger type
// at the blockchain state specified by the blockchain height.
// NOTE: this is test invoke and will not affect the blockchain.
func (c *Client) InvokeContractVerifyAtHeight(height uint32, contract util.Uint160, params []smartcontract.Parameter, signers []transaction.Signer, witnesses ...transaction.Witness) (*result.Invoke, error) {
var p = request.NewRawParams(height, contract.StringLE(), params)
return c.invokeSomething("invokecontractverifyhistoric", p, signers, witnesses...)
}
// InvokeContractVerifyAtBlock returns the results after calling `verify` method
// of the smart contract with the given parameters under verification trigger type
// at the blockchain state specified by the block hash.
// NOTE: this is test invoke and will not affect the blockchain.
func (c *Client) InvokeContractVerifyAtBlock(blockHash util.Uint256, contract util.Uint160, params []smartcontract.Parameter, signers []transaction.Signer, witnesses ...transaction.Witness) (*result.Invoke, error) {
var p = request.NewRawParams(blockHash.StringLE(), contract.StringLE(), params)
return c.invokeSomething("invokecontractverifyhistoric", p, signers, witnesses...)
}
// InvokeContractVerifyWithState returns the results after calling `verify` method
// of the smart contract with the given parameters under verification trigger type
// at the blockchain state specified by the stateroot hash.
// NOTE: this is test invoke and will not affect the blockchain.
func (c *Client) InvokeContractVerifyWithState(stateroot util.Uint256, contract util.Uint160, params []smartcontract.Parameter, signers []transaction.Signer, witnesses ...transaction.Witness) (*result.Invoke, error) {
var p = request.NewRawParams(stateroot.StringLE(), contract.StringLE(), params)
return c.invokeSomething("invokecontractverifyhistoric", p, signers, witnesses...)
}
// invokeSomething is an inner wrapper for Invoke* functions.
func (c *Client) invokeSomething(method string, p request.RawParams, signers []transaction.Signer, witnesses ...transaction.Witness) (*result.Invoke, error) {
var resp = new(result.Invoke)

View file

@ -4,9 +4,11 @@ import (
"context"
"encoding/base64"
"encoding/hex"
"strings"
"testing"
"github.com/nspcc-dev/neo-go/internal/testchain"
"github.com/nspcc-dev/neo-go/pkg/core"
"github.com/nspcc-dev/neo-go/pkg/core/fee"
"github.com/nspcc-dev/neo-go/pkg/core/native/nativenames"
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
@ -766,6 +768,56 @@ func TestInvokeVerify(t *testing.T) {
require.True(t, res.Stack[0].Value().(bool))
})
t.Run("positive, historic, by height, with signer", func(t *testing.T) {
h := chain.BlockHeight() - 1
res, err := c.InvokeContractVerifyAtHeight(h, contract, smartcontract.Params{}, []transaction.Signer{{Account: testchain.PrivateKeyByID(0).PublicKey().GetScriptHash()}})
require.NoError(t, err)
require.Equal(t, "HALT", res.State)
require.Equal(t, 1, len(res.Stack))
require.True(t, res.Stack[0].Value().(bool))
})
t.Run("positive, historic, by block, with signer", func(t *testing.T) {
res, err := c.InvokeContractVerifyAtBlock(chain.GetHeaderHash(int(chain.BlockHeight())-1), contract, smartcontract.Params{}, []transaction.Signer{{Account: testchain.PrivateKeyByID(0).PublicKey().GetScriptHash()}})
require.NoError(t, err)
require.Equal(t, "HALT", res.State)
require.Equal(t, 1, len(res.Stack))
require.True(t, res.Stack[0].Value().(bool))
})
t.Run("positive, historic, by stateroot, with signer", func(t *testing.T) {
h := chain.BlockHeight() - 1
sr, err := chain.GetStateModule().GetStateRoot(h)
require.NoError(t, err)
res, err := c.InvokeContractVerifyWithState(sr.Root, contract, smartcontract.Params{}, []transaction.Signer{{Account: testchain.PrivateKeyByID(0).PublicKey().GetScriptHash()}})
require.NoError(t, err)
require.Equal(t, "HALT", res.State)
require.Equal(t, 1, len(res.Stack))
require.True(t, res.Stack[0].Value().(bool))
})
t.Run("bad, historic, by hash: contract not found", func(t *testing.T) {
var h uint32 = 1
_, err = c.InvokeContractVerifyAtHeight(h, contract, smartcontract.Params{}, []transaction.Signer{{Account: testchain.PrivateKeyByID(0).PublicKey().GetScriptHash()}})
require.Error(t, err)
require.True(t, strings.Contains(err.Error(), core.ErrUnknownVerificationContract.Error())) // contract wasn't deployed at block #1 yet
})
t.Run("bad, historic, by block: contract not found", func(t *testing.T) {
_, err = c.InvokeContractVerifyAtBlock(chain.GetHeaderHash(1), contract, smartcontract.Params{}, []transaction.Signer{{Account: testchain.PrivateKeyByID(0).PublicKey().GetScriptHash()}})
require.Error(t, err)
require.True(t, strings.Contains(err.Error(), core.ErrUnknownVerificationContract.Error())) // contract wasn't deployed at block #1 yet
})
t.Run("bad, historic, by stateroot: contract not found", func(t *testing.T) {
var h uint32 = 1
sr, err := chain.GetStateModule().GetStateRoot(h)
require.NoError(t, err)
_, err = c.InvokeContractVerifyWithState(sr.Root, contract, smartcontract.Params{}, []transaction.Signer{{Account: testchain.PrivateKeyByID(0).PublicKey().GetScriptHash()}})
require.Error(t, err)
require.True(t, strings.Contains(err.Error(), core.ErrUnknownVerificationContract.Error())) // contract wasn't deployed at block #1 yet
})
t.Run("positive, with signer and witness", func(t *testing.T) {
res, err := c.InvokeContractVerify(contract, smartcontract.Params{}, []transaction.Signer{{Account: testchain.PrivateKeyByID(0).PublicKey().GetScriptHash()}}, transaction.Witness{InvocationScript: []byte{byte(opcode.PUSH1), byte(opcode.RET)}})
require.NoError(t, err)

View file

@ -24,6 +24,7 @@ import (
"github.com/nspcc-dev/neo-go/pkg/core/block"
"github.com/nspcc-dev/neo-go/pkg/core/blockchainer"
"github.com/nspcc-dev/neo-go/pkg/core/fee"
"github.com/nspcc-dev/neo-go/pkg/core/interop"
"github.com/nspcc-dev/neo-go/pkg/core/interop/iterator"
"github.com/nspcc-dev/neo-go/pkg/core/mempoolevent"
"github.com/nspcc-dev/neo-go/pkg/core/mpt"
@ -108,46 +109,49 @@ const (
)
var rpcHandlers = map[string]func(*Server, request.Params) (interface{}, *response.Error){
"calculatenetworkfee": (*Server).calculateNetworkFee,
"findstates": (*Server).findStates,
"getapplicationlog": (*Server).getApplicationLog,
"getbestblockhash": (*Server).getBestBlockHash,
"getblock": (*Server).getBlock,
"getblockcount": (*Server).getBlockCount,
"getblockhash": (*Server).getBlockHash,
"getblockheader": (*Server).getBlockHeader,
"getblockheadercount": (*Server).getBlockHeaderCount,
"getblocksysfee": (*Server).getBlockSysFee,
"getcommittee": (*Server).getCommittee,
"getconnectioncount": (*Server).getConnectionCount,
"getcontractstate": (*Server).getContractState,
"getnativecontracts": (*Server).getNativeContracts,
"getnep11balances": (*Server).getNEP11Balances,
"getnep11properties": (*Server).getNEP11Properties,
"getnep11transfers": (*Server).getNEP11Transfers,
"getnep17balances": (*Server).getNEP17Balances,
"getnep17transfers": (*Server).getNEP17Transfers,
"getpeers": (*Server).getPeers,
"getproof": (*Server).getProof,
"getrawmempool": (*Server).getRawMempool,
"getrawtransaction": (*Server).getrawtransaction,
"getstate": (*Server).getState,
"getstateheight": (*Server).getStateHeight,
"getstateroot": (*Server).getStateRoot,
"getstorage": (*Server).getStorage,
"gettransactionheight": (*Server).getTransactionHeight,
"getunclaimedgas": (*Server).getUnclaimedGas,
"getnextblockvalidators": (*Server).getNextBlockValidators,
"getversion": (*Server).getVersion,
"invokefunction": (*Server).invokeFunction,
"invokescript": (*Server).invokescript,
"invokecontractverify": (*Server).invokeContractVerify,
"sendrawtransaction": (*Server).sendrawtransaction,
"submitblock": (*Server).submitBlock,
"submitnotaryrequest": (*Server).submitNotaryRequest,
"submitoracleresponse": (*Server).submitOracleResponse,
"validateaddress": (*Server).validateAddress,
"verifyproof": (*Server).verifyProof,
"calculatenetworkfee": (*Server).calculateNetworkFee,
"findstates": (*Server).findStates,
"getapplicationlog": (*Server).getApplicationLog,
"getbestblockhash": (*Server).getBestBlockHash,
"getblock": (*Server).getBlock,
"getblockcount": (*Server).getBlockCount,
"getblockhash": (*Server).getBlockHash,
"getblockheader": (*Server).getBlockHeader,
"getblockheadercount": (*Server).getBlockHeaderCount,
"getblocksysfee": (*Server).getBlockSysFee,
"getcommittee": (*Server).getCommittee,
"getconnectioncount": (*Server).getConnectionCount,
"getcontractstate": (*Server).getContractState,
"getnativecontracts": (*Server).getNativeContracts,
"getnep11balances": (*Server).getNEP11Balances,
"getnep11properties": (*Server).getNEP11Properties,
"getnep11transfers": (*Server).getNEP11Transfers,
"getnep17balances": (*Server).getNEP17Balances,
"getnep17transfers": (*Server).getNEP17Transfers,
"getpeers": (*Server).getPeers,
"getproof": (*Server).getProof,
"getrawmempool": (*Server).getRawMempool,
"getrawtransaction": (*Server).getrawtransaction,
"getstate": (*Server).getState,
"getstateheight": (*Server).getStateHeight,
"getstateroot": (*Server).getStateRoot,
"getstorage": (*Server).getStorage,
"gettransactionheight": (*Server).getTransactionHeight,
"getunclaimedgas": (*Server).getUnclaimedGas,
"getnextblockvalidators": (*Server).getNextBlockValidators,
"getversion": (*Server).getVersion,
"invokefunction": (*Server).invokeFunction,
"invokefunctionhistoric": (*Server).invokeFunctionHistoric,
"invokescript": (*Server).invokescript,
"invokescripthistoric": (*Server).invokescripthistoric,
"invokecontractverify": (*Server).invokeContractVerify,
"invokecontractverifyhistoric": (*Server).invokeContractVerifyHistoric,
"sendrawtransaction": (*Server).sendrawtransaction,
"submitblock": (*Server).submitBlock,
"submitnotaryrequest": (*Server).submitNotaryRequest,
"submitoracleresponse": (*Server).submitOracleResponse,
"validateaddress": (*Server).validateAddress,
"verifyproof": (*Server).verifyProof,
}
var rpcWsHandlers = map[string]func(*Server, request.Params, *subscriber) (interface{}, *response.Error){
@ -866,7 +870,7 @@ func (s *Server) invokeReadOnly(bw *io.BufBinWriter, h util.Uint160, method stri
}
script := bw.Bytes()
tx := &transaction.Transaction{Script: script}
b, err := s.getFakeNextBlock()
b, err := s.getFakeNextBlock(s.chain.BlockHeight() + 1)
if err != nil {
return nil, nil, err
}
@ -1571,16 +1575,40 @@ func (s *Server) getCommittee(_ request.Params) (interface{}, *response.Error) {
// invokeFunction implements the `invokeFunction` RPC call.
func (s *Server) invokeFunction(reqParams request.Params) (interface{}, *response.Error) {
tx, verbose, respErr := s.getInvokeFunctionParams(reqParams)
if respErr != nil {
return nil, respErr
}
return s.runScriptInVM(trigger.Application, tx.Script, util.Uint160{}, tx, nil, verbose)
}
// invokeFunctionHistoric implements the `invokeFunctionHistoric` RPC call.
func (s *Server) invokeFunctionHistoric(reqParams request.Params) (interface{}, *response.Error) {
b, respErr := s.getHistoricParams(reqParams)
if respErr != nil {
return nil, respErr
}
if len(reqParams) < 2 {
return nil, response.ErrInvalidParams
}
tx, verbose, respErr := s.getInvokeFunctionParams(reqParams[1:])
if respErr != nil {
return nil, respErr
}
return s.runScriptInVM(trigger.Application, tx.Script, util.Uint160{}, tx, b, verbose)
}
func (s *Server) getInvokeFunctionParams(reqParams request.Params) (*transaction.Transaction, bool, *response.Error) {
if len(reqParams) < 2 {
return nil, false, response.ErrInvalidParams
}
scriptHash, responseErr := s.contractScriptHashFromParam(reqParams.Value(0))
if responseErr != nil {
return nil, responseErr
return nil, false, responseErr
}
method, err := reqParams[1].GetString()
if err != nil {
return nil, response.ErrInvalidParams
return nil, false, response.ErrInvalidParams
}
var params *request.Param
if len(reqParams) > 2 {
@ -1590,7 +1618,7 @@ func (s *Server) invokeFunction(reqParams request.Params) (interface{}, *respons
if len(reqParams) > 3 {
signers, _, err := reqParams[3].GetSignersWithWitnesses()
if err != nil {
return nil, response.ErrInvalidParams
return nil, false, response.ErrInvalidParams
}
tx.Signers = signers
}
@ -1598,7 +1626,7 @@ func (s *Server) invokeFunction(reqParams request.Params) (interface{}, *respons
if len(reqParams) > 4 {
verbose, err = reqParams[4].GetBoolean()
if err != nil {
return nil, response.ErrInvalidParams
return nil, false, response.ErrInvalidParams
}
}
if len(tx.Signers) == 0 {
@ -1606,28 +1634,48 @@ func (s *Server) invokeFunction(reqParams request.Params) (interface{}, *respons
}
script, err := request.CreateFunctionInvocationScript(scriptHash, method, params)
if err != nil {
return nil, response.NewInternalServerError("can't create invocation script", err)
return nil, false, response.NewInternalServerError("can't create invocation script", err)
}
tx.Script = script
return s.runScriptInVM(trigger.Application, script, util.Uint160{}, tx, verbose)
return tx, verbose, nil
}
// invokescript implements the `invokescript` RPC call.
func (s *Server) invokescript(reqParams request.Params) (interface{}, *response.Error) {
if len(reqParams) < 1 {
tx, verbose, respErr := s.getInvokeScriptParams(reqParams)
if respErr != nil {
return nil, respErr
}
return s.runScriptInVM(trigger.Application, tx.Script, util.Uint160{}, tx, nil, verbose)
}
// invokescripthistoric implements the `invokescripthistoric` RPC call.
func (s *Server) invokescripthistoric(reqParams request.Params) (interface{}, *response.Error) {
b, respErr := s.getHistoricParams(reqParams)
if respErr != nil {
return nil, respErr
}
if len(reqParams) < 2 {
return nil, response.ErrInvalidParams
}
tx, verbose, respErr := s.getInvokeScriptParams(reqParams[1:])
if respErr != nil {
return nil, respErr
}
return s.runScriptInVM(trigger.Application, tx.Script, util.Uint160{}, tx, b, verbose)
}
script, err := reqParams[0].GetBytesBase64()
func (s *Server) getInvokeScriptParams(reqParams request.Params) (*transaction.Transaction, bool, *response.Error) {
script, err := reqParams.Value(0).GetBytesBase64()
if err != nil {
return nil, response.ErrInvalidParams
return nil, false, response.ErrInvalidParams
}
tx := &transaction.Transaction{}
if len(reqParams) > 1 {
signers, witnesses, err := reqParams[1].GetSignersWithWitnesses()
if err != nil {
return nil, response.ErrInvalidParams
return nil, false, response.ErrInvalidParams
}
tx.Signers = signers
tx.Scripts = witnesses
@ -1636,33 +1684,57 @@ func (s *Server) invokescript(reqParams request.Params) (interface{}, *response.
if len(reqParams) > 2 {
verbose, err = reqParams[2].GetBoolean()
if err != nil {
return nil, response.ErrInvalidParams
return nil, false, response.ErrInvalidParams
}
}
if len(tx.Signers) == 0 {
tx.Signers = []transaction.Signer{{Account: util.Uint160{}, Scopes: transaction.None}}
}
tx.Script = script
return s.runScriptInVM(trigger.Application, script, util.Uint160{}, tx, verbose)
return tx, verbose, nil
}
// invokeContractVerify implements the `invokecontractverify` RPC call.
func (s *Server) invokeContractVerify(reqParams request.Params) (interface{}, *response.Error) {
scriptHash, tx, invocationScript, respErr := s.getInvokeContractVerifyParams(reqParams)
if respErr != nil {
return nil, respErr
}
return s.runScriptInVM(trigger.Verification, invocationScript, scriptHash, tx, nil, false)
}
// invokeContractVerifyHistoric implements the `invokecontractverifyhistoric` RPC call.
func (s *Server) invokeContractVerifyHistoric(reqParams request.Params) (interface{}, *response.Error) {
b, respErr := s.getHistoricParams(reqParams)
if respErr != nil {
return nil, respErr
}
if len(reqParams) < 2 {
return nil, response.ErrInvalidParams
}
scriptHash, tx, invocationScript, respErr := s.getInvokeContractVerifyParams(reqParams[1:])
if respErr != nil {
return nil, respErr
}
return s.runScriptInVM(trigger.Verification, invocationScript, scriptHash, tx, b, false)
}
func (s *Server) getInvokeContractVerifyParams(reqParams request.Params) (util.Uint160, *transaction.Transaction, []byte, *response.Error) {
scriptHash, responseErr := s.contractScriptHashFromParam(reqParams.Value(0))
if responseErr != nil {
return nil, responseErr
return util.Uint160{}, nil, nil, responseErr
}
bw := io.NewBufBinWriter()
if len(reqParams) > 1 {
args, err := reqParams[1].GetArray() // second `invokecontractverify` parameter is an array of arguments for `verify` method
if err != nil {
return nil, response.WrapErrorWithData(response.ErrInvalidParams, err)
return util.Uint160{}, nil, nil, response.WrapErrorWithData(response.ErrInvalidParams, err)
}
if len(args) > 0 {
err := request.ExpandArrayIntoScript(bw.BinWriter, args)
if err != nil {
return nil, response.NewRPCError("can't create witness invocation script", err.Error(), err)
return util.Uint160{}, nil, nil, response.NewRPCError("can't create witness invocation script", err.Error(), err)
}
}
}
@ -1672,7 +1744,7 @@ func (s *Server) invokeContractVerify(reqParams request.Params) (interface{}, *r
if len(reqParams) > 2 {
signers, witnesses, err := reqParams[2].GetSignersWithWitnesses()
if err != nil {
return nil, response.ErrInvalidParams
return util.Uint160{}, nil, nil, response.ErrInvalidParams
}
tx.Signers = signers
tx.Scripts = witnesses
@ -1680,16 +1752,51 @@ func (s *Server) invokeContractVerify(reqParams request.Params) (interface{}, *r
tx.Signers = []transaction.Signer{{Account: scriptHash}}
tx.Scripts = []transaction.Witness{{InvocationScript: invocationScript, VerificationScript: []byte{}}}
}
return s.runScriptInVM(trigger.Verification, invocationScript, scriptHash, tx, false)
return scriptHash, tx, invocationScript, nil
}
func (s *Server) getFakeNextBlock() (*block.Block, error) {
// getHistoricParams checks that historic calls are supported and returns fake block
// with the specified index to perform the historic call. It also checks that
// specified stateroot is stored at the specified height for further request
// handling consistency.
func (s *Server) getHistoricParams(reqParams request.Params) (*block.Block, *response.Error) {
if s.chain.GetConfig().KeepOnlyLatestState {
return nil, response.NewInvalidRequestError("only latest state is supported", errKeepOnlyLatestState)
}
if len(reqParams) < 1 {
return nil, response.ErrInvalidParams
}
height, respErr := s.blockHeightFromParam(reqParams.Value(0))
if respErr != nil {
hash, err := reqParams.Value(0).GetUint256()
if err != nil {
return nil, response.NewInvalidParamsError("invalid block hash or index or stateroot hash", err)
}
b, err := s.chain.GetBlock(hash)
if err != nil {
stateH, err := s.chain.GetStateModule().GetLatestStateHeight(hash)
if err != nil {
return nil, response.NewInvalidParamsError(fmt.Sprintf("unknown block or stateroot: %s", err), err)
}
height = int(stateH)
} else {
height = int(b.Index)
}
}
b, err := s.getFakeNextBlock(uint32(height))
if err != nil {
return nil, response.NewInternalServerError(fmt.Sprintf("can't create fake block for height %d: %s", height, err.Error()), err)
}
return b, nil
}
func (s *Server) getFakeNextBlock(nextBlockHeight uint32) (*block.Block, error) {
// When transferring funds, script execution does no auto GAS claim,
// because it depends on persisting tx height.
// This is why we provide block here.
b := block.New(s.stateRootEnabled)
b.Index = s.chain.BlockHeight() + 1
hdr, err := s.chain.GetHeader(s.chain.GetHeaderHash(int(s.chain.BlockHeight())))
b.Index = nextBlockHeight
hdr, err := s.chain.GetHeader(s.chain.GetHeaderHash(int(nextBlockHeight - 1)))
if err != nil {
return nil, err
}
@ -1702,12 +1809,23 @@ func (s *Server) getFakeNextBlock() (*block.Block, error) {
// witness invocation script in case of `verification` trigger (it pushes `verify`
// arguments on stack before verification). In case of contract verification
// contractScriptHash should be specified.
func (s *Server) runScriptInVM(t trigger.Type, script []byte, contractScriptHash util.Uint160, tx *transaction.Transaction, verbose bool) (*result.Invoke, *response.Error) {
b, err := s.getFakeNextBlock()
if err != nil {
return nil, response.NewInternalServerError("can't create fake block", err)
func (s *Server) runScriptInVM(t trigger.Type, script []byte, contractScriptHash util.Uint160, tx *transaction.Transaction, b *block.Block, verbose bool) (*result.Invoke, *response.Error) {
var (
err error
ic *interop.Context
)
if b == nil {
b, err = s.getFakeNextBlock(s.chain.BlockHeight() + 1)
if err != nil {
return nil, response.NewInternalServerError("can't create fake block", err)
}
ic = s.chain.GetTestVM(t, tx, b)
} else {
ic, err = s.chain.GetTestHistoricVM(t, tx, b)
if err != nil {
return nil, response.NewInternalServerError("failed to create historic VM", err)
}
}
ic := s.chain.GetTestVM(t, tx, b)
if verbose {
ic.VM.EnableInvocationTree()
}
@ -1720,9 +1838,9 @@ func (s *Server) runScriptInVM(t trigger.Type, script []byte, contractScriptHash
ic.VM.GasLimit = gasPolicy
}
err := s.chain.InitVerificationContext(ic, contractScriptHash, &transaction.Witness{InvocationScript: script, VerificationScript: []byte{}})
err = s.chain.InitVerificationContext(ic, contractScriptHash, &transaction.Witness{InvocationScript: script, VerificationScript: []byte{}})
if err != nil {
return nil, response.NewInternalServerError("can't prepare verification VM", err)
return nil, response.NewInternalServerError(fmt.Sprintf("can't prepare verification VM: %s", err.Error()), err)
}
} else {
ic.VM.LoadScriptWithFlags(script, callflag.All)

View file

@ -75,6 +75,7 @@ const (
nfsoContractHash = "5f9ebd6b001b54c7bc70f96e0412fcf415dfe09f"
nfsoToken1ID = "7e244ffd6aa85fb1579d2ed22e9b761ab62e3486"
invokescriptContractAVM = "VwIADBQBDAMOBQYMDQIODw0DDgcJAAAAAErZMCQE2zBwaEH4J+yMqiYEEUAMFA0PAwIJAAIBAwcDBAUCAQAOBgwJStkwJATbMHFpQfgn7IyqJgQSQBNA"
block20StateRootLE = "19ec3c3d01afe5274e8bb4a393c97da708c5608c5b0ad116c16108b6a04fb08e"
)
var (
@ -999,6 +1000,134 @@ var rpcTestCases = map[string][]rpcTestCase{
fail: true,
},
},
"invokefunctionhistoric": {
{
name: "positive, by index",
params: `[20, "50befd26fdf6e4d957c11e078b24ebce6291456f", "test", []]`,
result: func(e *executor) interface{} { return &result.Invoke{} },
check: func(t *testing.T, e *executor, inv interface{}) {
res, ok := inv.(*result.Invoke)
require.True(t, ok)
assert.NotNil(t, res.Script)
assert.NotEqual(t, "", res.State)
assert.NotEqual(t, 0, res.GasConsumed)
},
},
{
name: "positive, by stateroot",
params: `["` + block20StateRootLE + `", "50befd26fdf6e4d957c11e078b24ebce6291456f", "test", []]`,
result: func(e *executor) interface{} { return &result.Invoke{} },
check: func(t *testing.T, e *executor, inv interface{}) {
res, ok := inv.(*result.Invoke)
require.True(t, ok)
assert.NotNil(t, res.Script)
assert.NotEqual(t, "", res.State)
assert.NotEqual(t, 0, res.GasConsumed)
},
},
{
name: "positive, with notifications",
params: `[20, "` + nnsContractHash + `", "transfer", [{"type":"Hash160", "value":"0x0bcd2978634d961c24f5aea0802297ff128724d6"},{"type":"String", "value":"neo.com"},{"type":"Any", "value":null}],["0xb248508f4ef7088e10c48f14d04be3272ca29eee"]]`,
result: func(e *executor) interface{} {
script := []byte{0x0b, 0x0c, 0x07, 0x6e, 0x65, 0x6f, 0x2e, 0x63, 0x6f, 0x6d, 0x0c, 0x14, 0xd6, 0x24, 0x87, 0x12, 0xff, 0x97, 0x22, 0x80, 0xa0, 0xae, 0xf5, 0x24, 0x1c, 0x96, 0x4d, 0x63, 0x78, 0x29, 0xcd, 0xb, 0x13, 0xc0, 0x1f, 0xc, 0x8, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x65, 0x72, 0xc, 0x14, 0x1f, 0xe2, 0x37, 0x5c, 0xdc, 0xdb, 0xb2, 0x80, 0x40, 0x78, 0x65, 0x35, 0xd5, 0xef, 0xe4, 0x3, 0x39, 0x56, 0x92, 0xee, 0x41, 0x62, 0x7d, 0x5b, 0x52}
return &result.Invoke{
State: "HALT",
GasConsumed: 32167260,
Script: script,
Stack: []stackitem.Item{stackitem.Make(true)},
Notifications: []state.NotificationEvent{{
ScriptHash: nnsHash,
Name: "Transfer",
Item: stackitem.NewArray([]stackitem.Item{
stackitem.Make([]byte{0xee, 0x9e, 0xa2, 0x2c, 0x27, 0xe3, 0x4b, 0xd0, 0x14, 0x8f, 0xc4, 0x10, 0x8e, 0x08, 0xf7, 0x4e, 0x8f, 0x50, 0x48, 0xb2}),
stackitem.Make([]byte{0xd6, 0x24, 0x87, 0x12, 0xff, 0x97, 0x22, 0x80, 0xa0, 0xae, 0xf5, 0x24, 0x1c, 0x96, 0x4d, 0x63, 0x78, 0x29, 0xcd, 0x0b}),
stackitem.Make(1),
stackitem.Make("neo.com"),
}),
}},
}
},
},
{
name: "positive, verbose",
params: `[20, "` + nnsContractHash + `", "resolve", [{"type":"String", "value":"neo.com"},{"type":"Integer","value":1}], [], true]`,
result: func(e *executor) interface{} {
script := []byte{0x11, 0xc, 0x7, 0x6e, 0x65, 0x6f, 0x2e, 0x63, 0x6f, 0x6d, 0x12, 0xc0, 0x1f, 0xc, 0x7, 0x72, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0xc, 0x14, 0x1f, 0xe2, 0x37, 0x5c, 0xdc, 0xdb, 0xb2, 0x80, 0x40, 0x78, 0x65, 0x35, 0xd5, 0xef, 0xe4, 0x3, 0x39, 0x56, 0x92, 0xee, 0x41, 0x62, 0x7d, 0x5b, 0x52}
stdHash, _ := e.chain.GetNativeContractScriptHash(nativenames.StdLib)
cryptoHash, _ := e.chain.GetNativeContractScriptHash(nativenames.CryptoLib)
return &result.Invoke{
State: "HALT",
GasConsumed: 15928320,
Script: script,
Stack: []stackitem.Item{stackitem.Make("1.2.3.4")},
Notifications: []state.NotificationEvent{},
Diagnostics: &result.InvokeDiag{
Changes: []storage.Operation{},
Invocations: []*vm.InvocationTree{{
Current: hash.Hash160(script),
Calls: []*vm.InvocationTree{
{
Current: nnsHash,
Calls: []*vm.InvocationTree{
{
Current: stdHash,
},
{
Current: cryptoHash,
},
{
Current: stdHash,
},
{
Current: cryptoHash,
},
{
Current: cryptoHash,
},
},
},
},
}},
},
}
},
},
{
name: "no params",
params: `[]`,
fail: true,
},
{
name: "no args",
params: `[20]`,
fail: true,
},
{
name: "not a string",
params: `[20, 42, "test", []]`,
fail: true,
},
{
name: "not a scripthash",
params: `[20,"qwerty", "test", []]`,
fail: true,
},
{
name: "bad params",
params: `[20,"50befd26fdf6e4d957c11e078b24ebce6291456f", "test", [{"type": "Integer", "value": "qwerty"}]]`,
fail: true,
},
{
name: "bad height",
params: `[100500,"50befd26fdf6e4d957c11e078b24ebce6291456f", "test", [{"type": "Integer", "value": 1}]]`,
fail: true,
},
{
name: "bad stateroot",
params: `["` + util.Uint256{1, 2, 3}.StringLE() + `","50befd26fdf6e4d957c11e078b24ebce6291456f", "test", [{"type": "Integer", "value": 1}]]`,
fail: true,
},
},
"invokescript": {
{
name: "positive",
@ -1098,6 +1227,132 @@ var rpcTestCases = map[string][]rpcTestCase{
fail: true,
},
},
"invokescripthistoric": {
{
name: "positive, by index",
params: `[20,"UcVrDUhlbGxvLCB3b3JsZCFoD05lby5SdW50aW1lLkxvZ2FsdWY="]`,
result: func(e *executor) interface{} { return &result.Invoke{} },
check: func(t *testing.T, e *executor, inv interface{}) {
res, ok := inv.(*result.Invoke)
require.True(t, ok)
assert.NotEqual(t, "", res.Script)
assert.NotEqual(t, "", res.State)
assert.NotEqual(t, 0, res.GasConsumed)
},
},
{
name: "positive, by stateroot",
params: `["` + block20StateRootLE + `","UcVrDUhlbGxvLCB3b3JsZCFoD05lby5SdW50aW1lLkxvZ2FsdWY="]`,
result: func(e *executor) interface{} { return &result.Invoke{} },
check: func(t *testing.T, e *executor, inv interface{}) {
res, ok := inv.(*result.Invoke)
require.True(t, ok)
assert.NotEqual(t, "", res.Script)
assert.NotEqual(t, "", res.State)
assert.NotEqual(t, 0, res.GasConsumed)
},
},
{
name: "positive,verbose",
params: `[20, "UcVrDUhlbGxvLCB3b3JsZCFoD05lby5SdW50aW1lLkxvZ2FsdWY=",[],true]`,
result: func(e *executor) interface{} {
script := []byte{0x51, 0xc5, 0x6b, 0xd, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x2c, 0x20, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x21, 0x68, 0xf, 0x4e, 0x65, 0x6f, 0x2e, 0x52, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x4c, 0x6f, 0x67, 0x61, 0x6c, 0x75, 0x66}
return &result.Invoke{
State: "FAULT",
GasConsumed: 60,
Script: script,
Stack: []stackitem.Item{},
FaultException: "at instruction 0 (ROT): too big index",
Notifications: []state.NotificationEvent{},
Diagnostics: &result.InvokeDiag{
Changes: []storage.Operation{},
Invocations: []*vm.InvocationTree{{
Current: hash.Hash160(script),
}},
},
}
},
},
{
name: "positive, good witness",
// script is base64-encoded `invokescript_contract.avm` representation, hashes are hex-encoded LE bytes of hashes used in the contract with `0x` prefix
params: fmt.Sprintf(`[20,"%s",["0x0000000009070e030d0f0e020d0c06050e030c01","0x090c060e00010205040307030102000902030f0d"]]`, invokescriptContractAVM),
result: func(e *executor) interface{} { return &result.Invoke{} },
check: func(t *testing.T, e *executor, inv interface{}) {
res, ok := inv.(*result.Invoke)
require.True(t, ok)
assert.Equal(t, "HALT", res.State)
require.Equal(t, 1, len(res.Stack))
require.Equal(t, big.NewInt(3), res.Stack[0].Value())
},
},
{
name: "positive, bad witness of second hash",
params: fmt.Sprintf(`[20,"%s",["0x0000000009070e030d0f0e020d0c06050e030c01"]]`, invokescriptContractAVM),
result: func(e *executor) interface{} { return &result.Invoke{} },
check: func(t *testing.T, e *executor, inv interface{}) {
res, ok := inv.(*result.Invoke)
require.True(t, ok)
assert.Equal(t, "HALT", res.State)
require.Equal(t, 1, len(res.Stack))
require.Equal(t, big.NewInt(2), res.Stack[0].Value())
},
},
{
name: "positive, no good hashes",
params: fmt.Sprintf(`[20,"%s"]`, invokescriptContractAVM),
result: func(e *executor) interface{} { return &result.Invoke{} },
check: func(t *testing.T, e *executor, inv interface{}) {
res, ok := inv.(*result.Invoke)
require.True(t, ok)
assert.Equal(t, "HALT", res.State)
require.Equal(t, 1, len(res.Stack))
require.Equal(t, big.NewInt(1), res.Stack[0].Value())
},
},
{
name: "positive, bad hashes witness",
params: fmt.Sprintf(`[20,"%s",["0x0000000009070e030d0f0e020d0c06050e030c02"]]`, invokescriptContractAVM),
result: func(e *executor) interface{} { return &result.Invoke{} },
check: func(t *testing.T, e *executor, inv interface{}) {
res, ok := inv.(*result.Invoke)
require.True(t, ok)
assert.Equal(t, "HALT", res.State)
assert.Equal(t, 1, len(res.Stack))
assert.Equal(t, big.NewInt(1), res.Stack[0].Value())
},
},
{
name: "no params",
params: `[]`,
fail: true,
},
{
name: "no script",
params: `[20]`,
fail: true,
},
{
name: "not a string",
params: `[20,42]`,
fail: true,
},
{
name: "bas string",
params: `[20, "qwerty"]`,
fail: true,
},
{
name: "bas height",
params: `[100500,"qwerty"]`,
fail: true,
},
{
name: "bas stateroot",
params: `["` + util.Uint256{1, 2, 3}.StringLE() + `","UcVrDUhlbGxvLCB3b3JsZCFoD05lby5SdW50aW1lLkxvZ2FsdWY="]`,
fail: true,
},
},
"invokecontractverify": {
{
name: "positive",
@ -1203,6 +1458,129 @@ var rpcTestCases = map[string][]rpcTestCase{
fail: true,
},
},
"invokecontractverifyhistoric": {
{
name: "positive, by index",
params: fmt.Sprintf(`[20,"%s", [], [{"account":"%s"}]]`, verifyContractHash, testchain.PrivateKeyByID(0).PublicKey().GetScriptHash().StringLE()),
result: func(e *executor) interface{} { return &result.Invoke{} },
check: func(t *testing.T, e *executor, inv interface{}) {
res, ok := inv.(*result.Invoke)
require.True(t, ok)
assert.Nil(t, res.Script) // empty witness invocation script (pushes args of `verify` on stack, but this `verify` don't have args)
assert.Equal(t, "HALT", res.State)
assert.NotEqual(t, 0, res.GasConsumed)
assert.Equal(t, true, res.Stack[0].Value().(bool), fmt.Sprintf("check address in verification_contract.go: expected %s", testchain.PrivateKeyByID(0).Address()))
},
},
{
name: "positive, by stateroot",
params: fmt.Sprintf(`["`+block20StateRootLE+`","%s", [], [{"account":"%s"}]]`, verifyContractHash, testchain.PrivateKeyByID(0).PublicKey().GetScriptHash().StringLE()),
result: func(e *executor) interface{} { return &result.Invoke{} },
check: func(t *testing.T, e *executor, inv interface{}) {
res, ok := inv.(*result.Invoke)
require.True(t, ok)
assert.Nil(t, res.Script) // empty witness invocation script (pushes args of `verify` on stack, but this `verify` don't have args)
assert.Equal(t, "HALT", res.State)
assert.NotEqual(t, 0, res.GasConsumed)
assert.Equal(t, true, res.Stack[0].Value().(bool), fmt.Sprintf("check address in verification_contract.go: expected %s", testchain.PrivateKeyByID(0).Address()))
},
},
{
name: "positive, no signers",
params: fmt.Sprintf(`[20,"%s", []]`, verifyContractHash),
result: func(e *executor) interface{} { return &result.Invoke{} },
check: func(t *testing.T, e *executor, inv interface{}) {
res, ok := inv.(*result.Invoke)
require.True(t, ok)
assert.Nil(t, res.Script)
assert.Equal(t, "HALT", res.State, res.FaultException)
assert.NotEqual(t, 0, res.GasConsumed)
assert.Equal(t, false, res.Stack[0].Value().(bool))
},
},
{
name: "positive, no arguments",
params: fmt.Sprintf(`[20,"%s"]`, verifyContractHash),
result: func(e *executor) interface{} { return &result.Invoke{} },
check: func(t *testing.T, e *executor, inv interface{}) {
res, ok := inv.(*result.Invoke)
require.True(t, ok)
assert.Nil(t, res.Script)
assert.Equal(t, "HALT", res.State, res.FaultException)
assert.NotEqual(t, 0, res.GasConsumed)
assert.Equal(t, false, res.Stack[0].Value().(bool))
},
},
{
name: "positive, with signers and scripts",
params: fmt.Sprintf(`[20,"%s", [], [{"account":"%s", "invocation":"MQo=", "verification": ""}]]`, verifyContractHash, testchain.PrivateKeyByID(0).PublicKey().GetScriptHash().StringLE()),
result: func(e *executor) interface{} { return &result.Invoke{} },
check: func(t *testing.T, e *executor, inv interface{}) {
res, ok := inv.(*result.Invoke)
require.True(t, ok)
assert.Nil(t, res.Script)
assert.Equal(t, "HALT", res.State)
assert.NotEqual(t, 0, res.GasConsumed)
assert.Equal(t, true, res.Stack[0].Value().(bool))
},
},
{
name: "positive, with arguments, result=true",
params: fmt.Sprintf(`[20,"%s", [{"type": "String", "value": "good_string"}, {"type": "Integer", "value": "4"}, {"type":"Boolean", "value": false}]]`, verifyWithArgsContractHash),
result: func(e *executor) interface{} { return &result.Invoke{} },
check: func(t *testing.T, e *executor, inv interface{}) {
res, ok := inv.(*result.Invoke)
require.True(t, ok)
expectedInvScript := io.NewBufBinWriter()
emit.Int(expectedInvScript.BinWriter, 0)
emit.Int(expectedInvScript.BinWriter, int64(4))
emit.String(expectedInvScript.BinWriter, "good_string")
require.NoError(t, expectedInvScript.Err)
assert.Equal(t, expectedInvScript.Bytes(), res.Script) // witness invocation script (pushes args of `verify` on stack)
assert.Equal(t, "HALT", res.State, res.FaultException)
assert.NotEqual(t, 0, res.GasConsumed)
assert.Equal(t, true, res.Stack[0].Value().(bool))
},
},
{
name: "positive, with arguments, result=false",
params: fmt.Sprintf(`[20, "%s", [{"type": "String", "value": "invalid_string"}, {"type": "Integer", "value": "4"}, {"type":"Boolean", "value": false}]]`, verifyWithArgsContractHash),
result: func(e *executor) interface{} { return &result.Invoke{} },
check: func(t *testing.T, e *executor, inv interface{}) {
res, ok := inv.(*result.Invoke)
require.True(t, ok)
expectedInvScript := io.NewBufBinWriter()
emit.Int(expectedInvScript.BinWriter, 0)
emit.Int(expectedInvScript.BinWriter, int64(4))
emit.String(expectedInvScript.BinWriter, "invalid_string")
require.NoError(t, expectedInvScript.Err)
assert.Equal(t, expectedInvScript.Bytes(), res.Script)
assert.Equal(t, "HALT", res.State, res.FaultException)
assert.NotEqual(t, 0, res.GasConsumed)
assert.Equal(t, false, res.Stack[0].Value().(bool))
},
},
{
name: "unknown contract",
params: fmt.Sprintf(`[20, "%s", []]`, util.Uint160{}.String()),
fail: true,
},
{
name: "no params",
params: `[]`,
fail: true,
},
{
name: "no args",
params: `[20]`,
fail: true,
},
{
name: "not a string",
params: `[20,42, []]`,
fail: true,
},
},
"sendrawtransaction": {
{
name: "positive",