From 0cf525d62ecbc7cf6a4cf3d3643738eaaa679525 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Thu, 7 Apr 2022 18:11:05 +0300 Subject: [PATCH 01/24] core: add ability to traverse backwards for Billet --- pkg/core/mpt/billet.go | 30 ++++++++++++++++++++++-------- pkg/core/mpt/trie.go | 2 +- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/pkg/core/mpt/billet.go b/pkg/core/mpt/billet.go index af7edf2be..40ee35de0 100644 --- a/pkg/core/mpt/billet.go +++ b/pkg/core/mpt/billet.go @@ -205,7 +205,7 @@ func (b *Billet) incrementRefAndStore(h util.Uint256, bs []byte) { // returned from `process` function. It also replaces all HashNodes to their // "unhashed" counterparts until the stop condition is satisfied. func (b *Billet) Traverse(process func(pathToNode []byte, node Node, nodeBytes []byte) bool, ignoreStorageErr bool) error { - r, err := b.traverse(b.root, []byte{}, []byte{}, process, ignoreStorageErr) + r, err := b.traverse(b.root, []byte{}, []byte{}, process, ignoreStorageErr, false) if err != nil && !errors.Is(err, errStop) { return err } @@ -213,7 +213,7 @@ func (b *Billet) Traverse(process func(pathToNode []byte, node Node, nodeBytes [ return nil } -func (b *Billet) traverse(curr Node, path, from []byte, process func(pathToNode []byte, node Node, nodeBytes []byte) bool, ignoreStorageErr bool) (Node, error) { +func (b *Billet) traverse(curr Node, path, from []byte, process func(pathToNode []byte, node Node, nodeBytes []byte) bool, ignoreStorageErr bool, backwards bool) (Node, error) { if _, ok := curr.(EmptyNode); ok { // We're not interested in EmptyNodes, and they do not affect the // traversal process, thus remain them untouched. @@ -227,7 +227,7 @@ func (b *Billet) traverse(curr Node, path, from []byte, process func(pathToNode } return nil, err } - return b.traverse(r, path, from, process, ignoreStorageErr) + return b.traverse(r, path, from, process, ignoreStorageErr, backwards) } if len(from) == 0 { bytes := slice.Copy(curr.Bytes()) @@ -242,22 +242,36 @@ func (b *Billet) traverse(curr Node, path, from []byte, process func(pathToNode var ( startIndex byte endIndex byte = childrenCount + cmp = func(i int) bool { + return i < int(endIndex) + } + step = 1 ) + if backwards { + startIndex, endIndex = lastChild, startIndex + cmp = func(i int) bool { + return i >= int(endIndex) + } + step = -1 + } if len(from) != 0 { endIndex = lastChild + if backwards { + endIndex = 0 + } startIndex, from = splitPath(from) } - for i := startIndex; i < endIndex; i++ { + for i := int(startIndex); cmp(i); i += step { var newPath []byte if i == lastChild { newPath = path } else { - newPath = append(path, i) + newPath = append(path, byte(i)) } - if i != startIndex { + if byte(i) != startIndex { from = []byte{} } - r, err := b.traverse(n.Children[i], newPath, from, process, ignoreStorageErr) + r, err := b.traverse(n.Children[i], newPath, from, process, ignoreStorageErr, backwards) if err != nil { if !errors.Is(err, errStop) { return nil, err @@ -276,7 +290,7 @@ func (b *Billet) traverse(curr Node, path, from []byte, process func(pathToNode } else { return b.tryCollapseExtension(n), nil } - r, err := b.traverse(n.next, append(path, n.key...), from, process, ignoreStorageErr) + r, err := b.traverse(n.next, append(path, n.key...), from, process, ignoreStorageErr, backwards) if err != nil && !errors.Is(err, errStop) { return nil, err } diff --git a/pkg/core/mpt/trie.go b/pkg/core/mpt/trie.go index 608a39cfb..90352af3d 100644 --- a/pkg/core/mpt/trie.go +++ b/pkg/core/mpt/trie.go @@ -625,7 +625,7 @@ func (t *Trie) Find(prefix, from []byte, max int) ([]storage.KeyValue, error) { } return count >= max } - _, err = b.traverse(start, path, fromP, process, false) + _, err = b.traverse(start, path, fromP, process, false, false) if err != nil && !errors.Is(err, errStop) { return nil, err } From f8b5972f617248d27d8b5073c22d018d8e88d872 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Thu, 7 Apr 2022 18:11:49 +0300 Subject: [PATCH 02/24] core: support Store interface over MPT --- pkg/core/mpt/trie_store.go | 126 ++++++++++++++++++++++++++++++++ pkg/core/mpt/trie_store_test.go | 73 ++++++++++++++++++ 2 files changed, 199 insertions(+) create mode 100644 pkg/core/mpt/trie_store.go create mode 100644 pkg/core/mpt/trie_store_test.go diff --git a/pkg/core/mpt/trie_store.go b/pkg/core/mpt/trie_store.go new file mode 100644 index 000000000..b07cc99df --- /dev/null +++ b/pkg/core/mpt/trie_store.go @@ -0,0 +1,126 @@ +package mpt + +import ( + "bytes" + "errors" + "fmt" + + "github.com/nspcc-dev/neo-go/pkg/core/storage" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/util/slice" +) + +// TrieStore is an MPT-based storage implementation for storing and retrieving +// historic blockchain data. TrieStore is supposed to be used within transaction +// script invocations only, thus only contract storage related operations are +// supported. All storage-related operations are being performed using historical +// storage data retrieved from MPT state. TrieStore is read-only and does not +// support put-related operations, thus, it should always be wrapped into +// MemCachedStore for proper puts handling. TrieStore never changes the provided +// backend store. +type TrieStore struct { + trie *Trie +} + +// ErrForbiddenTrieStoreOperation is returned when operation is not supposed to +// be performed over MPT-based Store. +var ErrForbiddenTrieStoreOperation = errors.New("operation is not allowed to be performed over TrieStore") + +// NewTrieStore returns a new ready to use MPT-backed storage. +func NewTrieStore(root util.Uint256, mode TrieMode, backed storage.Store) *TrieStore { + cache, ok := backed.(*storage.MemCachedStore) + if !ok { + cache = storage.NewMemCachedStore(backed) + } + tr := NewTrie(NewHashNode(root), mode, cache) + return &TrieStore{ + trie: tr, + } +} + +// Get implements the Store interface. +func (m *TrieStore) Get(key []byte) ([]byte, error) { + if len(key) == 0 { + return nil, fmt.Errorf("%w: Get is supported only for contract storage items", ErrForbiddenTrieStoreOperation) + } + switch storage.KeyPrefix(key[0]) { + case storage.STStorage, storage.STTempStorage: + res, err := m.trie.Get(key[1:]) + if err != nil && errors.Is(err, ErrNotFound) { + // Mimic the real storage behaviour. + return nil, storage.ErrKeyNotFound + } + return res, err + default: + return nil, fmt.Errorf("%w: Get is supported only for contract storage items", ErrForbiddenTrieStoreOperation) + } +} + +// PutChangeSet implements the Store interface. +func (m *TrieStore) PutChangeSet(puts map[string][]byte, stor map[string][]byte) error { + // Only Get and Seek should be supported, as TrieStore is read-only and is always + // should be wrapped by MemCachedStore to properly support put operations (if any). + return fmt.Errorf("%w: PutChangeSet is not supported", ErrForbiddenTrieStoreOperation) +} + +// Seek implements the Store interface. +func (m *TrieStore) Seek(rng storage.SeekRange, f func(k, v []byte) bool) { + prefix := storage.KeyPrefix(rng.Prefix[0]) + if prefix != storage.STStorage && prefix != storage.STTempStorage { // Prefix is always non-empty. + panic(fmt.Errorf("%w: Seek is supported only for contract storage items", ErrForbiddenTrieStoreOperation)) + } + prefixP := toNibbles(rng.Prefix[1:]) + fromP := []byte{} + if len(rng.Start) > 0 { + fromP = toNibbles(rng.Start) + } + _, start, path, err := m.trie.getWithPath(m.trie.root, prefixP, false) + if err != nil { + // Failed to determine the start node => no matching items. + return + } + path = path[len(prefixP):] + + if len(fromP) > 0 { + if len(path) <= len(fromP) && bytes.HasPrefix(fromP, path) { + fromP = fromP[len(path):] + } else if len(path) > len(fromP) && bytes.HasPrefix(path, fromP) { + fromP = []byte{} + } else { + cmp := bytes.Compare(path, fromP) + if cmp < 0 == rng.Backwards { + // No matching items. + return + } + fromP = []byte{} + } + } + + b := NewBillet(m.trie.root.Hash(), m.trie.mode, 0, m.trie.Store) + process := func(pathToNode []byte, node Node, _ []byte) bool { + if leaf, ok := node.(*LeafNode); ok { + // (*Billet).traverse includes `from` path into the result if so. It's OK for Seek, so shouldn't be filtered out. + kv := storage.KeyValue{ + Key: append(slice.Copy(rng.Prefix), pathToNode...), // Do not cut prefix. + Value: slice.Copy(leaf.value), + } + return !f(kv.Key, kv.Value) // Should return whether to stop. + } + return false + } + _, err = b.traverse(start, path, fromP, process, false, rng.Backwards) + if err != nil && !errors.Is(err, errStop) { + panic(fmt.Errorf("failed to perform Seek operation on TrieStore: %w", err)) + } +} + +// SeekGC implements the Store interface. +func (m *TrieStore) SeekGC(rng storage.SeekRange, keep func(k, v []byte) bool) error { + return fmt.Errorf("%w: SeekGC is not supported", ErrForbiddenTrieStoreOperation) +} + +// Close implements the Store interface. +func (m *TrieStore) Close() error { + m.trie = nil + return nil +} diff --git a/pkg/core/mpt/trie_store_test.go b/pkg/core/mpt/trie_store_test.go new file mode 100644 index 000000000..063d23c73 --- /dev/null +++ b/pkg/core/mpt/trie_store_test.go @@ -0,0 +1,73 @@ +package mpt + +import ( + "bytes" + "testing" + + "github.com/nspcc-dev/neo-go/pkg/core/storage" + "github.com/stretchr/testify/require" +) + +func TestTrieStore_TestTrieOperations(t *testing.T) { + source := newTestTrie(t) + backed := source.Store + + st := NewTrieStore(source.root.Hash(), ModeAll, backed) + + t.Run("forbidden operations", func(t *testing.T) { + require.ErrorIs(t, st.SeekGC(storage.SeekRange{}, nil), ErrForbiddenTrieStoreOperation) + _, err := st.Get([]byte{byte(storage.STTokenTransferInfo)}) + require.ErrorIs(t, err, ErrForbiddenTrieStoreOperation) + require.ErrorIs(t, st.PutChangeSet(nil, nil), ErrForbiddenTrieStoreOperation) + }) + + t.Run("Get", func(t *testing.T) { + t.Run("good", func(t *testing.T) { + res, err := st.Get(append([]byte{byte(storage.STStorage)}, 0xAC, 0xae)) // leaf `hello` + require.NoError(t, err) + require.Equal(t, []byte("hello"), res) + }) + t.Run("bad path", func(t *testing.T) { + _, err := st.Get(append([]byte{byte(storage.STStorage)}, 0xAC, 0xa0)) // bad path + require.ErrorIs(t, err, storage.ErrKeyNotFound) + }) + t.Run("path to not-a-leaf", func(t *testing.T) { + _, err := st.Get(append([]byte{byte(storage.STStorage)}, 0xAC)) // path to extension + require.ErrorIs(t, err, storage.ErrKeyNotFound) + }) + }) + + t.Run("Seek", func(t *testing.T) { + check := func(t *testing.T, backwards bool) { + var res [][]byte + st.Seek(storage.SeekRange{ + Prefix: []byte{byte(storage.STStorage)}, + Start: nil, + Backwards: backwards, + }, func(k, v []byte) bool { + res = append(res, k) + return true + }) + require.Equal(t, 4, len(res)) + for i := 0; i < len(res); i++ { + require.Equal(t, byte(storage.STStorage), res[i][0]) + if i < len(res)-1 { + cmp := bytes.Compare(res[i], res[i+1]) + if backwards { + require.True(t, cmp > 0) + } else { + require.True(t, cmp < 0) + } + } + } + } + t.Run("good: over whole storage", func(t *testing.T) { + t.Run("forwards", func(t *testing.T) { + check(t, false) + }) + t.Run("backwards", func(t *testing.T) { + check(t, true) + }) + }) + }) +} From 63c26ca2702980b541180c21090095c78fd3b859 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Thu, 7 Apr 2022 18:13:08 +0300 Subject: [PATCH 03/24] core, rpc: support [invokefunction, invokescript, invokecontractverify]historic --- docs/rpc.md | 22 ++ pkg/core/blockchain.go | 29 ++ pkg/core/blockchainer/blockchainer.go | 1 + pkg/core/blockchainer/state_root.go | 1 + pkg/core/stateroot/module.go | 29 ++ pkg/core/stateroot_test.go | 17 ++ pkg/rpc/client/rpc.go | 81 ++++++ pkg/rpc/server/client_test.go | 50 ++++ pkg/rpc/server/server.go | 252 ++++++++++++----- pkg/rpc/server/server_test.go | 378 ++++++++++++++++++++++++++ 10 files changed, 793 insertions(+), 67 deletions(-) diff --git a/docs/rpc.md b/docs/rpc.md index ad22e75cb..4fb8c8a86 100644 --- a/docs/rpc.md +++ b/docs/rpc.md @@ -168,6 +168,28 @@ block. It can be removed in future versions, but at the moment you can use it to see how much GAS is burned with particular block (because system fees are burned). +#### `invokecontractverifyhistoric`, `invokefunctionhistoric` and `invokescripthistoric` calls + +These methods provide the ability of *historical* calls and accept block hash or +block index or stateroot hash as the first parameter and the list of parameters +that is the same as of `invokecontractverify`, `invokefunction` and +`invokescript` correspondingly. The historical call assumes that the contracts' +storage state has all its values got from MPT with the specified stateroot and +the transaction will be invoked using interop context with block of the specified +height. This allows to perform test invocation using the specified past chain +state. These methods may be useful for debugging purposes. + +Behavior note: any historical RPC call need the historical chain state to be +presented in the node storage, thus if the node keeps only latest MPT state +the historical call can not be handled properly.The historical calls only +guaranteed to correctly work on archival node that stores all MPT data. If a +node keeps the number of latest states and has the GC on (this setting +corresponds to the `RemoveUntraceableBlocks` set to `true`), then the behaviour +of historical RPC call is undefined. GC can always kick some data out of the +storage while the historical call is executing, thus keep in mind that the call +can be processed with `RemoveUntraceableBlocks` only with limitations on +available data. + #### `submitnotaryrequest` call This method can be used on P2P Notary enabled networks to submit new notary diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index efbf57d0d..2c36e6fd2 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -2148,6 +2148,35 @@ 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 + systemInterop := bc.newInteropContext(t, dTrie, b, tx) + vm := systemInterop.SpawnVM() + vm.SetPriceGetter(systemInterop.GetPrice) + vm.LoadToken = contract.LoadToken(systemInterop) + return systemInterop, nil +} + // Various witness verification errors. var ( ErrWitnessHashMismatch = errors.New("witness hash mismatch") diff --git a/pkg/core/blockchainer/blockchainer.go b/pkg/core/blockchainer/blockchainer.go index f5179245f..6e7a1fcac 100644 --- a/pkg/core/blockchainer/blockchainer.go +++ b/pkg/core/blockchainer/blockchainer.go @@ -60,6 +60,7 @@ type Blockchainer interface { GetStateModule() StateRoot GetStorageItem(id int32, key []byte) state.StorageItem GetTestVM(t trigger.Type, tx *transaction.Transaction, b *block.Block) *interop.Context + GetTestHistoricVM(t trigger.Type, tx *transaction.Transaction, b *block.Block) (*interop.Context, error) GetTransaction(util.Uint256) (*transaction.Transaction, uint32, error) SetOracle(service services.Oracle) mempool.Feer // fee interface diff --git a/pkg/core/blockchainer/state_root.go b/pkg/core/blockchainer/state_root.go index 0328c3a63..3b11f6c63 100644 --- a/pkg/core/blockchainer/state_root.go +++ b/pkg/core/blockchainer/state_root.go @@ -15,4 +15,5 @@ type StateRoot interface { GetState(root util.Uint256, key []byte) ([]byte, error) GetStateProof(root util.Uint256, key []byte) ([][]byte, error) GetStateRoot(height uint32) (*state.MPTRoot, error) + GetLatestStateHeight(root util.Uint256) (uint32, error) } diff --git a/pkg/core/stateroot/module.go b/pkg/core/stateroot/module.go index b09c5a953..fc43e7a50 100644 --- a/pkg/core/stateroot/module.go +++ b/pkg/core/stateroot/module.go @@ -1,6 +1,7 @@ package stateroot import ( + "bytes" "encoding/binary" "errors" "fmt" @@ -101,6 +102,34 @@ func (s *Module) GetStateRoot(height uint32) (*state.MPTRoot, error) { return s.getStateRoot(makeStateRootKey(height)) } +// GetLatestStateHeight returns the latest blockchain height by the given stateroot. +func (s *Module) GetLatestStateHeight(root util.Uint256) (uint32, error) { + rootBytes := root.BytesBE() + rootStartOffset := 1 + 4 // stateroot version (1 byte) + stateroot index (4 bytes) + rootEndOffset := rootStartOffset + util.Uint256Size + var ( + h uint32 + found bool + rootKey = makeStateRootKey(s.localHeight.Load()) + ) + s.Store.Seek(storage.SeekRange{ + Prefix: []byte{rootKey[0]}, // DataMPTAux + Start: rootKey[1:], // Start is a value that should be appended to the Prefix + Backwards: true, + }, func(k, v []byte) bool { + if len(k) == 5 && bytes.Equal(v[rootStartOffset:rootEndOffset], rootBytes) { + h = binary.BigEndian.Uint32(k[1:]) // cut prefix DataMPTAux + found = true + return false + } + return true + }) + if found { + return h, nil + } + return h, storage.ErrKeyNotFound +} + // CurrentLocalStateRoot returns hash of the local state root. func (s *Module) CurrentLocalStateRoot() util.Uint256 { return s.currentLocal.Load().(util.Uint256) diff --git a/pkg/core/stateroot_test.go b/pkg/core/stateroot_test.go index 3a958def5..86df6c8b5 100644 --- a/pkg/core/stateroot_test.go +++ b/pkg/core/stateroot_test.go @@ -296,3 +296,20 @@ func checkVoteBroadcasted(t *testing.T, bc *core.Blockchain, p *payload.Extensib require.True(t, len(pubs) > int(valIndex)) require.True(t, pubs[valIndex].VerifyHashable(vote.Signature, uint32(netmode.UnitTestNet), r)) } + +func TestStateroot_GetLatestStateHeight(t *testing.T) { + bc, validators, committee := chain.NewMultiWithCustomConfig(t, func(c *config.ProtocolConfiguration) { + c.P2PSigExtensions = true + }) + e := neotest.NewExecutor(t, bc, validators, committee) + initBasicChain(t, e) + + m := bc.GetStateModule() + for i := uint32(0); i < bc.BlockHeight(); i++ { + r, err := m.GetStateRoot(i) + require.NoError(t, err) + h, err := bc.GetStateModule().GetLatestStateHeight(r.Root) + require.NoError(t, err, i) + require.Equal(t, i, h) + } +} diff --git a/pkg/rpc/client/rpc.go b/pkg/rpc/client/rpc.go index d643d4414..02648d1fb 100644 --- a/pkg/rpc/client/rpc.go +++ b/pkg/rpc/client/rpc.go @@ -588,6 +588,33 @@ func (c *Client) InvokeScript(script []byte, signers []transaction.Signer) (*res return c.invokeSomething("invokescript", p, signers) } +// InvokeScriptAtHeight returns the result of the given script after running it +// true the VM using the provided chain state retrieved from the specified chain +// height. +// NOTE: This is a test invoke and will not affect the blockchain. +func (c *Client) InvokeScriptAtHeight(height uint32, script []byte, signers []transaction.Signer) (*result.Invoke, error) { + var p = request.NewRawParams(height, script) + return c.invokeSomething("invokescripthistoric", p, signers) +} + +// InvokeScriptAtBlock returns the result of the given script after running it +// true the VM using the provided chain state retrieved from the specified block +// hash. +// NOTE: This is a test invoke and will not affect the blockchain. +func (c *Client) InvokeScriptAtBlock(blockHash util.Uint256, script []byte, signers []transaction.Signer) (*result.Invoke, error) { + var p = request.NewRawParams(blockHash.StringLE(), script) + return c.invokeSomething("invokescripthistoric", p, signers) +} + +// InvokeScriptWithState returns the result of the given script after running it +// true the VM using the provided chain state retrieved from the specified +// stateroot hash. +// NOTE: This is a test invoke and will not affect the blockchain. +func (c *Client) InvokeScriptWithState(stateroot util.Uint256, script []byte, signers []transaction.Signer) (*result.Invoke, error) { + var p = request.NewRawParams(stateroot.StringLE(), script) + return c.invokeSomething("invokescripthistoric", p, signers) +} + // InvokeFunction returns the results after calling the smart contract scripthash // with the given operation and parameters. // NOTE: this is test invoke and will not affect the blockchain. @@ -596,6 +623,33 @@ func (c *Client) InvokeFunction(contract util.Uint160, operation string, params return c.invokeSomething("invokefunction", p, signers) } +// InvokeFunctionAtHeight returns the results after calling the smart contract +// with the given operation and parameters at the given blockchain state +// specified by the blockchain height. +// NOTE: this is test invoke and will not affect the blockchain. +func (c *Client) InvokeFunctionAtHeight(height uint32, contract util.Uint160, operation string, params []smartcontract.Parameter, signers []transaction.Signer) (*result.Invoke, error) { + var p = request.NewRawParams(height, contract.StringLE(), operation, params) + return c.invokeSomething("invokefunctionhistoric", p, signers) +} + +// InvokeFunctionAtBlock returns the results after calling the smart contract +// with the given operation and parameters at given the blockchain state +// specified by the block hash. +// NOTE: this is test invoke and will not affect the blockchain. +func (c *Client) InvokeFunctionAtBlock(blockHash util.Uint256, contract util.Uint160, operation string, params []smartcontract.Parameter, signers []transaction.Signer) (*result.Invoke, error) { + var p = request.NewRawParams(blockHash.StringLE(), contract.StringLE(), operation, params) + return c.invokeSomething("invokefunctionhistoric", p, signers) +} + +// InvokeFunctionWithState returns the results after calling the smart contract +// with the given operation and parameters at the given blockchain state defined +// by the specified stateroot hash. +// NOTE: this is test invoke and will not affect the blockchain. +func (c *Client) InvokeFunctionWithState(stateroot util.Uint256, contract util.Uint160, operation string, params []smartcontract.Parameter, signers []transaction.Signer) (*result.Invoke, error) { + var p = request.NewRawParams(stateroot.StringLE(), contract.StringLE(), operation, params) + return c.invokeSomething("invokefunctionhistoric", p, signers) +} + // InvokeContractVerify returns the results after calling `verify` method of the smart contract // with the given parameters under verification trigger type. // NOTE: this is test invoke and will not affect the blockchain. @@ -604,6 +658,33 @@ func (c *Client) InvokeContractVerify(contract util.Uint160, params []smartcontr return c.invokeSomething("invokecontractverify", p, signers, witnesses...) } +// InvokeContractVerifyAtHeight returns the results after calling `verify` method +// of the smart contract with the given parameters under verification trigger type +// at the blockchain state specified by the blockchain height. +// NOTE: this is test invoke and will not affect the blockchain. +func (c *Client) InvokeContractVerifyAtHeight(height uint32, contract util.Uint160, params []smartcontract.Parameter, signers []transaction.Signer, witnesses ...transaction.Witness) (*result.Invoke, error) { + var p = request.NewRawParams(height, contract.StringLE(), params) + return c.invokeSomething("invokecontractverifyhistoric", p, signers, witnesses...) +} + +// InvokeContractVerifyAtBlock returns the results after calling `verify` method +// of the smart contract with the given parameters under verification trigger type +// at the blockchain state specified by the block hash. +// NOTE: this is test invoke and will not affect the blockchain. +func (c *Client) InvokeContractVerifyAtBlock(blockHash util.Uint256, contract util.Uint160, params []smartcontract.Parameter, signers []transaction.Signer, witnesses ...transaction.Witness) (*result.Invoke, error) { + var p = request.NewRawParams(blockHash.StringLE(), contract.StringLE(), params) + return c.invokeSomething("invokecontractverifyhistoric", p, signers, witnesses...) +} + +// InvokeContractVerifyWithState returns the results after calling `verify` method +// of the smart contract with the given parameters under verification trigger type +// at the blockchain state specified by the stateroot hash. +// NOTE: this is test invoke and will not affect the blockchain. +func (c *Client) InvokeContractVerifyWithState(stateroot util.Uint256, contract util.Uint160, params []smartcontract.Parameter, signers []transaction.Signer, witnesses ...transaction.Witness) (*result.Invoke, error) { + var p = request.NewRawParams(stateroot.StringLE(), contract.StringLE(), params) + return c.invokeSomething("invokecontractverifyhistoric", p, signers, witnesses...) +} + // invokeSomething is an inner wrapper for Invoke* functions. func (c *Client) invokeSomething(method string, p request.RawParams, signers []transaction.Signer, witnesses ...transaction.Witness) (*result.Invoke, error) { var resp = new(result.Invoke) diff --git a/pkg/rpc/server/client_test.go b/pkg/rpc/server/client_test.go index dc1ae0c5e..ca2ac2b63 100644 --- a/pkg/rpc/server/client_test.go +++ b/pkg/rpc/server/client_test.go @@ -766,6 +766,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) + // TODO: check that error is `ErrUnknownVerificationContract` + }) + + 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) + // TODO: check that error is `ErrUnknownVerificationContract` + }) + + 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) + // TODO: check that error is `ErrUnknownVerificationContract` + }) + t.Run("positive, with signer and witness", func(t *testing.T) { res, err := c.InvokeContractVerify(contract, smartcontract.Params{}, []transaction.Signer{{Account: testchain.PrivateKeyByID(0).PublicKey().GetScriptHash()}}, transaction.Witness{InvocationScript: []byte{byte(opcode.PUSH1), byte(opcode.RET)}}) require.NoError(t, err) diff --git a/pkg/rpc/server/server.go b/pkg/rpc/server/server.go index d08758173..590a44662 100644 --- a/pkg/rpc/server/server.go +++ b/pkg/rpc/server/server.go @@ -24,6 +24,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/core/block" "github.com/nspcc-dev/neo-go/pkg/core/blockchainer" "github.com/nspcc-dev/neo-go/pkg/core/fee" + "github.com/nspcc-dev/neo-go/pkg/core/interop" "github.com/nspcc-dev/neo-go/pkg/core/interop/iterator" "github.com/nspcc-dev/neo-go/pkg/core/mempoolevent" "github.com/nspcc-dev/neo-go/pkg/core/mpt" @@ -108,46 +109,49 @@ const ( ) var rpcHandlers = map[string]func(*Server, request.Params) (interface{}, *response.Error){ - "calculatenetworkfee": (*Server).calculateNetworkFee, - "findstates": (*Server).findStates, - "getapplicationlog": (*Server).getApplicationLog, - "getbestblockhash": (*Server).getBestBlockHash, - "getblock": (*Server).getBlock, - "getblockcount": (*Server).getBlockCount, - "getblockhash": (*Server).getBlockHash, - "getblockheader": (*Server).getBlockHeader, - "getblockheadercount": (*Server).getBlockHeaderCount, - "getblocksysfee": (*Server).getBlockSysFee, - "getcommittee": (*Server).getCommittee, - "getconnectioncount": (*Server).getConnectionCount, - "getcontractstate": (*Server).getContractState, - "getnativecontracts": (*Server).getNativeContracts, - "getnep11balances": (*Server).getNEP11Balances, - "getnep11properties": (*Server).getNEP11Properties, - "getnep11transfers": (*Server).getNEP11Transfers, - "getnep17balances": (*Server).getNEP17Balances, - "getnep17transfers": (*Server).getNEP17Transfers, - "getpeers": (*Server).getPeers, - "getproof": (*Server).getProof, - "getrawmempool": (*Server).getRawMempool, - "getrawtransaction": (*Server).getrawtransaction, - "getstate": (*Server).getState, - "getstateheight": (*Server).getStateHeight, - "getstateroot": (*Server).getStateRoot, - "getstorage": (*Server).getStorage, - "gettransactionheight": (*Server).getTransactionHeight, - "getunclaimedgas": (*Server).getUnclaimedGas, - "getnextblockvalidators": (*Server).getNextBlockValidators, - "getversion": (*Server).getVersion, - "invokefunction": (*Server).invokeFunction, - "invokescript": (*Server).invokescript, - "invokecontractverify": (*Server).invokeContractVerify, - "sendrawtransaction": (*Server).sendrawtransaction, - "submitblock": (*Server).submitBlock, - "submitnotaryrequest": (*Server).submitNotaryRequest, - "submitoracleresponse": (*Server).submitOracleResponse, - "validateaddress": (*Server).validateAddress, - "verifyproof": (*Server).verifyProof, + "calculatenetworkfee": (*Server).calculateNetworkFee, + "findstates": (*Server).findStates, + "getapplicationlog": (*Server).getApplicationLog, + "getbestblockhash": (*Server).getBestBlockHash, + "getblock": (*Server).getBlock, + "getblockcount": (*Server).getBlockCount, + "getblockhash": (*Server).getBlockHash, + "getblockheader": (*Server).getBlockHeader, + "getblockheadercount": (*Server).getBlockHeaderCount, + "getblocksysfee": (*Server).getBlockSysFee, + "getcommittee": (*Server).getCommittee, + "getconnectioncount": (*Server).getConnectionCount, + "getcontractstate": (*Server).getContractState, + "getnativecontracts": (*Server).getNativeContracts, + "getnep11balances": (*Server).getNEP11Balances, + "getnep11properties": (*Server).getNEP11Properties, + "getnep11transfers": (*Server).getNEP11Transfers, + "getnep17balances": (*Server).getNEP17Balances, + "getnep17transfers": (*Server).getNEP17Transfers, + "getpeers": (*Server).getPeers, + "getproof": (*Server).getProof, + "getrawmempool": (*Server).getRawMempool, + "getrawtransaction": (*Server).getrawtransaction, + "getstate": (*Server).getState, + "getstateheight": (*Server).getStateHeight, + "getstateroot": (*Server).getStateRoot, + "getstorage": (*Server).getStorage, + "gettransactionheight": (*Server).getTransactionHeight, + "getunclaimedgas": (*Server).getUnclaimedGas, + "getnextblockvalidators": (*Server).getNextBlockValidators, + "getversion": (*Server).getVersion, + "invokefunction": (*Server).invokeFunction, + "invokefunctionhistoric": (*Server).invokeFunctionHistoric, + "invokescript": (*Server).invokescript, + "invokescripthistoric": (*Server).invokescripthistoric, + "invokecontractverify": (*Server).invokeContractVerify, + "invokecontractverifyhistoric": (*Server).invokeContractVerifyHistoric, + "sendrawtransaction": (*Server).sendrawtransaction, + "submitblock": (*Server).submitBlock, + "submitnotaryrequest": (*Server).submitNotaryRequest, + "submitoracleresponse": (*Server).submitOracleResponse, + "validateaddress": (*Server).validateAddress, + "verifyproof": (*Server).verifyProof, } var rpcWsHandlers = map[string]func(*Server, request.Params, *subscriber) (interface{}, *response.Error){ @@ -866,7 +870,7 @@ func (s *Server) invokeReadOnly(bw *io.BufBinWriter, h util.Uint160, method stri } script := bw.Bytes() tx := &transaction.Transaction{Script: script} - b, err := s.getFakeNextBlock() + b, err := s.getFakeNextBlock(s.chain.BlockHeight() + 1) if err != nil { return nil, nil, err } @@ -1571,16 +1575,40 @@ func (s *Server) getCommittee(_ request.Params) (interface{}, *response.Error) { // invokeFunction implements the `invokeFunction` RPC call. func (s *Server) invokeFunction(reqParams request.Params) (interface{}, *response.Error) { + tx, verbose, respErr := s.getInvokeFunctionParams(reqParams) + if respErr != nil { + return nil, respErr + } + return s.runScriptInVM(trigger.Application, tx.Script, util.Uint160{}, tx, nil, verbose) +} + +// invokeFunctionHistoric implements the `invokeFunctionHistoric` RPC call. +func (s *Server) invokeFunctionHistoric(reqParams request.Params) (interface{}, *response.Error) { + b, respErr := s.getHistoricParams(reqParams) + if respErr != nil { + return nil, respErr + } if len(reqParams) < 2 { return nil, response.ErrInvalidParams } + tx, verbose, respErr := s.getInvokeFunctionParams(reqParams[1:]) + if respErr != nil { + return nil, respErr + } + return s.runScriptInVM(trigger.Application, tx.Script, util.Uint160{}, tx, b, verbose) +} + +func (s *Server) getInvokeFunctionParams(reqParams request.Params) (*transaction.Transaction, bool, *response.Error) { + if len(reqParams) < 2 { + return nil, false, response.ErrInvalidParams + } scriptHash, responseErr := s.contractScriptHashFromParam(reqParams.Value(0)) if responseErr != nil { - return nil, responseErr + return nil, false, responseErr } method, err := reqParams[1].GetString() if err != nil { - return nil, response.ErrInvalidParams + return nil, false, response.ErrInvalidParams } var params *request.Param if len(reqParams) > 2 { @@ -1590,7 +1618,7 @@ func (s *Server) invokeFunction(reqParams request.Params) (interface{}, *respons if len(reqParams) > 3 { signers, _, err := reqParams[3].GetSignersWithWitnesses() if err != nil { - return nil, response.ErrInvalidParams + return nil, false, response.ErrInvalidParams } tx.Signers = signers } @@ -1598,7 +1626,7 @@ func (s *Server) invokeFunction(reqParams request.Params) (interface{}, *respons if len(reqParams) > 4 { verbose, err = reqParams[4].GetBoolean() if err != nil { - return nil, response.ErrInvalidParams + return nil, false, response.ErrInvalidParams } } if len(tx.Signers) == 0 { @@ -1606,28 +1634,48 @@ func (s *Server) invokeFunction(reqParams request.Params) (interface{}, *respons } script, err := request.CreateFunctionInvocationScript(scriptHash, method, params) if err != nil { - return nil, response.NewInternalServerError("can't create invocation script", err) + return nil, false, response.NewInternalServerError("can't create invocation script", err) } tx.Script = script - return s.runScriptInVM(trigger.Application, script, util.Uint160{}, tx, verbose) + return tx, verbose, nil } // invokescript implements the `invokescript` RPC call. func (s *Server) invokescript(reqParams request.Params) (interface{}, *response.Error) { - if len(reqParams) < 1 { + tx, verbose, respErr := s.getInvokeScriptParams(reqParams) + if respErr != nil { + return nil, respErr + } + return s.runScriptInVM(trigger.Application, tx.Script, util.Uint160{}, tx, nil, verbose) +} + +// invokescripthistoric implements the `invokescripthistoric` RPC call. +func (s *Server) invokescripthistoric(reqParams request.Params) (interface{}, *response.Error) { + b, respErr := s.getHistoricParams(reqParams) + if respErr != nil { + return nil, respErr + } + if len(reqParams) < 2 { return nil, response.ErrInvalidParams } + tx, verbose, respErr := s.getInvokeScriptParams(reqParams[1:]) + if respErr != nil { + return nil, respErr + } + return s.runScriptInVM(trigger.Application, tx.Script, util.Uint160{}, tx, b, verbose) +} - script, err := reqParams[0].GetBytesBase64() +func (s *Server) getInvokeScriptParams(reqParams request.Params) (*transaction.Transaction, bool, *response.Error) { + script, err := reqParams.Value(0).GetBytesBase64() if err != nil { - return nil, response.ErrInvalidParams + return nil, false, response.ErrInvalidParams } tx := &transaction.Transaction{} if len(reqParams) > 1 { signers, witnesses, err := reqParams[1].GetSignersWithWitnesses() if err != nil { - return nil, response.ErrInvalidParams + return nil, false, response.ErrInvalidParams } tx.Signers = signers tx.Scripts = witnesses @@ -1636,33 +1684,57 @@ func (s *Server) invokescript(reqParams request.Params) (interface{}, *response. if len(reqParams) > 2 { verbose, err = reqParams[2].GetBoolean() if err != nil { - return nil, response.ErrInvalidParams + return nil, false, response.ErrInvalidParams } } if len(tx.Signers) == 0 { tx.Signers = []transaction.Signer{{Account: util.Uint160{}, Scopes: transaction.None}} } tx.Script = script - return s.runScriptInVM(trigger.Application, script, util.Uint160{}, tx, verbose) + return tx, verbose, nil } // invokeContractVerify implements the `invokecontractverify` RPC call. func (s *Server) invokeContractVerify(reqParams request.Params) (interface{}, *response.Error) { + scriptHash, tx, invocationScript, respErr := s.getInvokeContractVerifyParams(reqParams) + if respErr != nil { + return nil, respErr + } + return s.runScriptInVM(trigger.Verification, invocationScript, scriptHash, tx, nil, false) +} + +// invokeContractVerifyHistoric implements the `invokecontractverifyhistoric` RPC call. +func (s *Server) invokeContractVerifyHistoric(reqParams request.Params) (interface{}, *response.Error) { + b, respErr := s.getHistoricParams(reqParams) + if respErr != nil { + return nil, respErr + } + if len(reqParams) < 2 { + return nil, response.ErrInvalidParams + } + scriptHash, tx, invocationScript, respErr := s.getInvokeContractVerifyParams(reqParams[1:]) + if respErr != nil { + return nil, respErr + } + return s.runScriptInVM(trigger.Verification, invocationScript, scriptHash, tx, b, false) +} + +func (s *Server) getInvokeContractVerifyParams(reqParams request.Params) (util.Uint160, *transaction.Transaction, []byte, *response.Error) { scriptHash, responseErr := s.contractScriptHashFromParam(reqParams.Value(0)) if responseErr != nil { - return nil, responseErr + return util.Uint160{}, nil, nil, responseErr } bw := io.NewBufBinWriter() if len(reqParams) > 1 { args, err := reqParams[1].GetArray() // second `invokecontractverify` parameter is an array of arguments for `verify` method if err != nil { - return nil, response.WrapErrorWithData(response.ErrInvalidParams, err) + return util.Uint160{}, nil, nil, response.WrapErrorWithData(response.ErrInvalidParams, err) } if len(args) > 0 { err := request.ExpandArrayIntoScript(bw.BinWriter, args) if err != nil { - return nil, response.NewRPCError("can't create witness invocation script", err.Error(), err) + return util.Uint160{}, nil, nil, response.NewRPCError("can't create witness invocation script", err.Error(), err) } } } @@ -1672,7 +1744,7 @@ func (s *Server) invokeContractVerify(reqParams request.Params) (interface{}, *r if len(reqParams) > 2 { signers, witnesses, err := reqParams[2].GetSignersWithWitnesses() if err != nil { - return nil, response.ErrInvalidParams + return util.Uint160{}, nil, nil, response.ErrInvalidParams } tx.Signers = signers tx.Scripts = witnesses @@ -1680,16 +1752,51 @@ func (s *Server) invokeContractVerify(reqParams request.Params) (interface{}, *r tx.Signers = []transaction.Signer{{Account: scriptHash}} tx.Scripts = []transaction.Witness{{InvocationScript: invocationScript, VerificationScript: []byte{}}} } - return s.runScriptInVM(trigger.Verification, invocationScript, scriptHash, tx, false) + return scriptHash, tx, invocationScript, nil } -func (s *Server) getFakeNextBlock() (*block.Block, error) { +// getHistoricParams checks that historic calls are supported and returns fake block +// with the specified index to perform the historic call. It also checks that +// specified stateroot is stored at the specified height for further request +// handling consistency. +func (s *Server) getHistoricParams(reqParams request.Params) (*block.Block, *response.Error) { + if s.chain.GetConfig().KeepOnlyLatestState { + return nil, response.NewInvalidRequestError("only latest state is supported", errKeepOnlyLatestState) + } + if len(reqParams) < 1 { + return nil, response.ErrInvalidParams + } + height, respErr := s.blockHeightFromParam(reqParams.Value(0)) + if respErr != nil { + hash, err := reqParams.Value(0).GetUint256() + if err != nil { + return nil, response.NewInvalidParamsError("invalid block hash or index or stateroot hash", err) + } + b, err := s.chain.GetBlock(hash) + if err != nil { + stateH, err := s.chain.GetStateModule().GetLatestStateHeight(hash) + if err != nil { + return nil, response.NewInvalidParamsError(fmt.Sprintf("unknown block or stateroot: %s", err), err) + } + height = int(stateH) + } else { + height = int(b.Index) + } + } + b, err := s.getFakeNextBlock(uint32(height)) + if err != nil { + return nil, response.NewInternalServerError(fmt.Sprintf("can't create fake block for height %d: %s", height, err.Error()), err) + } + return b, nil +} + +func (s *Server) getFakeNextBlock(nextBlockHeight uint32) (*block.Block, error) { // When transferring funds, script execution does no auto GAS claim, // because it depends on persisting tx height. // This is why we provide block here. b := block.New(s.stateRootEnabled) - b.Index = s.chain.BlockHeight() + 1 - hdr, err := s.chain.GetHeader(s.chain.GetHeaderHash(int(s.chain.BlockHeight()))) + b.Index = nextBlockHeight + hdr, err := s.chain.GetHeader(s.chain.GetHeaderHash(int(nextBlockHeight - 1))) if err != nil { return nil, err } @@ -1702,12 +1809,23 @@ func (s *Server) getFakeNextBlock() (*block.Block, error) { // witness invocation script in case of `verification` trigger (it pushes `verify` // arguments on stack before verification). In case of contract verification // contractScriptHash should be specified. -func (s *Server) runScriptInVM(t trigger.Type, script []byte, contractScriptHash util.Uint160, tx *transaction.Transaction, verbose bool) (*result.Invoke, *response.Error) { - b, err := s.getFakeNextBlock() - if err != nil { - return nil, response.NewInternalServerError("can't create fake block", err) +func (s *Server) runScriptInVM(t trigger.Type, script []byte, contractScriptHash util.Uint160, tx *transaction.Transaction, b *block.Block, verbose bool) (*result.Invoke, *response.Error) { + var ( + err error + ic *interop.Context + ) + if b == nil { + b, err = s.getFakeNextBlock(s.chain.BlockHeight() + 1) + if err != nil { + return nil, response.NewInternalServerError("can't create fake block", err) + } + ic = s.chain.GetTestVM(t, tx, b) + } else { + ic, err = s.chain.GetTestHistoricVM(t, tx, b) + if err != nil { + return nil, response.NewInternalServerError("failed to create historic VM", err) + } } - ic := s.chain.GetTestVM(t, tx, b) if verbose { ic.VM.EnableInvocationTree() } @@ -1720,7 +1838,7 @@ 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) } diff --git a/pkg/rpc/server/server_test.go b/pkg/rpc/server/server_test.go index 868e19eb4..b7bfc46fc 100644 --- a/pkg/rpc/server/server_test.go +++ b/pkg/rpc/server/server_test.go @@ -75,6 +75,7 @@ const ( nfsoContractHash = "5f9ebd6b001b54c7bc70f96e0412fcf415dfe09f" nfsoToken1ID = "7e244ffd6aa85fb1579d2ed22e9b761ab62e3486" invokescriptContractAVM = "VwIADBQBDAMOBQYMDQIODw0DDgcJAAAAAErZMCQE2zBwaEH4J+yMqiYEEUAMFA0PAwIJAAIBAwcDBAUCAQAOBgwJStkwJATbMHFpQfgn7IyqJgQSQBNA" + block20StateRootLE = "19ec3c3d01afe5274e8bb4a393c97da708c5608c5b0ad116c16108b6a04fb08e" ) var ( @@ -999,6 +1000,134 @@ var rpcTestCases = map[string][]rpcTestCase{ fail: true, }, }, + "invokefunctionhistoric": { + { + name: "positive, by index", + params: `[20, "50befd26fdf6e4d957c11e078b24ebce6291456f", "test", []]`, + result: func(e *executor) interface{} { return &result.Invoke{} }, + check: func(t *testing.T, e *executor, inv interface{}) { + res, ok := inv.(*result.Invoke) + require.True(t, ok) + assert.NotNil(t, res.Script) + assert.NotEqual(t, "", res.State) + assert.NotEqual(t, 0, res.GasConsumed) + }, + }, + { + name: "positive, by stateroot", + params: `["` + block20StateRootLE + `", "50befd26fdf6e4d957c11e078b24ebce6291456f", "test", []]`, + result: func(e *executor) interface{} { return &result.Invoke{} }, + check: func(t *testing.T, e *executor, inv interface{}) { + res, ok := inv.(*result.Invoke) + require.True(t, ok) + assert.NotNil(t, res.Script) + assert.NotEqual(t, "", res.State) + assert.NotEqual(t, 0, res.GasConsumed) + }, + }, + { + name: "positive, with notifications", + params: `[20, "` + nnsContractHash + `", "transfer", [{"type":"Hash160", "value":"0x0bcd2978634d961c24f5aea0802297ff128724d6"},{"type":"String", "value":"neo.com"},{"type":"Any", "value":null}],["0xb248508f4ef7088e10c48f14d04be3272ca29eee"]]`, + result: func(e *executor) interface{} { + script := []byte{0x0b, 0x0c, 0x07, 0x6e, 0x65, 0x6f, 0x2e, 0x63, 0x6f, 0x6d, 0x0c, 0x14, 0xd6, 0x24, 0x87, 0x12, 0xff, 0x97, 0x22, 0x80, 0xa0, 0xae, 0xf5, 0x24, 0x1c, 0x96, 0x4d, 0x63, 0x78, 0x29, 0xcd, 0xb, 0x13, 0xc0, 0x1f, 0xc, 0x8, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x65, 0x72, 0xc, 0x14, 0x1f, 0xe2, 0x37, 0x5c, 0xdc, 0xdb, 0xb2, 0x80, 0x40, 0x78, 0x65, 0x35, 0xd5, 0xef, 0xe4, 0x3, 0x39, 0x56, 0x92, 0xee, 0x41, 0x62, 0x7d, 0x5b, 0x52} + return &result.Invoke{ + State: "HALT", + GasConsumed: 32167260, + Script: script, + Stack: []stackitem.Item{stackitem.Make(true)}, + Notifications: []state.NotificationEvent{{ + ScriptHash: nnsHash, + Name: "Transfer", + Item: stackitem.NewArray([]stackitem.Item{ + stackitem.Make([]byte{0xee, 0x9e, 0xa2, 0x2c, 0x27, 0xe3, 0x4b, 0xd0, 0x14, 0x8f, 0xc4, 0x10, 0x8e, 0x08, 0xf7, 0x4e, 0x8f, 0x50, 0x48, 0xb2}), + stackitem.Make([]byte{0xd6, 0x24, 0x87, 0x12, 0xff, 0x97, 0x22, 0x80, 0xa0, 0xae, 0xf5, 0x24, 0x1c, 0x96, 0x4d, 0x63, 0x78, 0x29, 0xcd, 0x0b}), + stackitem.Make(1), + stackitem.Make("neo.com"), + }), + }}, + } + }, + }, + { + name: "positive, verbose", + params: `[20, "` + nnsContractHash + `", "resolve", [{"type":"String", "value":"neo.com"},{"type":"Integer","value":1}], [], true]`, + result: func(e *executor) interface{} { + script := []byte{0x11, 0xc, 0x7, 0x6e, 0x65, 0x6f, 0x2e, 0x63, 0x6f, 0x6d, 0x12, 0xc0, 0x1f, 0xc, 0x7, 0x72, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0xc, 0x14, 0x1f, 0xe2, 0x37, 0x5c, 0xdc, 0xdb, 0xb2, 0x80, 0x40, 0x78, 0x65, 0x35, 0xd5, 0xef, 0xe4, 0x3, 0x39, 0x56, 0x92, 0xee, 0x41, 0x62, 0x7d, 0x5b, 0x52} + stdHash, _ := e.chain.GetNativeContractScriptHash(nativenames.StdLib) + cryptoHash, _ := e.chain.GetNativeContractScriptHash(nativenames.CryptoLib) + return &result.Invoke{ + State: "HALT", + GasConsumed: 15928320, + Script: script, + Stack: []stackitem.Item{stackitem.Make("1.2.3.4")}, + Notifications: []state.NotificationEvent{}, + Diagnostics: &result.InvokeDiag{ + Changes: []storage.Operation{}, + Invocations: []*vm.InvocationTree{{ + Current: hash.Hash160(script), + Calls: []*vm.InvocationTree{ + { + Current: nnsHash, + Calls: []*vm.InvocationTree{ + { + Current: stdHash, + }, + { + Current: cryptoHash, + }, + { + Current: stdHash, + }, + { + Current: cryptoHash, + }, + { + Current: cryptoHash, + }, + }, + }, + }, + }}, + }, + } + }, + }, + { + name: "no params", + params: `[]`, + fail: true, + }, + { + name: "no args", + params: `[20]`, + fail: true, + }, + { + name: "not a string", + params: `[20, 42, "test", []]`, + fail: true, + }, + { + name: "not a scripthash", + params: `[20,"qwerty", "test", []]`, + fail: true, + }, + { + name: "bad params", + params: `[20,"50befd26fdf6e4d957c11e078b24ebce6291456f", "test", [{"type": "Integer", "value": "qwerty"}]]`, + fail: true, + }, + { + name: "bad height", + params: `[100500,"50befd26fdf6e4d957c11e078b24ebce6291456f", "test", [{"type": "Integer", "value": 1}]]`, + fail: true, + }, + { + name: "bad stateroot", + params: `["` + util.Uint256{1, 2, 3}.StringLE() + `","50befd26fdf6e4d957c11e078b24ebce6291456f", "test", [{"type": "Integer", "value": 1}]]`, + fail: true, + }, + }, "invokescript": { { name: "positive", @@ -1098,6 +1227,132 @@ var rpcTestCases = map[string][]rpcTestCase{ fail: true, }, }, + "invokescripthistoric": { + { + name: "positive, by index", + params: `[20,"UcVrDUhlbGxvLCB3b3JsZCFoD05lby5SdW50aW1lLkxvZ2FsdWY="]`, + result: func(e *executor) interface{} { return &result.Invoke{} }, + check: func(t *testing.T, e *executor, inv interface{}) { + res, ok := inv.(*result.Invoke) + require.True(t, ok) + assert.NotEqual(t, "", res.Script) + assert.NotEqual(t, "", res.State) + assert.NotEqual(t, 0, res.GasConsumed) + }, + }, + { + name: "positive, by stateroot", + params: `["` + block20StateRootLE + `","UcVrDUhlbGxvLCB3b3JsZCFoD05lby5SdW50aW1lLkxvZ2FsdWY="]`, + result: func(e *executor) interface{} { return &result.Invoke{} }, + check: func(t *testing.T, e *executor, inv interface{}) { + res, ok := inv.(*result.Invoke) + require.True(t, ok) + assert.NotEqual(t, "", res.Script) + assert.NotEqual(t, "", res.State) + assert.NotEqual(t, 0, res.GasConsumed) + }, + }, + { + name: "positive,verbose", + params: `[20, "UcVrDUhlbGxvLCB3b3JsZCFoD05lby5SdW50aW1lLkxvZ2FsdWY=",[],true]`, + result: func(e *executor) interface{} { + script := []byte{0x51, 0xc5, 0x6b, 0xd, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x2c, 0x20, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x21, 0x68, 0xf, 0x4e, 0x65, 0x6f, 0x2e, 0x52, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x4c, 0x6f, 0x67, 0x61, 0x6c, 0x75, 0x66} + return &result.Invoke{ + State: "FAULT", + GasConsumed: 60, + Script: script, + Stack: []stackitem.Item{}, + FaultException: "at instruction 0 (ROT): too big index", + Notifications: []state.NotificationEvent{}, + Diagnostics: &result.InvokeDiag{ + Changes: []storage.Operation{}, + Invocations: []*vm.InvocationTree{{ + Current: hash.Hash160(script), + }}, + }, + } + }, + }, + { + name: "positive, good witness", + // script is base64-encoded `invokescript_contract.avm` representation, hashes are hex-encoded LE bytes of hashes used in the contract with `0x` prefix + params: fmt.Sprintf(`[20,"%s",["0x0000000009070e030d0f0e020d0c06050e030c01","0x090c060e00010205040307030102000902030f0d"]]`, invokescriptContractAVM), + result: func(e *executor) interface{} { return &result.Invoke{} }, + check: func(t *testing.T, e *executor, inv interface{}) { + res, ok := inv.(*result.Invoke) + require.True(t, ok) + assert.Equal(t, "HALT", res.State) + require.Equal(t, 1, len(res.Stack)) + require.Equal(t, big.NewInt(3), res.Stack[0].Value()) + }, + }, + { + name: "positive, bad witness of second hash", + params: fmt.Sprintf(`[20,"%s",["0x0000000009070e030d0f0e020d0c06050e030c01"]]`, invokescriptContractAVM), + result: func(e *executor) interface{} { return &result.Invoke{} }, + check: func(t *testing.T, e *executor, inv interface{}) { + res, ok := inv.(*result.Invoke) + require.True(t, ok) + assert.Equal(t, "HALT", res.State) + require.Equal(t, 1, len(res.Stack)) + require.Equal(t, big.NewInt(2), res.Stack[0].Value()) + }, + }, + { + name: "positive, no good hashes", + params: fmt.Sprintf(`[20,"%s"]`, invokescriptContractAVM), + result: func(e *executor) interface{} { return &result.Invoke{} }, + check: func(t *testing.T, e *executor, inv interface{}) { + res, ok := inv.(*result.Invoke) + require.True(t, ok) + assert.Equal(t, "HALT", res.State) + require.Equal(t, 1, len(res.Stack)) + require.Equal(t, big.NewInt(1), res.Stack[0].Value()) + }, + }, + { + name: "positive, bad hashes witness", + params: fmt.Sprintf(`[20,"%s",["0x0000000009070e030d0f0e020d0c06050e030c02"]]`, invokescriptContractAVM), + result: func(e *executor) interface{} { return &result.Invoke{} }, + check: func(t *testing.T, e *executor, inv interface{}) { + res, ok := inv.(*result.Invoke) + require.True(t, ok) + assert.Equal(t, "HALT", res.State) + assert.Equal(t, 1, len(res.Stack)) + assert.Equal(t, big.NewInt(1), res.Stack[0].Value()) + }, + }, + { + name: "no params", + params: `[]`, + fail: true, + }, + { + name: "no script", + params: `[20]`, + fail: true, + }, + { + name: "not a string", + params: `[20,42]`, + fail: true, + }, + { + name: "bas string", + params: `[20, "qwerty"]`, + fail: true, + }, + { + name: "bas height", + params: `[100500,"qwerty"]`, + fail: true, + }, + { + name: "bas stateroot", + params: `["` + util.Uint256{1, 2, 3}.StringLE() + `","UcVrDUhlbGxvLCB3b3JsZCFoD05lby5SdW50aW1lLkxvZ2FsdWY="]`, + fail: true, + }, + }, "invokecontractverify": { { name: "positive", @@ -1203,6 +1458,129 @@ var rpcTestCases = map[string][]rpcTestCase{ fail: true, }, }, + "invokecontractverifyhistoric": { + { + name: "positive, by index", + params: fmt.Sprintf(`[20,"%s", [], [{"account":"%s"}]]`, verifyContractHash, testchain.PrivateKeyByID(0).PublicKey().GetScriptHash().StringLE()), + result: func(e *executor) interface{} { return &result.Invoke{} }, + check: func(t *testing.T, e *executor, inv interface{}) { + res, ok := inv.(*result.Invoke) + require.True(t, ok) + assert.Nil(t, res.Script) // empty witness invocation script (pushes args of `verify` on stack, but this `verify` don't have args) + assert.Equal(t, "HALT", res.State) + assert.NotEqual(t, 0, res.GasConsumed) + assert.Equal(t, true, res.Stack[0].Value().(bool), fmt.Sprintf("check address in verification_contract.go: expected %s", testchain.PrivateKeyByID(0).Address())) + }, + }, + { + name: "positive, by stateroot", + params: fmt.Sprintf(`["`+block20StateRootLE+`","%s", [], [{"account":"%s"}]]`, verifyContractHash, testchain.PrivateKeyByID(0).PublicKey().GetScriptHash().StringLE()), + result: func(e *executor) interface{} { return &result.Invoke{} }, + check: func(t *testing.T, e *executor, inv interface{}) { + res, ok := inv.(*result.Invoke) + require.True(t, ok) + assert.Nil(t, res.Script) // empty witness invocation script (pushes args of `verify` on stack, but this `verify` don't have args) + assert.Equal(t, "HALT", res.State) + assert.NotEqual(t, 0, res.GasConsumed) + assert.Equal(t, true, res.Stack[0].Value().(bool), fmt.Sprintf("check address in verification_contract.go: expected %s", testchain.PrivateKeyByID(0).Address())) + }, + }, + { + name: "positive, no signers", + params: fmt.Sprintf(`[20,"%s", []]`, verifyContractHash), + result: func(e *executor) interface{} { return &result.Invoke{} }, + check: func(t *testing.T, e *executor, inv interface{}) { + res, ok := inv.(*result.Invoke) + require.True(t, ok) + assert.Nil(t, res.Script) + assert.Equal(t, "HALT", res.State, res.FaultException) + assert.NotEqual(t, 0, res.GasConsumed) + assert.Equal(t, false, res.Stack[0].Value().(bool)) + }, + }, + { + name: "positive, no arguments", + params: fmt.Sprintf(`[20,"%s"]`, verifyContractHash), + result: func(e *executor) interface{} { return &result.Invoke{} }, + check: func(t *testing.T, e *executor, inv interface{}) { + res, ok := inv.(*result.Invoke) + require.True(t, ok) + assert.Nil(t, res.Script) + assert.Equal(t, "HALT", res.State, res.FaultException) + assert.NotEqual(t, 0, res.GasConsumed) + assert.Equal(t, false, res.Stack[0].Value().(bool)) + }, + }, + { + name: "positive, with signers and scripts", + params: fmt.Sprintf(`[20,"%s", [], [{"account":"%s", "invocation":"MQo=", "verification": ""}]]`, verifyContractHash, testchain.PrivateKeyByID(0).PublicKey().GetScriptHash().StringLE()), + result: func(e *executor) interface{} { return &result.Invoke{} }, + check: func(t *testing.T, e *executor, inv interface{}) { + res, ok := inv.(*result.Invoke) + require.True(t, ok) + assert.Nil(t, res.Script) + assert.Equal(t, "HALT", res.State) + assert.NotEqual(t, 0, res.GasConsumed) + assert.Equal(t, true, res.Stack[0].Value().(bool)) + }, + }, + { + name: "positive, with arguments, result=true", + params: fmt.Sprintf(`[20,"%s", [{"type": "String", "value": "good_string"}, {"type": "Integer", "value": "4"}, {"type":"Boolean", "value": false}]]`, verifyWithArgsContractHash), + result: func(e *executor) interface{} { return &result.Invoke{} }, + check: func(t *testing.T, e *executor, inv interface{}) { + res, ok := inv.(*result.Invoke) + require.True(t, ok) + expectedInvScript := io.NewBufBinWriter() + emit.Int(expectedInvScript.BinWriter, 0) + emit.Int(expectedInvScript.BinWriter, int64(4)) + emit.String(expectedInvScript.BinWriter, "good_string") + require.NoError(t, expectedInvScript.Err) + assert.Equal(t, expectedInvScript.Bytes(), res.Script) // witness invocation script (pushes args of `verify` on stack) + assert.Equal(t, "HALT", res.State, res.FaultException) + assert.NotEqual(t, 0, res.GasConsumed) + assert.Equal(t, true, res.Stack[0].Value().(bool)) + }, + }, + { + name: "positive, with arguments, result=false", + params: fmt.Sprintf(`[20, "%s", [{"type": "String", "value": "invalid_string"}, {"type": "Integer", "value": "4"}, {"type":"Boolean", "value": false}]]`, verifyWithArgsContractHash), + result: func(e *executor) interface{} { return &result.Invoke{} }, + check: func(t *testing.T, e *executor, inv interface{}) { + res, ok := inv.(*result.Invoke) + require.True(t, ok) + expectedInvScript := io.NewBufBinWriter() + emit.Int(expectedInvScript.BinWriter, 0) + emit.Int(expectedInvScript.BinWriter, int64(4)) + emit.String(expectedInvScript.BinWriter, "invalid_string") + require.NoError(t, expectedInvScript.Err) + assert.Equal(t, expectedInvScript.Bytes(), res.Script) + assert.Equal(t, "HALT", res.State, res.FaultException) + assert.NotEqual(t, 0, res.GasConsumed) + assert.Equal(t, false, res.Stack[0].Value().(bool)) + }, + }, + { + name: "unknown contract", + params: fmt.Sprintf(`[20, "%s", []]`, util.Uint160{}.String()), + fail: true, + }, + { + name: "no params", + params: `[]`, + fail: true, + }, + { + name: "no args", + params: `[20]`, + fail: true, + }, + { + name: "not a string", + params: `[20,42, []]`, + fail: true, + }, + }, "sendrawtransaction": { { name: "positive", From 93fde3b4e67b5893683f49c0af1ffced67a88952 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Fri, 22 Apr 2022 13:48:07 +0300 Subject: [PATCH 04/24] core: fix typo in TestCreateBasicChain --- pkg/core/basic_chain_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/core/basic_chain_test.go b/pkg/core/basic_chain_test.go index 480db1eb8..d460685bb 100644 --- a/pkg/core/basic_chain_test.go +++ b/pkg/core/basic_chain_test.go @@ -76,7 +76,7 @@ func TestCreateBasicChain(t *testing.T) { func initBasicChain(t *testing.T, e *neotest.Executor) { if !e.Chain.GetConfig().P2PSigExtensions { - t.Fatal("P2PSitExtensions should be enabled to init basic chain") + t.Fatal("P2PSigExtensions should be enabled to init basic chain") } const neoAmount = 99999000 From e63d6aeff7a57ff6506320416b094cff9d6784d4 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Wed, 13 Apr 2022 14:08:44 +0300 Subject: [PATCH 05/24] core: move natives cache initialisation to a separate method --- pkg/core/blockchain.go | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index 2c36e6fd2..02d930896 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -433,14 +433,9 @@ func (bc *Blockchain) init() error { return fmt.Errorf("can't init MPT at height %d: %w", bHeight, err) } - err = bc.contracts.NEO.InitializeCache(bc, bc.dao) + err = bc.initializeNativeCache(bc.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(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,19 @@ func (bc *Blockchain) jumpToStateInternal(p uint32, stage stateJumpStage) error return nil } +func (bc *Blockchain) initializeNativeCache(d *dao.Simple) error { + err := bc.contracts.NEO.InitializeCache(bc, 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) + } + bc.contracts.Designate.InitializeCache() + 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() { From 812fa3f76afbcbcf5096a0422d41256be12ed9c6 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Wed, 13 Apr 2022 16:09:39 +0300 Subject: [PATCH 06/24] core: initialize NEO config cache in constructor It isn't changed within the contract lifetime, thus initialisation can be safely performed in constructor. --- pkg/core/native/contract.go | 2 +- pkg/core/native/native_neo.go | 24 +++++++++++------------- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/pkg/core/native/contract.go b/pkg/core/native/contract.go index 13f8ae0d9..d6deaa375 100644 --- a/pkg/core/native/contract.go +++ b/pkg/core/native/contract.go @@ -76,7 +76,7 @@ func NewContracts(cfg config.ProtocolConfiguration) *Contracts { cs.Contracts = append(cs.Contracts, ledger) gas := newGAS(int64(cfg.InitialGASSupply), cfg.P2PSigExtensions) - neo := newNEO() + neo := newNEO(cfg) policy := newPolicy() neo.GAS = gas neo.Policy = policy diff --git a/pkg/core/native/native_neo.go b/pkg/core/native/native_neo.go index 8ded7a5d5..830682790 100644 --- a/pkg/core/native/native_neo.go +++ b/pkg/core/native/native_neo.go @@ -58,7 +58,8 @@ type NEO struct { // 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 + + // Configuration and standby keys are set in constructor and then // only read from. cfg config.ProtocolConfiguration standbyKeys keys.PublicKeys @@ -116,7 +117,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() @@ -136,6 +137,11 @@ func newNEO() *NEO { 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), manifest.NewParameter("end", smartcontract.IntegerType)) @@ -198,10 +204,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 } @@ -213,7 +215,7 @@ func (n *NEO) Initialize(ic *interop.Context) error { committee0 := n.standbyKeys[:n.cfg.GetCommitteeSize(ic.Block.Index)] cvs := toKeysWithVotes(committee0) - err = n.updateCache(cvs, ic.Chain) + err := n.updateCache(cvs, ic.Chain) if err != nil { return err } @@ -245,10 +247,6 @@ func (n *NEO) Initialize(ic *interop.Context) error { // 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 - } var committee = keysWithVotes{} si := d.GetStorageItem(n.ID, prefixCommittee) if err := committee.DecodeBytes(si); err != nil { @@ -264,10 +262,10 @@ func (n *NEO) InitializeCache(bc interop.Ledger, d *dao.Simple) error { 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 } From aa886f67ce0b55e17b0069a4892a1490236810ca Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Tue, 12 Apr 2022 17:29:11 +0300 Subject: [PATCH 07/24] core: use dao-binded cache for native contracts All native cached values are binded to DAO, so that it's possible to properly handle historic calls. --- pkg/compiler/interop_test.go | 3 +- pkg/core/blockchain.go | 43 +++++-- pkg/core/blockchain_core_test.go | 3 +- pkg/core/interop_system_core_test.go | 9 +- pkg/core/interops_test.go | 5 +- pkg/core/native/designate.go | 64 ++++++---- pkg/core/native/management.go | 112 +++++++++------- pkg/core/native/management_test.go | 12 +- pkg/core/native/native_gas.go | 2 +- pkg/core/native/native_neo.go | 183 ++++++++++++++++----------- pkg/core/native/notary.go | 67 ++++++---- pkg/core/native/oracle.go | 39 ++++-- pkg/core/native/policy.go | 165 ++++++++++++++---------- pkg/core/storage/memcached_store.go | 40 ++++++ pkg/rpc/server/client_test.go | 8 +- pkg/rpc/server/server.go | 2 +- 16 files changed, 475 insertions(+), 282 deletions(-) diff --git a/pkg/compiler/interop_test.go b/pkg/compiler/interop_test.go index 3243f4a1d..120e29709 100644 --- a/pkg/compiler/interop_test.go +++ b/pkg/compiler/interop_test.go @@ -201,7 +201,8 @@ func TestAppCall(t *testing.T) { } fc := fakechain.NewFakeChain() - ic := interop.NewContext(trigger.Application, fc, dao.NewSimple(storage.NewMemoryStore(), false, false), interop.DefaultBaseExecFee, native.DefaultStoragePrice, contractGetter, nil, nil, nil, zaptest.NewLogger(t)) + ic := interop.NewContext(trigger.Application, fc, dao.NewSimple(storage.NewMemoryStore(), false, false), + interop.DefaultBaseExecFee, native.DefaultStoragePrice, contractGetter, nil, nil, nil, zaptest.NewLogger(t)) t.Run("valid script", func(t *testing.T) { src := getAppCallScript(fmt.Sprintf("%#v", ih.BytesBE())) diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index 02d930896..e53047c1d 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -594,7 +594,18 @@ func (bc *Blockchain) initializeNativeCache(d *dao.Simple) error { if err != nil { return fmt.Errorf("can't init cache for Management native contract: %w", err) } - bc.contracts.Designate.InitializeCache() + bc.contracts.Designate.InitializeCache(d) + 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 } @@ -1223,14 +1234,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) @@ -1454,12 +1465,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 @@ -1826,7 +1837,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, @@ -1942,7 +1953,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...) @@ -1966,11 +1977,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) } @@ -2064,7 +2075,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 { @@ -2122,7 +2133,7 @@ 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 } @@ -2134,7 +2145,7 @@ func (bc *Blockchain) GetValidators() ([]*keys.PublicKey, error) { // 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. @@ -2173,6 +2184,12 @@ func (bc *Blockchain) GetTestHistoricVM(t trigger.Type, tx *transaction.Transact 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(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) diff --git a/pkg/core/blockchain_core_test.go b/pkg/core/blockchain_core_test.go index fbfe43213..a0ccae047 100644 --- a/pkg/core/blockchain_core_test.go +++ b/pkg/core/blockchain_core_test.go @@ -11,7 +11,6 @@ import ( "github.com/nspcc-dev/neo-go/internal/testchain" "github.com/nspcc-dev/neo-go/pkg/config" "github.com/nspcc-dev/neo-go/pkg/core/block" - "github.com/nspcc-dev/neo-go/pkg/core/dao" "github.com/nspcc-dev/neo-go/pkg/core/state" "github.com/nspcc-dev/neo-go/pkg/core/storage" "github.com/nspcc-dev/neo-go/pkg/core/transaction" @@ -329,7 +328,7 @@ func TestBlockchain_BaseExecFeeBaseStoragePrice_Compat(t *testing.T) { bc := newTestChain(t) check := func(t *testing.T) { - ic := bc.newInteropContext(trigger.Application, dao.NewSimple(storage.NewMemoryStore(), bc.config.StateRootInHeader, bc.config.P2PSigExtensions), bc.topBlock.Load().(*block.Block), nil) + ic := bc.newInteropContext(trigger.Application, bc.dao, bc.topBlock.Load().(*block.Block), nil) require.Equal(t, bc.GetBaseExecFee(), ic.BaseExecFee()) require.Equal(t, bc.GetStoragePrice(), ic.BaseStorageFee()) } diff --git a/pkg/core/interop_system_core_test.go b/pkg/core/interop_system_core_test.go index 5224c0e4c..2407b5e46 100644 --- a/pkg/core/interop_system_core_test.go +++ b/pkg/core/interop_system_core_test.go @@ -530,10 +530,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) + dao.NewSimple(chain.dao.Store, chain.config.StateRootInHeader, chain.config.P2PSigExtensions), nil, nil) v := context.SpawnVM() return v, context, chain } @@ -552,10 +552,7 @@ func createVMAndContractState(t testing.TB) (*vm.VM, *state.Contract, *interop.C }, } - chain := newTestChain(t) - d := dao.NewSimple(storage.NewMemoryStore(), chain.config.StateRootInHeader, chain.config.P2PSigExtensions) - context := chain.newInteropContext(trigger.Application, d, nil, nil) - v := context.SpawnVM() + v, context, chain := createVM(t) return v, contractState, context, chain } diff --git a/pkg/core/interops_test.go b/pkg/core/interops_test.go index ef4dde7bf..fb31c0e25 100644 --- a/pkg/core/interops_test.go +++ b/pkg/core/interops_test.go @@ -5,9 +5,7 @@ import ( "runtime" "testing" - "github.com/nspcc-dev/neo-go/pkg/core/dao" "github.com/nspcc-dev/neo-go/pkg/core/interop" - "github.com/nspcc-dev/neo-go/pkg/core/storage" "github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger" "github.com/nspcc-dev/neo-go/pkg/vm" "github.com/stretchr/testify/require" @@ -17,8 +15,7 @@ func testNonInterop(t *testing.T, value interface{}, f func(*interop.Context) er v := vm.New() v.Estack().PushVal(value) chain := newTestChain(t) - d := dao.NewSimple(storage.NewMemoryStore(), chain.config.StateRootInHeader, chain.config.P2PSigExtensions) - context := chain.newInteropContext(trigger.Application, d, nil, nil) + context := chain.newInteropContext(trigger.Application, chain.dao, nil, nil) context.VM = v require.Error(t, f(context)) } diff --git a/pkg/core/native/designate.go b/pkg/core/native/designate.go index 7a33e667a..785bd2f9f 100644 --- a/pkg/core/native/designate.go +++ b/pkg/core/native/designate.go @@ -31,12 +31,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 +47,14 @@ type roleData struct { height uint32 } +type DesignationCache struct { + rolesChangedFlag atomic.Value + oracles atomic.Value + stateVals atomic.Value + neofsAlphabet atomic.Value + notaries atomic.Value +} + const ( designateContractID = -8 @@ -104,6 +106,9 @@ func newDesignate(p2pSigExtensionsEnabled bool) *Designate { // Initialize initializes Oracle contract. func (s *Designate) Initialize(ic *interop.Context) error { + cache := &DesignationCache{} + cache.rolesChangedFlag.Store(true) + ic.DAO.Store.SetCache(s.ID, cache) return nil } @@ -114,26 +119,27 @@ 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.Store.GetCache(s.ID).(*DesignationCache) + if !rolesChanged(cache) { return nil } - if err := s.updateCachedRoleData(&s.oracles, ic.DAO, noderoles.Oracle); err != nil { + if err := s.updateCachedRoleData(&cache.oracles, ic.DAO, noderoles.Oracle); err != nil { return err } - if err := s.updateCachedRoleData(&s.stateVals, ic.DAO, noderoles.StateValidator); err != nil { + if err := s.updateCachedRoleData(&cache.stateVals, ic.DAO, noderoles.StateValidator); err != nil { return err } - if err := s.updateCachedRoleData(&s.neofsAlphabet, ic.DAO, noderoles.NeoFSAlphabet); err != nil { + if err := s.updateCachedRoleData(&cache.neofsAlphabet, ic.DAO, noderoles.NeoFSAlphabet); err != nil { return err } if s.p2pSigExtensionsEnabled { - if err := s.updateCachedRoleData(&s.notaries, ic.DAO, noderoles.P2PNotary); err != nil { + if err := s.updateCachedRoleData(&cache.notaries, ic.DAO, noderoles.P2PNotary); err != nil { return err } } - s.rolesChangedFlag.Store(false) + cache.rolesChangedFlag.Store(false) return nil } @@ -162,8 +168,8 @@ func (s *Designate) getDesignatedByRole(ic *interop.Context, args []stackitem.It return pubsToArray(pubs) } -func (s *Designate) rolesChanged() bool { - rc := s.rolesChangedFlag.Load() +func rolesChanged(cache *DesignationCache) bool { + rc := cache.rolesChangedFlag.Load() return rc == nil || rc.(bool) } @@ -208,17 +214,17 @@ func (s *Designate) updateCachedRoleData(v *atomic.Value, d *dao.Simple, r noder return nil } -func (s *Designate) getCachedRoleData(r noderoles.Role) *roleData { +func getCachedRoleData(cache *DesignationCache, r noderoles.Role) *roleData { var val interface{} switch r { case noderoles.Oracle: - val = s.oracles.Load() + val = cache.oracles.Load() case noderoles.StateValidator: - val = s.stateVals.Load() + val = cache.stateVals.Load() case noderoles.NeoFSAlphabet: - val = s.neofsAlphabet.Load() + val = cache.neofsAlphabet.Load() case noderoles.P2PNotary: - val = s.notaries.Load() + val = cache.notaries.Load() } if val != nil { return val.(*roleData) @@ -231,8 +237,9 @@ 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 { + cache := d.Store.GetCache(s.ID).(*DesignationCache) + if !rolesChanged(cache) { + if val := getCachedRoleData(cache, r); val != nil { return val.addr, nil } } @@ -249,8 +256,9 @@ 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.Store.GetCache(s.ID).(*DesignationCache) + if !rolesChanged(cache) { + if val := getCachedRoleData(cache, r); val != nil && val.height <= index { return val.nodes.Copy(), val.height, nil } } @@ -310,7 +318,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,7 +335,7 @@ func (s *Designate) DesignateAsRole(ic *interop.Context, r noderoles.Role, pubs } sort.Sort(pubs) nl := NodeList(pubs) - s.rolesChangedFlag.Store(true) + ic.DAO.Store.GetCache(s.ID).(*DesignationCache).rolesChangedFlag.Store(true) err := putConvertibleToDAO(s.ID, ic.DAO, key, &nl) if err != nil { return err @@ -357,6 +365,8 @@ func (s *Designate) getRole(item stackitem.Item) (noderoles.Role, bool) { } // InitializeCache invalidates native Designate cache. -func (s *Designate) InitializeCache() { - s.rolesChangedFlag.Store(true) +func (s *Designate) InitializeCache(d *dao.Simple) { + cache := &DesignationCache{} + cache.rolesChangedFlag.Store(true) + d.Store.SetCache(s.ID, cache) } diff --git a/pkg/core/native/management.go b/pkg/core/native/management.go index dc9257331..9fcd8a6f1 100644 --- a/pkg/core/native/management.go +++ b/pkg/core/native/management.go @@ -30,7 +30,9 @@ import ( type Management struct { interop.ContractMD NEO *NEO +} +type ManagementCache struct { mtx sync.RWMutex contracts map[util.Uint160]*state.Contract // nep11 is a map of NEP11-compliant contracts which is updated with every PostPersist. @@ -66,9 +68,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,9 +145,10 @@ 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.Store.GetCache(m.ID).(*ManagementCache) + cache.mtx.RLock() + cs, ok := cache.contracts[hash] + cache.mtx.RUnlock() if !ok { return nil, storage.ErrKeyNotFound } else if cs != nil { @@ -260,11 +260,12 @@ func (m *Management) deployWithData(ic *interop.Context, args []stackitem.Item) return contractToStack(newcontract) } -func (m *Management) markUpdated(h util.Uint160) { - m.mtx.Lock() +func (m *Management) markUpdated(d *dao.Simple, h util.Uint160) { + cache := d.Store.GetCache(m.ID).(*ManagementCache) + cache.mtx.Lock() // Just set it to nil, to refresh cache in `PostPersist`. - m.contracts[h] = nil - m.mtx.Unlock() + cache.contracts[h] = nil + cache.mtx.Unlock() } // Deploy creates contract's hash/ID and saves new contract into the given DAO. @@ -300,7 +301,7 @@ func (m *Management) Deploy(d *dao.Simple, sender util.Uint160, neff *nef.File, if err != nil { return nil, err } - m.markUpdated(newcontract.Hash) + m.markUpdated(d, newcontract.Hash) return newcontract, nil } @@ -340,7 +341,7 @@ 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) + m.markUpdated(d, hash) contract.NEF = *neff } // if manifest was provided, update the contract manifest @@ -352,7 +353,7 @@ 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) + m.markUpdated(d, hash) contract.Manifest = *manif } err = checkScriptAndMethods(contract.NEF.Script, contract.Manifest.ABI.Methods) @@ -393,7 +394,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) return nil } @@ -444,18 +445,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 +468,19 @@ func (m *Management) OnPersist(ic *interop.Context) error { cs := &state.Contract{ ContractBase: md.ContractBase, } + if err := native.Initialize(ic); err != nil { + return fmt.Errorf("initializing %s native contract: %w", md.Name, err) + } 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) + if cache == nil { + cache = ic.DAO.Store.GetCache(m.ID).(*ManagementCache) } - m.mtx.Lock() - m.updateContractCache(cs) - m.mtx.Unlock() + cache.mtx.Lock() + updateContractCache(cache, cs) + cache.mtx.Unlock() } return nil @@ -485,8 +490,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 +503,63 @@ 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.Store.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 { + cache := ic.DAO.Store.GetCache(m.ID).(*ManagementCache) + cache.mtx.Lock() + defer cache.mtx.Unlock() + for h, cs := range cache.contracts { if cs != nil { continue } - delete(m.nep11, h) - delete(m.nep17, h) + delete(cache.nep11, h) + delete(cache.nep17, h) newCs, err := m.getContractFromDAO(ic.DAO, h) if err != nil { // Contract was destroyed. - delete(m.contracts, h) + delete(cache.contracts, h) continue } - m.updateContractCache(newCs) + updateContractCache(cache, 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.Store.GetCache(m.ID).(*ManagementCache) + cache.mtx.RLock() + result := make([]util.Uint160, 0, len(cache.nep11)) + for h := range cache.nep11 { result = append(result, h) } - m.mtx.RUnlock() + cache.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.Store.GetCache(m.ID).(*ManagementCache) + cache.mtx.RLock() + result := make([]util.Uint160, 0, len(cache.nep17)) + for h := range cache.nep17 { result = append(result, h) } - m.mtx.RUnlock() + cache.mtx.RUnlock() return result } @@ -552,6 +567,13 @@ 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.Store.SetCache(m.ID, cache) return nil } @@ -561,7 +583,7 @@ func (m *Management) PutContractState(d *dao.Simple, cs *state.Contract) error { if err := putConvertibleToDAO(m.ID, d, key, cs); err != nil { return err } - m.markUpdated(cs.Hash) + m.markUpdated(d, cs.Hash) if cs.UpdateCounter != 0 { // Update. return nil } diff --git a/pkg/core/native/management_test.go b/pkg/core/native/management_test.go index 15b6cf0c1..696a97a81 100644 --- a/pkg/core/native/management_test.go +++ b/pkg/core/native/management_test.go @@ -89,8 +89,10 @@ 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)) // Deploy NEP-17 contract script := []byte{byte(opcode.RET)} @@ -108,11 +110,11 @@ func TestManagement_GetNEP17Contracts(t *testing.T) { require.NoError(t, err) // PostPersist is not yet called, thus no NEP-17 contracts are expected - require.Empty(t, mgmt.GetNEP17Contracts()) + require.Empty(t, mgmt.GetNEP17Contracts(d)) // 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()) + require.Equal(t, []util.Uint160{c1.Hash}, mgmt.GetNEP17Contracts(d)) // Update contract manif.ABI.Methods = append(manif.ABI.Methods, manifest.Method{ @@ -124,9 +126,9 @@ func TestManagement_GetNEP17Contracts(t *testing.T) { require.NoError(t, err) // No changes expected before PostPersist call. - require.Equal(t, []util.Uint160{c1.Hash}, mgmt.GetNEP17Contracts()) + require.Equal(t, []util.Uint160{c1.Hash}, mgmt.GetNEP17Contracts(d)) // 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()) + require.Equal(t, []util.Uint160{c2.Hash}, mgmt.GetNEP17Contracts(d)) } diff --git a/pkg/core/native/native_gas.go b/pkg/core/native/native_gas.go index d02b672e5..14e83694c 100644 --- a/pkg/core/native/native_gas.go +++ b/pkg/core/native/native_gas.go @@ -108,7 +108,7 @@ func (g *GAS) OnPersist(ic *interop.Context) error { absAmount := big.NewInt(tx.SystemFee + tx.NetworkFee) g.burn(ic, tx.Sender(), absAmount) } - validators := g.NEO.GetNextBlockValidatorsInternal() + validators := g.NEO.GetNextBlockValidatorsInternal(ic.DAO) primary := validators[ic.Block.PrimaryIndex].GetScriptHash() var netFee int64 for _, tx := range ic.Block.Transactions { diff --git a/pkg/core/native/native_neo.go b/pkg/core/native/native_neo.go index 830682790..681e77867 100644 --- a/pkg/core/native/native_neo.go +++ b/pkg/core/native/native_neo.go @@ -35,6 +35,13 @@ type NEO struct { GAS *GAS Policy *Policy + // Configuration and standby keys are set in constructor and then + // only read from. + cfg config.ProtocolConfiguration + standbyKeys keys.PublicKeys +} + +type NeoCache struct { // 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 @@ -58,11 +65,6 @@ type NEO struct { // 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 in constructor and then - // only read from. - cfg config.ProtocolConfiguration - standbyKeys keys.PublicKeys } const ( @@ -129,13 +131,6 @@ func newNEO(cfg config.ProtocolConfiguration) *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 { @@ -213,9 +208,22 @@ func (n *NEO) Initialize(ic *interop.Context) error { return errors.New("already initialized") } + cache := &NeoCache{ + gasPerVoteCache: make(map[string]big.Int), + } + cache.votesChanged.Store(true) + cache.nextValidators.Store(keys.PublicKeys(nil)) + cache.validators.Store(keys.PublicKeys(nil)) + cache.committee.Store(keysWithVotes(nil)) + cache.committeeHash.Store(util.Uint160{}) + cache.registerPriceChanged.Store(true) + + // We need cache to be present in DAO before the subsequent call to `mint`. + ic.DAO.Store.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.Chain) if err != nil { return err } @@ -233,13 +241,14 @@ 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.Store(*gr) + cache.gasPerBlockChanged.Store(false) 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.Store(int64(DefaultRegisterPrice)) + cache.registerPriceChanged.Store(false) + return nil } @@ -247,18 +256,30 @@ func (n *NEO) Initialize(ic *interop.Context) error { // 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 { + cache := &NeoCache{ + gasPerVoteCache: make(map[string]big.Int), + } + + cache.votesChanged.Store(true) + cache.nextValidators.Store(keys.PublicKeys(nil)) + cache.validators.Store(keys.PublicKeys(nil)) + cache.committee.Store(keysWithVotes(nil)) + cache.committeeHash.Store(util.Uint160{}) + cache.registerPriceChanged.Store(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, bc); err != nil { + return fmt.Errorf("failed to update cache: %w", err) } - n.gasPerBlock.Store(n.getSortedGASRecordFromDAO(d)) - n.gasPerBlockChanged.Store(false) + cache.gasPerBlock.Store(n.getSortedGASRecordFromDAO(d)) + cache.gasPerBlockChanged.Store(false) + d.Store.SetCache(n.ID, cache) return nil } @@ -270,27 +291,28 @@ func (n *NEO) initConfigCache(cfg config.ProtocolConfiguration) error { return err } -func (n *NEO) updateCache(cvs keysWithVotes, bc interop.Ledger) error { - n.committee.Store(cvs) +func (n *NEO) updateCache(cache *NeoCache, cvs keysWithVotes, bc interop.Ledger) error { + cache.committee.Store(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.Store(hash.Hash160(script)) + // TODO: use block height from interop context for proper historical calls handling. nextVals := committee[:n.cfg.GetNumOfCNs(bc.BlockHeight()+1)].Copy() sort.Sort(nextVals) - n.nextValidators.Store(nextVals) + cache.nextValidators.Store(nextVals) return nil } -func (n *NEO) updateCommittee(ic *interop.Context) error { - votesChanged := n.votesChanged.Load().(bool) +func (n *NEO) updateCommittee(cache *NeoCache, ic *interop.Context) error { + votesChanged := cache.votesChanged.Load().(bool) if !votesChanged { // We need to put in storage anyway, as it affects dumps - committee := n.committee.Load().(keysWithVotes) + committee := cache.committee.Load().(keysWithVotes) ic.DAO.PutStorageItem(n.ID, prefixCommittee, committee.Bytes()) return nil } @@ -299,10 +321,10 @@ func (n *NEO) updateCommittee(ic *interop.Context) error { if err != nil { return err } - if err := n.updateCache(cvs, ic.Chain); err != nil { + if err := n.updateCache(cache, cvs, ic.Chain); err != nil { return err } - n.votesChanged.Store(false) + cache.votesChanged.Store(false) ic.DAO.PutStorageItem(n.ID, prefixCommittee, cvs.Bytes()) return nil } @@ -310,13 +332,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.Store.GetCache(n.ID).(*NeoCache) + oldKeys := cache.nextValidators.Load().(keys.PublicKeys) + oldCom := cache.committee.Load().(keysWithVotes) if n.cfg.GetNumOfCNs(ic.Block.Index) != len(oldKeys) || n.cfg.GetCommitteeSize(ic.Block.Index) != len(oldCom) { - n.votesChanged.Store(true) + cache.votesChanged.Store(true) } - if err := n.updateCommittee(ic); err != nil { + if err := n.updateCommittee(cache, ic); err != nil { return err } } @@ -326,7 +349,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.Store.GetCache(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) @@ -340,7 +364,7 @@ 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 cs = cache.committee.Load().(keysWithVotes) var key = make([]byte, 38) for i := range cs { if cs[i].Votes.Sign() > 0 { @@ -356,7 +380,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}) @@ -365,21 +389,21 @@ 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 + 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 cache.gasPerBlockChanged.Load().(bool) { + cache.gasPerBlock.Store(n.getSortedGASRecordFromDAO(ic.DAO)) + cache.gasPerBlockChanged.Store(false) } - if n.registerPriceChanged.Load().(bool) { + if cache.registerPriceChanged.Load().(bool) { p := getIntWithKey(n.ID, ic.DAO, []byte{prefixRegisterPrice}) - n.registerPrice.Store(p) - n.registerPriceChanged.Store(false) + cache.registerPrice.Store(p) + cache.registerPriceChanged.Store(false) } return nil } @@ -502,11 +526,12 @@ 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 { + cache := d.Store.GetCache(n.ID).(*NeoCache) var gr gasRecord - if n.gasPerBlockChanged.Load().(bool) { + if cache.gasPerBlockChanged.Load().(bool) { gr = n.getSortedGASRecordFromDAO(d) } else { - gr = n.gasPerBlock.Load().(gasRecord) + gr = cache.gasPerBlock.Load().(gasRecord) } for i := len(gr) - 1; i >= 0; i-- { if gr[i].Index <= index { @@ -518,12 +543,13 @@ func (n *NEO) GetGASPerBlock(d *dao.Simple, index uint32) *big.Int { } // 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.Store.GetCache(n.ID).(*NeoCache) + return cache.committeeHash.Load().(util.Uint160) } 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) } @@ -547,7 +573,8 @@ 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) + cache := ic.DAO.Store.GetCache(n.ID).(*NeoCache) + cache.gasPerBlockChanged.Store(true) n.putGASRecord(ic.DAO, index, gas) return nil } @@ -557,8 +584,9 @@ 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) + cache := d.Store.GetCache(n.ID).(*NeoCache) + if !cache.registerPriceChanged.Load().(bool) { + return cache.registerPrice.Load().(int64) } return getIntWithKey(n.ID, d, []byte{prefixRegisterPrice}) } @@ -573,11 +601,12 @@ 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.Store.GetCache(n.ID).(*NeoCache) + cache.registerPriceChanged.Store(true) 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, error) { if c.Registered || c.Votes.Sign() != 0 { return false, nil } @@ -588,7 +617,7 @@ 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 } @@ -643,8 +672,9 @@ func (n *NEO) CalculateNEOHolderReward(d *dao.Simple, value *big.Int, start, end return nil, errors.New("negative value") } var gr gasRecord - if !n.gasPerBlockChanged.Load().(bool) { - gr = n.gasPerBlock.Load().(gasRecord) + cache := d.Store.GetCache(n.ID).(*NeoCache) + if !cache.gasPerBlockChanged.Load().(bool) { + gr = cache.gasPerBlock.Load().(gasRecord) } else { gr = n.getSortedGASRecordFromDAO(d) } @@ -717,10 +747,11 @@ func (n *NEO) UnregisterCandidateInternal(ic *interop.Context, pub *keys.PublicK if si == nil { return nil } - n.validators.Store(keys.PublicKeys(nil)) + cache := ic.DAO.Store.GetCache(n.ID).(*NeoCache) + cache.validators.Store(keys.PublicKeys(nil)) c := new(candidate).FromBytes(si) c.Registered = false - ok, err := n.dropCandidateIfZero(ic.DAO, pub, c) + ok, err := n.dropCandidateIfZero(ic.DAO, cache, pub, c) if ok { return err } @@ -796,7 +827,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.Store.GetCache(n.ID).(*NeoCache) + cache.votesChanged.Store(true) if acc.VoteTo != nil { key := makeValidatorKey(acc.VoteTo) si := d.GetStorageItem(n.ID, key) @@ -806,12 +838,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, err := n.dropCandidateIfZero(d, cache, acc.VoteTo, cd) if ok { return err } } - n.validators.Store(keys.PublicKeys(nil)) + cache.validators.Store(keys.PublicKeys(nil)) return putConvertibleToDAO(n.ID, d, key, cd) } return nil @@ -906,7 +938,8 @@ 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) { + cache := d.Store.GetCache(n.ID).(*NeoCache) + if vals := cache.validators.Load().(keys.PublicKeys); vals != nil && numOfCNs == len(vals) { return vals.Copy(), nil } result, _, err := n.computeCommitteeMembers(bc, d) @@ -915,12 +948,12 @@ func (n *NEO) ComputeNextBlockValidators(bc interop.Ledger, d *dao.Simple) (keys } result = result[:numOfCNs] sort.Sort(result) - n.validators.Store(result) + cache.validators.Store(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) } @@ -939,8 +972,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.Store.GetCache(n.ID).(*NeoCache) + return getCommitteeMembers(cache) +} + +func getCommitteeMembers(cache *NeoCache) keys.PublicKeys { + var cvs = cache.committee.Load().(keysWithVotes) var committee = make(keys.PublicKeys, len(cvs)) var err error for i := range committee { @@ -1009,13 +1047,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.Store.GetCache(n.ID).(*NeoCache) + return cache.nextValidators.Load().(keys.PublicKeys).Copy() } // BalanceOf returns native NEO token balance for the acc. diff --git a/pkg/core/native/notary.go b/pkg/core/native/notary.go index 086a11da1..18abcc31b 100644 --- a/pkg/core/native/notary.go +++ b/pkg/core/native/notary.go @@ -32,7 +32,9 @@ type Notary struct { GAS *GAS NEO *NEO Desig *Designate +} +type NotaryCache struct { lock sync.RWMutex // isValid defies whether cached values were changed during the current // consensus iteration. If false, these values will be updated after @@ -125,9 +127,23 @@ 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{ + isValid: true, + maxNotValidBeforeDelta: defaultMaxNotValidBeforeDelta, + notaryServiceFeePerKey: defaultNotaryServiceFeePerKey, + } + ic.DAO.Store.SetCache(n.ID, cache) + return nil +} + +func (n *Notary) InitializeCache(d *dao.Simple) error { + cache := &NotaryCache{isValid: true} + + cache.maxNotValidBeforeDelta = uint32(getIntWithKey(n.ID, d, maxNotValidBeforeDeltaKey)) + cache.notaryServiceFeePerKey = getIntWithKey(n.ID, d, notaryServiceFeeKey) + + d.Store.SetCache(n.ID, cache) return nil } @@ -176,15 +192,16 @@ 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 { + cache := ic.DAO.Store.GetCache(n.ID).(*NotaryCache) + cache.lock.Lock() + defer cache.lock.Unlock() + if cache.isValid { return nil } - n.maxNotValidBeforeDelta = uint32(getIntWithKey(n.ID, ic.DAO, maxNotValidBeforeDeltaKey)) - n.notaryServiceFeePerKey = getIntWithKey(n.ID, ic.DAO, notaryServiceFeeKey) - n.isValid = true + cache.maxNotValidBeforeDelta = uint32(getIntWithKey(n.ID, ic.DAO, maxNotValidBeforeDeltaKey)) + cache.notaryServiceFeePerKey = getIntWithKey(n.ID, ic.DAO, notaryServiceFeeKey) + cache.isValid = true return nil } @@ -391,10 +408,11 @@ 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 + cache := dao.Store.GetCache(n.ID).(*NotaryCache) + cache.lock.RLock() + defer cache.lock.RUnlock() + if cache.isValid { + return cache.maxNotValidBeforeDelta } return uint32(getIntWithKey(n.ID, dao, maxNotValidBeforeDeltaKey)) } @@ -410,10 +428,11 @@ func (n *Notary) setMaxNotValidBeforeDelta(ic *interop.Context, args []stackitem if !n.NEO.checkCommittee(ic) { panic("invalid committee signature") } - n.lock.Lock() - defer n.lock.Unlock() + cache := ic.DAO.Store.GetCache(n.ID).(*NotaryCache) + cache.lock.Lock() + defer cache.lock.Unlock() setIntWithKey(n.ID, ic.DAO, maxNotValidBeforeDeltaKey, int64(value)) - n.isValid = false + cache.isValid = false return stackitem.Null{} } @@ -424,10 +443,11 @@ 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 + cache := dao.Store.GetCache(n.ID).(*NotaryCache) + cache.lock.RLock() + defer cache.lock.RUnlock() + if cache.isValid { + return cache.notaryServiceFeePerKey } return getIntWithKey(n.ID, dao, notaryServiceFeeKey) } @@ -441,10 +461,11 @@ 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() + cache := ic.DAO.Store.GetCache(n.ID).(*NotaryCache) + cache.lock.Lock() + defer cache.lock.Unlock() setIntWithKey(n.ID, ic.DAO, notaryServiceFeeKey, int64(value)) - n.isValid = false + cache.isValid = false return stackitem.Null{} } diff --git a/pkg/core/native/oracle.go b/pkg/core/native/oracle.go index 63b813c5b..664728ca9 100644 --- a/pkg/core/native/oracle.go +++ b/pkg/core/native/oracle.go @@ -40,15 +40,17 @@ 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 atomic.Value + requestPriceChanged atomic.Value +} + const ( oracleContractID = -9 maxURLLength = 256 @@ -121,8 +123,6 @@ func newOracle() *Oracle { md = newMethodAndPrice(o.setPrice, 1<<15, callflag.States) o.AddMethod(md, desc) - o.requestPriceChanged.Store(true) - return o } @@ -143,9 +143,10 @@ 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) + cache := ic.DAO.Store.GetCache(o.ID).(*OracleCache) + if cache.requestPriceChanged.Load().(bool) { + cache.requestPrice.Store(p) + cache.requestPriceChanged.Store(false) } var nodes keys.PublicKeys @@ -220,11 +221,21 @@ 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{} + cache.requestPrice.Store(int64(DefaultOracleRequestPrice)) + cache.requestPriceChanged.Store(false) + ic.DAO.Store.SetCache(o.ID, cache) return nil } +func (o *Oracle) InitializeCache(d *dao.Simple) { + cache := &OracleCache{} + cache.requestPrice.Store(getIntWithKey(o.ID, d, prefixRequestPrice)) + cache.requestPriceChanged.Store(false) + d.Store.SetCache(o.ID, cache) +} + func getResponse(tx *transaction.Transaction) *transaction.OracleResponse { for i := range tx.Attributes { if tx.Attributes[i].Type == transaction.OracleResponseT { @@ -439,8 +450,9 @@ 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) + cache := d.Store.GetCache(o.ID).(*OracleCache) + if !cache.requestPriceChanged.Load().(bool) { + return cache.requestPrice.Load().(int64) } return getIntWithKey(o.ID, d, prefixRequestPrice) } @@ -454,7 +466,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.Store.GetCache(o.ID).(*OracleCache) + cache.requestPriceChanged.Store(true) return stackitem.Null{} } diff --git a/pkg/core/native/policy.go b/pkg/core/native/policy.go index c097b8e30..9623ead77 100644 --- a/pkg/core/native/policy.go +++ b/pkg/core/native/policy.go @@ -52,7 +52,10 @@ var ( // Policy represents Policy native contract. type Policy struct { interop.ContractMD - NEO *NEO + NEO *NEO +} + +type PolicyCache struct { lock sync.RWMutex // isValid defies whether cached values were changed during the current // consensus iteration. If false, these values will be updated after @@ -128,16 +131,52 @@ 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{} + cache.isValid = true + cache.execFeeFactor = defaultExecFeeFactor + cache.feePerByte = defaultFeePerByte + cache.maxVerificationGas = defaultMaxVerificationGas + cache.storagePrice = DefaultStoragePrice + cache.blockedAccounts = make([]util.Uint160, 0) + ic.DAO.Store.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.Store.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) + } + cache.isValid = true + return nil +} + // OnPersist implements Contract interface. func (p *Policy) OnPersist(ic *interop.Context) error { return nil @@ -145,32 +184,14 @@ 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 { + cache := ic.DAO.Store.GetCache(p.ID).(*PolicyCache) + cache.lock.Lock() + defer cache.lock.Unlock() + if cache.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 p.fillCacheFromDAO(cache, ic.DAO) } // getFeePerByte is Policy contract method and returns required transaction's fee @@ -181,18 +202,22 @@ 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 + cache := dao.Store.GetCache(p.ID).(*PolicyCache) + cache.lock.RLock() + defer cache.lock.RUnlock() + if cache.isValid { + return cache.feePerByte } return getIntWithKey(p.ID, dao, feePerByteKey) } // GetMaxVerificationGas returns maximum gas allowed to be burned during verificaion. -func (p *Policy) GetMaxVerificationGas(_ *dao.Simple) int64 { - if p.isValid { - return p.maxVerificationGas +func (p *Policy) GetMaxVerificationGas(dao *dao.Simple) int64 { + cache := dao.Store.GetCache(p.ID).(*PolicyCache) + cache.lock.RLock() + defer cache.lock.RUnlock() + if cache.isValid { + return cache.maxVerificationGas } return defaultMaxVerificationGas } @@ -203,10 +228,11 @@ 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) + cache := d.Store.GetCache(p.ID).(*PolicyCache) + cache.lock.RLock() + defer cache.lock.RUnlock() + if cache.isValid { + return int64(cache.execFeeFactor) } return getIntWithKey(p.ID, d, execFeeFactorKey) } @@ -219,10 +245,11 @@ 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() + cache := ic.DAO.Store.GetCache(p.ID).(*PolicyCache) + cache.lock.Lock() + defer cache.lock.Unlock() setIntWithKey(p.ID, ic.DAO, execFeeFactorKey, int64(value)) - p.isValid = false + cache.isValid = false return stackitem.Null{} } @@ -234,14 +261,15 @@ func (p *Policy) isBlocked(ic *interop.Context, args []stackitem.Item) stackitem // 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) + cache := dao.Store.GetCache(p.ID).(*PolicyCache) + cache.lock.RLock() + defer cache.lock.RUnlock() + if cache.isValid { + length := len(cache.blockedAccounts) i := sort.Search(length, func(i int) bool { - return !p.blockedAccounts[i].Less(hash) + return !cache.blockedAccounts[i].Less(hash) }) - if length != 0 && i != length && p.blockedAccounts[i].Equals(hash) { + if length != 0 && i != length && cache.blockedAccounts[i].Equals(hash) { return true } return false @@ -256,10 +284,11 @@ 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) + cache := d.Store.GetCache(p.ID).(*PolicyCache) + cache.lock.RLock() + defer cache.lock.RUnlock() + if cache.isValid { + return int64(cache.storagePrice) } return getIntWithKey(p.ID, d, storagePriceKey) } @@ -272,10 +301,11 @@ 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() + cache := ic.DAO.Store.GetCache(p.ID).(*PolicyCache) + cache.lock.Lock() + defer cache.lock.Unlock() setIntWithKey(p.ID, ic.DAO, storagePriceKey, int64(value)) - p.isValid = false + cache.isValid = false return stackitem.Null{} } @@ -288,10 +318,11 @@ 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() + cache := ic.DAO.Store.GetCache(p.ID).(*PolicyCache) + cache.lock.Lock() + defer cache.lock.Unlock() setIntWithKey(p.ID, ic.DAO, feePerByteKey, value) - p.isValid = false + cache.isValid = false return stackitem.Null{} } @@ -311,10 +342,11 @@ func (p *Policy) blockAccount(ic *interop.Context, args []stackitem.Item) stacki return stackitem.NewBool(false) } key := append([]byte{blockedAccountPrefix}, hash.BytesBE()...) - p.lock.Lock() - defer p.lock.Unlock() + cache := ic.DAO.Store.GetCache(p.ID).(*PolicyCache) + cache.lock.Lock() + defer cache.lock.Unlock() ic.DAO.PutStorageItem(p.ID, key, state.StorageItem{}) - p.isValid = false + cache.isValid = false return stackitem.NewBool(true) } @@ -329,10 +361,11 @@ func (p *Policy) unblockAccount(ic *interop.Context, args []stackitem.Item) stac return stackitem.NewBool(false) } key := append([]byte{blockedAccountPrefix}, hash.BytesBE()...) - p.lock.Lock() - defer p.lock.Unlock() + cache := ic.DAO.Store.GetCache(p.ID).(*PolicyCache) + cache.lock.Lock() + defer cache.lock.Unlock() ic.DAO.DeleteStorageItem(p.ID, key) - p.isValid = false + cache.isValid = false return stackitem.NewBool(true) } diff --git a/pkg/core/storage/memcached_store.go b/pkg/core/storage/memcached_store.go index 38623d9e4..e241a3b39 100644 --- a/pkg/core/storage/memcached_store.go +++ b/pkg/core/storage/memcached_store.go @@ -15,6 +15,9 @@ import ( type MemCachedStore struct { MemoryStore + nativeCacheLock sync.RWMutex + nativeCache map[int32]*NativeCacheItem + private bool // plock protects Persist from double entrance. plock sync.Mutex @@ -22,6 +25,10 @@ type MemCachedStore struct { ps Store } +type NativeCacheItem struct { + Value interface{} +} + type ( // KeyValue represents key-value pair. KeyValue struct { @@ -46,8 +53,15 @@ type ( // NewMemCachedStore creates a new MemCachedStore object. func NewMemCachedStore(lower Store) *MemCachedStore { + var cache map[int32]*NativeCacheItem + if cached, ok := lower.(*MemCachedStore); ok { + cache = cached.nativeCache + } else { + cache = make(map[int32]*NativeCacheItem) + } return &MemCachedStore{ MemoryStore: *NewMemoryStore(), + nativeCache: cache, ps: lower, } } @@ -55,8 +69,15 @@ func NewMemCachedStore(lower Store) *MemCachedStore { // NewPrivateMemCachedStore creates a new private (unlocked) MemCachedStore object. // Private cached stores are closed after Persist. func NewPrivateMemCachedStore(lower Store) *MemCachedStore { + var cache map[int32]*NativeCacheItem + if cached, ok := lower.(*MemCachedStore); ok { + cache = cached.nativeCache + } else { + cache = make(map[int32]*NativeCacheItem) + } return &MemCachedStore{ MemoryStore: *NewMemoryStore(), + nativeCache: cache, private: true, ps: lower, } @@ -393,6 +414,25 @@ func (s *MemCachedStore) persist(isSync bool) (int, error) { return keys, err } +func (s *MemCachedStore) GetCache(k int32) interface{} { + s.nativeCacheLock.RLock() + defer s.nativeCacheLock.RUnlock() + + if itm, ok := s.nativeCache[k]; ok { + return itm.Value + } + return nil +} + +func (s *MemCachedStore) SetCache(k int32, v interface{}) { + s.nativeCacheLock.Lock() + defer s.nativeCacheLock.Unlock() + + s.nativeCache[k] = &NativeCacheItem{ + Value: v, + } +} + // Close implements Store interface, clears up memory and closes the lower layer // Store. func (s *MemCachedStore) Close() error { diff --git a/pkg/rpc/server/client_test.go b/pkg/rpc/server/client_test.go index ca2ac2b63..14826ea13 100644 --- a/pkg/rpc/server/client_test.go +++ b/pkg/rpc/server/client_test.go @@ -4,9 +4,11 @@ import ( "context" "encoding/base64" "encoding/hex" + "strings" "testing" "github.com/nspcc-dev/neo-go/internal/testchain" + "github.com/nspcc-dev/neo-go/pkg/core" "github.com/nspcc-dev/neo-go/pkg/core/fee" "github.com/nspcc-dev/neo-go/pkg/core/native/nativenames" "github.com/nspcc-dev/neo-go/pkg/core/transaction" @@ -798,13 +800,13 @@ func TestInvokeVerify(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) - // TODO: check that error is `ErrUnknownVerificationContract` + 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) - // TODO: check that error is `ErrUnknownVerificationContract` + 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) { @@ -813,7 +815,7 @@ func TestInvokeVerify(t *testing.T) { require.NoError(t, err) _, err = c.InvokeContractVerifyWithState(sr.Root, contract, smartcontract.Params{}, []transaction.Signer{{Account: testchain.PrivateKeyByID(0).PublicKey().GetScriptHash()}}) require.Error(t, err) - // TODO: check that error is `ErrUnknownVerificationContract` + 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) { diff --git a/pkg/rpc/server/server.go b/pkg/rpc/server/server.go index 590a44662..e370fb8f4 100644 --- a/pkg/rpc/server/server.go +++ b/pkg/rpc/server/server.go @@ -1840,7 +1840,7 @@ func (s *Server) runScriptInVM(t trigger.Type, script []byte, contractScriptHash 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) From 7b632c8ee8b4cb297635faebb5c36b64584dddeb Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Fri, 15 Apr 2022 17:48:58 +0300 Subject: [PATCH 08/24] core: refactor natives cache 1. Use layered natives cache. With layered cache the storeblock process includes the following steps: create a wrapper over current nativeCache, put changes into upper nativeCache layer, persist (or discard) changes. 2. Split contract getters to read-only and read-and-change. Read-only ones doesn't require the copy of an existing nativeCache item. Read-and-change ones create a copy and after that change the copy. --- pkg/core/native/designate.go | 54 +++++++++++- pkg/core/native/management.go | 54 ++++++++++-- pkg/core/native/native_neo.go | 79 +++++++++++++++--- pkg/core/native/notary.go | 41 +++++++-- pkg/core/native/oracle.go | 36 +++++++- pkg/core/native/policy.go | 54 +++++++++--- pkg/core/storage/memcached_store.go | 124 ++++++++++++++++++++++------ 7 files changed, 374 insertions(+), 68 deletions(-) diff --git a/pkg/core/native/designate.go b/pkg/core/native/designate.go index 785bd2f9f..93411c46f 100644 --- a/pkg/core/native/designate.go +++ b/pkg/core/native/designate.go @@ -75,6 +75,52 @@ var ( ErrNoBlock = errors.New("no persisting block in the context") ) +var ( + _ interop.Contract = (*Designate)(nil) + _ storage.NativeContractCache = (*DesignationCache)(nil) +) + +// Copy implements NativeContractCache interface. +func (c *DesignationCache) Copy() storage.NativeContractCache { + cp := &DesignationCache{} + copyDesignationCache(c, cp) + return cp +} + +// Persist implements NativeContractCache interface. +func (c *DesignationCache) Persist(ps storage.NativeContractCache) (storage.NativeContractCache, error) { + if ps == nil { + ps = &DesignationCache{} + } + psCache, ok := ps.(*DesignationCache) + if !ok { + return nil, errors.New("not a Designation native cache") + } + copyDesignationCache(c, psCache) + return psCache, nil +} + +func copyDesignationCache(src, dst *DesignationCache) { + dst.rolesChangedFlag.Store(src.rolesChangedFlag.Load()) + for _, r := range []noderoles.Role{noderoles.StateValidator, noderoles.Oracle, noderoles.NeoFSAlphabet, noderoles.P2PNotary} { + data := getCachedRoleData(src, r) + if data != nil { + var v = &roleData{} + *v = *data + switch r { + case noderoles.StateValidator: + dst.stateVals.Store(v) + case noderoles.Oracle: + dst.oracles.Store(v) + case noderoles.NeoFSAlphabet: + dst.neofsAlphabet.Store(v) + case noderoles.P2PNotary: + dst.notaries.Store(v) + } + } + } +} + func (s *Designate) isValidRole(r noderoles.Role) bool { return r == noderoles.Oracle || r == noderoles.StateValidator || r == noderoles.NeoFSAlphabet || (s.p2pSigExtensionsEnabled && r == noderoles.P2PNotary) @@ -119,7 +165,7 @@ func (s *Designate) OnPersist(ic *interop.Context) error { // PostPersist implements Contract interface. func (s *Designate) PostPersist(ic *interop.Context) error { - cache := ic.DAO.Store.GetCache(s.ID).(*DesignationCache) + cache := ic.DAO.Store.GetRWCache(s.ID).(*DesignationCache) if !rolesChanged(cache) { return nil } @@ -237,7 +283,7 @@ func (s *Designate) GetLastDesignatedHash(d *dao.Simple, r noderoles.Role) (util if !s.isValidRole(r) { return util.Uint160{}, ErrInvalidRole } - cache := d.Store.GetCache(s.ID).(*DesignationCache) + cache := d.Store.GetROCache(s.ID).(*DesignationCache) if !rolesChanged(cache) { if val := getCachedRoleData(cache, r); val != nil { return val.addr, nil @@ -256,7 +302,7 @@ func (s *Designate) GetDesignatedByRole(d *dao.Simple, r noderoles.Role, index u if !s.isValidRole(r) { return nil, 0, ErrInvalidRole } - cache := d.Store.GetCache(s.ID).(*DesignationCache) + cache := d.Store.GetROCache(s.ID).(*DesignationCache) if !rolesChanged(cache) { if val := getCachedRoleData(cache, r); val != nil && val.height <= index { return val.nodes.Copy(), val.height, nil @@ -335,7 +381,7 @@ func (s *Designate) DesignateAsRole(ic *interop.Context, r noderoles.Role, pubs } sort.Sort(pubs) nl := NodeList(pubs) - ic.DAO.Store.GetCache(s.ID).(*DesignationCache).rolesChangedFlag.Store(true) + ic.DAO.Store.GetRWCache(s.ID).(*DesignationCache).rolesChangedFlag.Store(true) err := putConvertibleToDAO(s.ID, ic.DAO, key, &nl) if err != nil { return err diff --git a/pkg/core/native/management.go b/pkg/core/native/management.go index 9fcd8a6f1..765bb7ed4 100644 --- a/pkg/core/native/management.go +++ b/pkg/core/native/management.go @@ -59,6 +59,48 @@ var ( keyMinimumDeploymentFee = []byte{20} ) +var ( + _ interop.Contract = (*Management)(nil) + _ storage.NativeContractCache = (*ManagementCache)(nil) +) + +// Copy implements NativeContractCache interface. +func (c *ManagementCache) Copy() storage.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 +} + +// Persist implements NativeContractCache interface. +func (c *ManagementCache) Persist(ps storage.NativeContractCache) (storage.NativeContractCache, error) { + if ps == nil { + ps = &ManagementCache{} + } + psCache, ok := ps.(*ManagementCache) + if !ok { + return nil, errors.New("not a Management native cache") + } + psCache.contracts = c.contracts + psCache.nep17 = c.nep17 + psCache.nep11 = c.nep11 + return psCache, nil +} + // MakeContractKey creates a key from account script hash. func MakeContractKey(h util.Uint160) []byte { return makeUint160Key(prefixContract, h) @@ -145,7 +187,7 @@ 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) { - cache := d.Store.GetCache(m.ID).(*ManagementCache) + cache := d.Store.GetROCache(m.ID).(*ManagementCache) cache.mtx.RLock() cs, ok := cache.contracts[hash] cache.mtx.RUnlock() @@ -261,7 +303,7 @@ func (m *Management) deployWithData(ic *interop.Context, args []stackitem.Item) } func (m *Management) markUpdated(d *dao.Simple, h util.Uint160) { - cache := d.Store.GetCache(m.ID).(*ManagementCache) + cache := d.Store.GetRWCache(m.ID).(*ManagementCache) cache.mtx.Lock() // Just set it to nil, to refresh cache in `PostPersist`. cache.contracts[h] = nil @@ -476,7 +518,7 @@ func (m *Management) OnPersist(ic *interop.Context) error { return err } if cache == nil { - cache = ic.DAO.Store.GetCache(m.ID).(*ManagementCache) + cache = ic.DAO.Store.GetRWCache(m.ID).(*ManagementCache) } cache.mtx.Lock() updateContractCache(cache, cs) @@ -515,7 +557,7 @@ func (m *Management) InitializeCache(d *dao.Simple) error { // PostPersist implements Contract interface. func (m *Management) PostPersist(ic *interop.Context) error { - cache := ic.DAO.Store.GetCache(m.ID).(*ManagementCache) + cache := ic.DAO.Store.GetRWCache(m.ID).(*ManagementCache) cache.mtx.Lock() defer cache.mtx.Unlock() for h, cs := range cache.contracts { @@ -539,7 +581,7 @@ func (m *Management) PostPersist(ic *interop.Context) error { // is updated every PostPersist, so until PostPersist is called, the result for the previous block // is returned. func (m *Management) GetNEP11Contracts(d *dao.Simple) []util.Uint160 { - cache := d.Store.GetCache(m.ID).(*ManagementCache) + cache := d.Store.GetROCache(m.ID).(*ManagementCache) cache.mtx.RLock() result := make([]util.Uint160, 0, len(cache.nep11)) for h := range cache.nep11 { @@ -553,7 +595,7 @@ func (m *Management) GetNEP11Contracts(d *dao.Simple) []util.Uint160 { // is updated every PostPersist, so until PostPersist is called, the result for the previous block // is returned. func (m *Management) GetNEP17Contracts(d *dao.Simple) []util.Uint160 { - cache := d.Store.GetCache(m.ID).(*ManagementCache) + cache := d.Store.GetROCache(m.ID).(*ManagementCache) cache.mtx.RLock() result := make([]util.Uint160, 0, len(cache.nep17)) for h := range cache.nep17 { diff --git a/pkg/core/native/native_neo.go b/pkg/core/native/native_neo.go index 681e77867..bf2574121 100644 --- a/pkg/core/native/native_neo.go +++ b/pkg/core/native/native_neo.go @@ -108,6 +108,59 @@ var ( big100 = big.NewInt(100) ) +var ( + _ interop.Contract = (*NEO)(nil) + _ storage.NativeContractCache = (*NeoCache)(nil) +) + +// Copy implements NativeContractCache interface. +func (c *NeoCache) Copy() storage.NativeContractCache { + cp := &NeoCache{} + copyNeoCache(c, cp) + return cp +} + +// Persist implements NativeContractCache interface. +func (c *NeoCache) Persist(ps storage.NativeContractCache) (storage.NativeContractCache, error) { + if ps == nil { + ps = &NeoCache{} + } + psCache, ok := ps.(*NeoCache) + if !ok { + return nil, errors.New("not a NEO native cache") + } + copyNeoCache(c, psCache) + return psCache, nil +} + +func copyNeoCache(src, dst *NeoCache) { + dst.votesChanged.Store(src.votesChanged.Load()) + dst.nextValidators.Store(src.nextValidators.Load().(keys.PublicKeys).Copy()) + dst.validators.Store(src.validators.Load().(keys.PublicKeys).Copy()) + committeeSrc := src.committee.Load().(keysWithVotes) + committeeDst := make(keysWithVotes, len(committeeSrc)) + copy(committeeDst, committeeSrc) + dst.committee.Store(committeeDst) + dst.committeeHash.Store(src.committeeHash.Load()) + regPriceChanged := src.registerPriceChanged.Load().(bool) + dst.registerPriceChanged.Store(regPriceChanged) + if !regPriceChanged { + dst.registerPrice.Store(src.registerPrice.Load()) + } + gasPerBlockChanged := src.gasPerBlockChanged.Load().(bool) + dst.gasPerBlockChanged.Store(gasPerBlockChanged) + if !gasPerBlockChanged { + recordsSrc := src.gasPerBlock.Load().(gasRecord) + recordsDst := make(gasRecord, len(recordsSrc)) + copy(recordsDst, recordsSrc) + dst.gasPerBlock.Store(recordsDst) + } + 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() @@ -332,7 +385,7 @@ func (n *NEO) updateCommittee(cache *NeoCache, ic *interop.Context) error { // OnPersist implements Contract interface. func (n *NEO) OnPersist(ic *interop.Context) error { if n.cfg.ShouldUpdateCommitteeAt(ic.Block.Index) { - cache := ic.DAO.Store.GetCache(n.ID).(*NeoCache) + cache := ic.DAO.Store.GetRWCache(n.ID).(*NeoCache) oldKeys := cache.nextValidators.Load().(keys.PublicKeys) oldCom := cache.committee.Load().(keysWithVotes) if n.cfg.GetNumOfCNs(ic.Block.Index) != len(oldKeys) || @@ -349,7 +402,7 @@ 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) - cache := ic.DAO.Store.GetCache(n.ID).(*NeoCache) + cache := ic.DAO.Store.GetRWCache(n.ID).(*NeoCache) pubs := getCommitteeMembers(cache) committeeSize := n.cfg.GetCommitteeSize(ic.Block.Index) index := int(ic.Block.Index) % committeeSize @@ -526,7 +579,7 @@ 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 { - cache := d.Store.GetCache(n.ID).(*NeoCache) + cache := d.Store.GetROCache(n.ID).(*NeoCache) var gr gasRecord if cache.gasPerBlockChanged.Load().(bool) { gr = n.getSortedGASRecordFromDAO(d) @@ -544,7 +597,7 @@ func (n *NEO) GetGASPerBlock(d *dao.Simple, index uint32) *big.Int { // GetCommitteeAddress returns address of the committee. func (n *NEO) GetCommitteeAddress(d *dao.Simple) util.Uint160 { - cache := d.Store.GetCache(n.ID).(*NeoCache) + cache := d.Store.GetROCache(n.ID).(*NeoCache) return cache.committeeHash.Load().(util.Uint160) } @@ -573,7 +626,7 @@ func (n *NEO) SetGASPerBlock(ic *interop.Context, index uint32, gas *big.Int) er if !n.checkCommittee(ic) { return errors.New("invalid committee signature") } - cache := ic.DAO.Store.GetCache(n.ID).(*NeoCache) + cache := ic.DAO.Store.GetRWCache(n.ID).(*NeoCache) cache.gasPerBlockChanged.Store(true) n.putGASRecord(ic.DAO, index, gas) return nil @@ -584,7 +637,7 @@ func (n *NEO) getRegisterPrice(ic *interop.Context, _ []stackitem.Item) stackite } func (n *NEO) getRegisterPriceInternal(d *dao.Simple) int64 { - cache := d.Store.GetCache(n.ID).(*NeoCache) + cache := d.Store.GetROCache(n.ID).(*NeoCache) if !cache.registerPriceChanged.Load().(bool) { return cache.registerPrice.Load().(int64) } @@ -601,7 +654,7 @@ func (n *NEO) setRegisterPrice(ic *interop.Context, args []stackitem.Item) stack } setIntWithKey(n.ID, ic.DAO, []byte{prefixRegisterPrice}, price.Int64()) - cache := ic.DAO.Store.GetCache(n.ID).(*NeoCache) + cache := ic.DAO.Store.GetRWCache(n.ID).(*NeoCache) cache.registerPriceChanged.Store(true) return stackitem.Null{} } @@ -672,7 +725,7 @@ func (n *NEO) CalculateNEOHolderReward(d *dao.Simple, value *big.Int, start, end return nil, errors.New("negative value") } var gr gasRecord - cache := d.Store.GetCache(n.ID).(*NeoCache) + cache := d.Store.GetROCache(n.ID).(*NeoCache) if !cache.gasPerBlockChanged.Load().(bool) { gr = cache.gasPerBlock.Load().(gasRecord) } else { @@ -747,7 +800,7 @@ func (n *NEO) UnregisterCandidateInternal(ic *interop.Context, pub *keys.PublicK if si == nil { return nil } - cache := ic.DAO.Store.GetCache(n.ID).(*NeoCache) + cache := ic.DAO.Store.GetRWCache(n.ID).(*NeoCache) cache.validators.Store(keys.PublicKeys(nil)) c := new(candidate).FromBytes(si) c.Registered = false @@ -827,7 +880,7 @@ 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 { - cache := d.Store.GetCache(n.ID).(*NeoCache) + cache := d.Store.GetRWCache(n.ID).(*NeoCache) cache.votesChanged.Store(true) if acc.VoteTo != nil { key := makeValidatorKey(acc.VoteTo) @@ -938,7 +991,7 @@ 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) - cache := d.Store.GetCache(n.ID).(*NeoCache) + cache := d.Store.GetRWCache(n.ID).(*NeoCache) if vals := cache.validators.Load().(keys.PublicKeys); vals != nil && numOfCNs == len(vals) { return vals.Copy(), nil } @@ -973,7 +1026,7 @@ 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(d *dao.Simple) keys.PublicKeys { - cache := d.Store.GetCache(n.ID).(*NeoCache) + cache := d.Store.GetROCache(n.ID).(*NeoCache) return getCommitteeMembers(cache) } @@ -1053,7 +1106,7 @@ func (n *NEO) getNextBlockValidators(ic *interop.Context, _ []stackitem.Item) st // GetNextBlockValidatorsInternal returns next block validators. func (n *NEO) GetNextBlockValidatorsInternal(d *dao.Simple) keys.PublicKeys { - cache := d.Store.GetCache(n.ID).(*NeoCache) + cache := d.Store.GetROCache(n.ID).(*NeoCache) return cache.nextValidators.Load().(keys.PublicKeys).Copy() } diff --git a/pkg/core/native/notary.go b/pkg/core/native/notary.go index 18abcc31b..817a5e73a 100644 --- a/pkg/core/native/notary.go +++ b/pkg/core/native/notary.go @@ -58,6 +58,37 @@ var ( notaryServiceFeeKey = []byte{5} ) +var ( + _ interop.Contract = (*Notary)(nil) + _ storage.NativeContractCache = (*NotaryCache)(nil) +) + +// Copy implements NativeContractCache interface. +func (c *NotaryCache) Copy() storage.NativeContractCache { + cp := &NotaryCache{} + copyNotaryCache(c, cp) + return cp +} + +// Persist implements NativeContractCache interface. +func (c *NotaryCache) Persist(ps storage.NativeContractCache) (storage.NativeContractCache, error) { + if ps == nil { + ps = &NotaryCache{} + } + psCache, ok := ps.(*NotaryCache) + if !ok { + return nil, errors.New("not a Notary native cache") + } + copyNotaryCache(c, psCache) + return psCache, nil +} + +func copyNotaryCache(src, dst *NotaryCache) { + dst.isValid = src.isValid + dst.maxNotValidBeforeDelta = src.maxNotValidBeforeDelta + dst.notaryServiceFeePerKey = src.notaryServiceFeePerKey +} + // newNotary returns Notary native contract. func newNotary() *Notary { n := &Notary{ContractMD: *interop.NewContractMD(nativenames.Notary, notaryContractID)} @@ -192,7 +223,7 @@ func (n *Notary) OnPersist(ic *interop.Context) error { // PostPersist implements Contract interface. func (n *Notary) PostPersist(ic *interop.Context) error { - cache := ic.DAO.Store.GetCache(n.ID).(*NotaryCache) + cache := ic.DAO.Store.GetRWCache(n.ID).(*NotaryCache) cache.lock.Lock() defer cache.lock.Unlock() if cache.isValid { @@ -408,7 +439,7 @@ 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 { - cache := dao.Store.GetCache(n.ID).(*NotaryCache) + cache := dao.Store.GetROCache(n.ID).(*NotaryCache) cache.lock.RLock() defer cache.lock.RUnlock() if cache.isValid { @@ -428,7 +459,7 @@ func (n *Notary) setMaxNotValidBeforeDelta(ic *interop.Context, args []stackitem if !n.NEO.checkCommittee(ic) { panic("invalid committee signature") } - cache := ic.DAO.Store.GetCache(n.ID).(*NotaryCache) + cache := ic.DAO.Store.GetRWCache(n.ID).(*NotaryCache) cache.lock.Lock() defer cache.lock.Unlock() setIntWithKey(n.ID, ic.DAO, maxNotValidBeforeDeltaKey, int64(value)) @@ -443,7 +474,7 @@ 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 { - cache := dao.Store.GetCache(n.ID).(*NotaryCache) + cache := dao.Store.GetROCache(n.ID).(*NotaryCache) cache.lock.RLock() defer cache.lock.RUnlock() if cache.isValid { @@ -461,7 +492,7 @@ func (n *Notary) setNotaryServiceFeePerKey(ic *interop.Context, args []stackitem if !n.NEO.checkCommittee(ic) { panic("invalid committee signature") } - cache := ic.DAO.Store.GetCache(n.ID).(*NotaryCache) + cache := ic.DAO.Store.GetRWCache(n.ID).(*NotaryCache) cache.lock.Lock() defer cache.lock.Unlock() setIntWithKey(n.ID, ic.DAO, notaryServiceFeeKey, int64(value)) diff --git a/pkg/core/native/oracle.go b/pkg/core/native/oracle.go index 664728ca9..893b5cebf 100644 --- a/pkg/core/native/oracle.go +++ b/pkg/core/native/oracle.go @@ -84,6 +84,36 @@ var ( ErrResponseNotFound = errors.New("oracle response not found") ) +var ( + _ interop.Contract = (*Oracle)(nil) + _ storage.NativeContractCache = (*OracleCache)(nil) +) + +// Copy implements NativeContractCache interface. +func (c *OracleCache) Copy() storage.NativeContractCache { + cp := &OracleCache{} + copyOracleCache(c, cp) + return cp +} + +// Persist implements NativeContractCache interface. +func (c *OracleCache) Persist(ps storage.NativeContractCache) (storage.NativeContractCache, error) { + if ps == nil { + ps = &OracleCache{} + } + psCache, ok := ps.(*OracleCache) + if !ok { + return nil, errors.New("not an Oracle native cache") + } + copyOracleCache(c, psCache) + return psCache, nil +} + +func copyOracleCache(src, dst *OracleCache) { + dst.requestPrice.Store(src.requestPrice.Load()) + dst.requestPriceChanged.Store(src.requestPriceChanged.Load()) +} + func newOracle() *Oracle { o := &Oracle{ContractMD: *interop.NewContractMD(nativenames.Oracle, oracleContractID)} defer o.UpdateHash() @@ -143,7 +173,7 @@ 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) - cache := ic.DAO.Store.GetCache(o.ID).(*OracleCache) + cache := ic.DAO.Store.GetRWCache(o.ID).(*OracleCache) if cache.requestPriceChanged.Load().(bool) { cache.requestPrice.Store(p) cache.requestPriceChanged.Store(false) @@ -450,7 +480,7 @@ func (o *Oracle) getPrice(ic *interop.Context, _ []stackitem.Item) stackitem.Ite } func (o *Oracle) getPriceInternal(d *dao.Simple) int64 { - cache := d.Store.GetCache(o.ID).(*OracleCache) + cache := d.Store.GetROCache(o.ID).(*OracleCache) if !cache.requestPriceChanged.Load().(bool) { return cache.requestPrice.Load().(int64) } @@ -466,7 +496,7 @@ func (o *Oracle) setPrice(ic *interop.Context, args []stackitem.Item) stackitem. panic("invalid committee signature") } setIntWithKey(o.ID, ic.DAO, prefixRequestPrice, price.Int64()) - cache := ic.DAO.Store.GetCache(o.ID).(*OracleCache) + cache := ic.DAO.Store.GetRWCache(o.ID).(*OracleCache) cache.requestPriceChanged.Store(true) return stackitem.Null{} } diff --git a/pkg/core/native/policy.go b/pkg/core/native/policy.go index 9623ead77..d4446e8b5 100644 --- a/pkg/core/native/policy.go +++ b/pkg/core/native/policy.go @@ -1,6 +1,7 @@ package native import ( + "errors" "fmt" "math/big" "sort" @@ -68,7 +69,36 @@ type PolicyCache struct { blockedAccounts []util.Uint160 } -var _ interop.Contract = (*Policy)(nil) +var ( + _ interop.Contract = (*Policy)(nil) + _ storage.NativeContractCache = (*PolicyCache)(nil) +) + +// Copy implements NativeContractCache interface. +func (c *PolicyCache) Copy() storage.NativeContractCache { + cp := &PolicyCache{} + copyPolicyCache(c, cp) + return cp +} + +// Persist implements NativeContractCache interface. +func (c *PolicyCache) Persist(ps storage.NativeContractCache) (storage.NativeContractCache, error) { + if ps == nil { + ps = &PolicyCache{} + } + psCache, ok := ps.(*PolicyCache) + if !ok { + return nil, errors.New("not a Policy native cache") + } + copyPolicyCache(c, psCache) + return psCache, nil +} + +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 { @@ -184,7 +214,7 @@ func (p *Policy) OnPersist(ic *interop.Context) error { // PostPersist implements Contract interface. func (p *Policy) PostPersist(ic *interop.Context) error { - cache := ic.DAO.Store.GetCache(p.ID).(*PolicyCache) + cache := ic.DAO.Store.GetRWCache(p.ID).(*PolicyCache) cache.lock.Lock() defer cache.lock.Unlock() if cache.isValid { @@ -202,7 +232,7 @@ 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 { - cache := dao.Store.GetCache(p.ID).(*PolicyCache) + cache := dao.Store.GetROCache(p.ID).(*PolicyCache) cache.lock.RLock() defer cache.lock.RUnlock() if cache.isValid { @@ -213,7 +243,7 @@ func (p *Policy) GetFeePerByteInternal(dao *dao.Simple) int64 { // GetMaxVerificationGas returns maximum gas allowed to be burned during verificaion. func (p *Policy) GetMaxVerificationGas(dao *dao.Simple) int64 { - cache := dao.Store.GetCache(p.ID).(*PolicyCache) + cache := dao.Store.GetROCache(p.ID).(*PolicyCache) cache.lock.RLock() defer cache.lock.RUnlock() if cache.isValid { @@ -228,7 +258,7 @@ func (p *Policy) getExecFeeFactor(ic *interop.Context, _ []stackitem.Item) stack // GetExecFeeFactorInternal returns current execution fee factor. func (p *Policy) GetExecFeeFactorInternal(d *dao.Simple) int64 { - cache := d.Store.GetCache(p.ID).(*PolicyCache) + cache := d.Store.GetROCache(p.ID).(*PolicyCache) cache.lock.RLock() defer cache.lock.RUnlock() if cache.isValid { @@ -245,7 +275,7 @@ func (p *Policy) setExecFeeFactor(ic *interop.Context, args []stackitem.Item) st if !p.NEO.checkCommittee(ic) { panic("invalid committee signature") } - cache := ic.DAO.Store.GetCache(p.ID).(*PolicyCache) + cache := ic.DAO.Store.GetRWCache(p.ID).(*PolicyCache) cache.lock.Lock() defer cache.lock.Unlock() setIntWithKey(p.ID, ic.DAO, execFeeFactorKey, int64(value)) @@ -261,7 +291,7 @@ func (p *Policy) isBlocked(ic *interop.Context, args []stackitem.Item) stackitem // IsBlockedInternal checks whether provided account is blocked. func (p *Policy) IsBlockedInternal(dao *dao.Simple, hash util.Uint160) bool { - cache := dao.Store.GetCache(p.ID).(*PolicyCache) + cache := dao.Store.GetROCache(p.ID).(*PolicyCache) cache.lock.RLock() defer cache.lock.RUnlock() if cache.isValid { @@ -284,7 +314,7 @@ func (p *Policy) getStoragePrice(ic *interop.Context, _ []stackitem.Item) stacki // GetStoragePriceInternal returns current execution fee factor. func (p *Policy) GetStoragePriceInternal(d *dao.Simple) int64 { - cache := d.Store.GetCache(p.ID).(*PolicyCache) + cache := d.Store.GetROCache(p.ID).(*PolicyCache) cache.lock.RLock() defer cache.lock.RUnlock() if cache.isValid { @@ -301,7 +331,7 @@ func (p *Policy) setStoragePrice(ic *interop.Context, args []stackitem.Item) sta if !p.NEO.checkCommittee(ic) { panic("invalid committee signature") } - cache := ic.DAO.Store.GetCache(p.ID).(*PolicyCache) + cache := ic.DAO.Store.GetRWCache(p.ID).(*PolicyCache) cache.lock.Lock() defer cache.lock.Unlock() setIntWithKey(p.ID, ic.DAO, storagePriceKey, int64(value)) @@ -318,7 +348,7 @@ func (p *Policy) setFeePerByte(ic *interop.Context, args []stackitem.Item) stack if !p.NEO.checkCommittee(ic) { panic("invalid committee signature") } - cache := ic.DAO.Store.GetCache(p.ID).(*PolicyCache) + cache := ic.DAO.Store.GetRWCache(p.ID).(*PolicyCache) cache.lock.Lock() defer cache.lock.Unlock() setIntWithKey(p.ID, ic.DAO, feePerByteKey, value) @@ -342,7 +372,7 @@ func (p *Policy) blockAccount(ic *interop.Context, args []stackitem.Item) stacki return stackitem.NewBool(false) } key := append([]byte{blockedAccountPrefix}, hash.BytesBE()...) - cache := ic.DAO.Store.GetCache(p.ID).(*PolicyCache) + cache := ic.DAO.Store.GetRWCache(p.ID).(*PolicyCache) cache.lock.Lock() defer cache.lock.Unlock() ic.DAO.PutStorageItem(p.ID, key, state.StorageItem{}) @@ -361,7 +391,7 @@ func (p *Policy) unblockAccount(ic *interop.Context, args []stackitem.Item) stac return stackitem.NewBool(false) } key := append([]byte{blockedAccountPrefix}, hash.BytesBE()...) - cache := ic.DAO.Store.GetCache(p.ID).(*PolicyCache) + cache := ic.DAO.Store.GetRWCache(p.ID).(*PolicyCache) cache.lock.Lock() defer cache.lock.Unlock() ic.DAO.DeleteStorageItem(p.ID, key) diff --git a/pkg/core/storage/memcached_store.go b/pkg/core/storage/memcached_store.go index e241a3b39..fcb50a346 100644 --- a/pkg/core/storage/memcached_store.go +++ b/pkg/core/storage/memcached_store.go @@ -3,6 +3,7 @@ package storage import ( "bytes" "context" + "fmt" "sort" "strings" "sync" @@ -16,7 +17,7 @@ type MemCachedStore struct { MemoryStore nativeCacheLock sync.RWMutex - nativeCache map[int32]*NativeCacheItem + nativeCache map[int32]NativeContractCache private bool // plock protects Persist from double entrance. @@ -25,8 +26,16 @@ type MemCachedStore struct { ps Store } -type NativeCacheItem struct { - Value interface{} +// 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 + // Persist persists changes from upper native cache wrapper to the underlying + // native cache `ps`. The resulting up-to-date cache and an error are returned. + Persist(ps NativeContractCache) (NativeContractCache, error) } type ( @@ -53,12 +62,9 @@ type ( // NewMemCachedStore creates a new MemCachedStore object. func NewMemCachedStore(lower Store) *MemCachedStore { - var cache map[int32]*NativeCacheItem - if cached, ok := lower.(*MemCachedStore); ok { - cache = cached.nativeCache - } else { - cache = make(map[int32]*NativeCacheItem) - } + // Do not copy cache from ps; instead should create clear map: GetRWCache and + // GetROCache will retrieve cache from the underlying nativeCache if requested. + cache := make(map[int32]NativeContractCache) return &MemCachedStore{ MemoryStore: *NewMemoryStore(), nativeCache: cache, @@ -69,12 +75,11 @@ func NewMemCachedStore(lower Store) *MemCachedStore { // NewPrivateMemCachedStore creates a new private (unlocked) MemCachedStore object. // Private cached stores are closed after Persist. func NewPrivateMemCachedStore(lower Store) *MemCachedStore { - var cache map[int32]*NativeCacheItem - if cached, ok := lower.(*MemCachedStore); ok { - cache = cached.nativeCache - } else { - cache = make(map[int32]*NativeCacheItem) - } + // Do not copy cache from ps; instead should create clear map: GetRWCache and + // GetROCache will retrieve cache from the underlying nativeCache if requested. + // The lowest underlying store MUST have its native cache initialized, otherwise + // GetROCache and GetRWCache won't work properly. + cache := make(map[int32]NativeContractCache) return &MemCachedStore{ MemoryStore: *NewMemoryStore(), nativeCache: cache, @@ -362,15 +367,27 @@ func (s *MemCachedStore) persist(isSync bool) (int, error) { } s.mem = nil s.stor = nil + if cached, ok := s.ps.(*MemCachedStore); ok { + for id, nativeCache := range s.nativeCache { + updatedCache, err := nativeCache.Persist(cached.nativeCache[id]) + if err != nil { + return 0, fmt.Errorf("failed to persist native cache changes for private MemCachedStore: %w", err) + } + cached.nativeCache[id] = updatedCache + } + s.nativeCache = nil + } return keys, nil } s.plock.Lock() defer s.plock.Unlock() s.mut.Lock() + s.nativeCacheLock.Lock() keys = len(s.mem) + len(s.stor) if keys == 0 { + s.nativeCacheLock.Unlock() s.mut.Unlock() return 0, nil } @@ -379,18 +396,35 @@ func (s *MemCachedStore) persist(isSync bool) (int, error) { // starts using fresh new maps. This tempstore is only known here and // nothing ever changes it, therefore accesses to it (reads) can go // unprotected while writes are handled by s proper. - var tempstore = &MemCachedStore{MemoryStore: MemoryStore{mem: s.mem, stor: s.stor}, ps: s.ps} + var tempstore = &MemCachedStore{MemoryStore: MemoryStore{mem: s.mem, stor: s.stor}, ps: s.ps, nativeCache: s.nativeCache} s.ps = tempstore s.mem = make(map[string][]byte, len(s.mem)) s.stor = make(map[string][]byte, len(s.stor)) + cached, isPSCached := tempstore.ps.(*MemCachedStore) + if isPSCached { + s.nativeCache = make(map[int32]NativeContractCache) + } if !isSync { + s.nativeCacheLock.Unlock() s.mut.Unlock() } - + if isPSCached { + cached.nativeCacheLock.Lock() + for id, nativeCache := range tempstore.nativeCache { + updatedCache, err := nativeCache.Persist(cached.nativeCache[id]) + if err != nil { + cached.nativeCacheLock.Unlock() + return 0, fmt.Errorf("failed to persist native cache changes: %w", err) + } + cached.nativeCache[id] = updatedCache + } + cached.nativeCacheLock.Unlock() + } err = tempstore.ps.PutChangeSet(tempstore.mem, tempstore.stor) if !isSync { s.mut.Lock() + s.nativeCacheLock.Lock() } if err == nil { // tempstore.mem and tempstore.del are completely flushed now @@ -406,31 +440,71 @@ func (s *MemCachedStore) persist(isSync bool) (int, error) { for k := range s.stor { put(tempstore.stor, k, s.stor[k]) } + if isPSCached { + for id, nativeCache := range s.nativeCache { + updatedCache, err := nativeCache.Persist(tempstore.nativeCache[id]) + if err != nil { + return 0, fmt.Errorf("failed to persist native cache changes: %w", err) + } + tempstore.nativeCache[id] = updatedCache + } + s.nativeCache = tempstore.nativeCache + } s.ps = tempstore.ps s.mem = tempstore.mem s.stor = tempstore.stor } + s.nativeCacheLock.Unlock() s.mut.Unlock() return keys, err } -func (s *MemCachedStore) GetCache(k int32) interface{} { +// 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 (s *MemCachedStore) GetROCache(id int32) NativeContractCache { s.nativeCacheLock.RLock() defer s.nativeCacheLock.RUnlock() - if itm, ok := s.nativeCache[k]; ok { - return itm.Value - } - return nil + return s.getCache(id, true) } -func (s *MemCachedStore) SetCache(k int32, v interface{}) { +// GetRWCache returns native contact cache. The cache CAN BE safely modified +// by the caller. +func (s *MemCachedStore) GetRWCache(k int32) NativeContractCache { s.nativeCacheLock.Lock() defer s.nativeCacheLock.Unlock() - s.nativeCache[k] = &NativeCacheItem{ - Value: v, + return s.getCache(k, false) +} + +func (s *MemCachedStore) getCache(k int32, ro bool) NativeContractCache { + if itm, ok := s.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 cached, ok := s.ps.(*MemCachedStore); ok { + if ro { + return cached.GetROCache(k) + } + v := cached.GetRWCache(k) + if v != nil { + // Create a copy here in order not to modify the existing cache. + cp := v.Copy() + s.nativeCache[k] = cp + return cp + } + } + + return nil +} + +func (s *MemCachedStore) SetCache(k int32, v NativeContractCache) { + s.nativeCacheLock.Lock() + defer s.nativeCacheLock.Unlock() + + s.nativeCache[k] = v } // Close implements Store interface, clears up memory and closes the lower layer From 8ec8511d9d2ad108426064ea9f73081ac0758212 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Tue, 19 Apr 2022 12:26:46 +0300 Subject: [PATCH 09/24] core: remove mutexes and atomic values from native cache Native cache is always wrapped into independant layers, so concurrent RW access is no-op. --- pkg/core/native/designate.go | 62 +++++--------- pkg/core/native/management.go | 14 ---- pkg/core/native/native_neo.go | 154 +++++++++++++++------------------- pkg/core/native/notary.go | 16 +--- pkg/core/native/oracle.go | 27 +++--- pkg/core/native/policy.go | 24 ------ 6 files changed, 101 insertions(+), 196 deletions(-) diff --git a/pkg/core/native/designate.go b/pkg/core/native/designate.go index 93411c46f..33cda42c2 100644 --- a/pkg/core/native/designate.go +++ b/pkg/core/native/designate.go @@ -48,11 +48,11 @@ type roleData struct { } type DesignationCache struct { - rolesChangedFlag atomic.Value - oracles atomic.Value - stateVals atomic.Value - neofsAlphabet atomic.Value - notaries atomic.Value + rolesChangedFlag bool + oracles roleData + stateVals roleData + neofsAlphabet roleData + notaries roleData } const ( @@ -101,24 +101,7 @@ func (c *DesignationCache) Persist(ps storage.NativeContractCache) (storage.Nati } func copyDesignationCache(src, dst *DesignationCache) { - dst.rolesChangedFlag.Store(src.rolesChangedFlag.Load()) - for _, r := range []noderoles.Role{noderoles.StateValidator, noderoles.Oracle, noderoles.NeoFSAlphabet, noderoles.P2PNotary} { - data := getCachedRoleData(src, r) - if data != nil { - var v = &roleData{} - *v = *data - switch r { - case noderoles.StateValidator: - dst.stateVals.Store(v) - case noderoles.Oracle: - dst.oracles.Store(v) - case noderoles.NeoFSAlphabet: - dst.neofsAlphabet.Store(v) - case noderoles.P2PNotary: - dst.notaries.Store(v) - } - } - } + *dst = *src } func (s *Designate) isValidRole(r noderoles.Role) bool { @@ -153,7 +136,7 @@ func newDesignate(p2pSigExtensionsEnabled bool) *Designate { // Initialize initializes Oracle contract. func (s *Designate) Initialize(ic *interop.Context) error { cache := &DesignationCache{} - cache.rolesChangedFlag.Store(true) + cache.rolesChangedFlag = true ic.DAO.Store.SetCache(s.ID, cache) return nil } @@ -185,7 +168,7 @@ func (s *Designate) PostPersist(ic *interop.Context) error { } } - cache.rolesChangedFlag.Store(false) + cache.rolesChangedFlag = false return nil } @@ -215,8 +198,7 @@ func (s *Designate) getDesignatedByRole(ic *interop.Context, args []stackitem.It } func rolesChanged(cache *DesignationCache) bool { - rc := cache.rolesChangedFlag.Load() - return rc == nil || rc.(bool) + return cache.rolesChangedFlag } func (s *Designate) hashFromNodes(r noderoles.Role, nodes keys.PublicKeys) util.Uint160 { @@ -233,16 +215,14 @@ 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 { +func (s *Designate) updateCachedRoleData(v *roleData, d *dao.Simple, r noderoles.Role) error { nodeKeys, height, err := s.GetDesignatedByRole(d, r, math.MaxUint32) if err != nil { return err } - v.Store(&roleData{ - nodes: nodeKeys, - addr: s.hashFromNodes(r, nodeKeys), - height: height, - }) + v.nodes = nodeKeys + v.addr = s.hashFromNodes(r, nodeKeys) + v.height = height switch r { case noderoles.Oracle: if orc, _ := s.OracleService.Load().(services.Oracle); orc != nil { @@ -261,19 +241,15 @@ func (s *Designate) updateCachedRoleData(v *atomic.Value, d *dao.Simple, r noder } func getCachedRoleData(cache *DesignationCache, r noderoles.Role) *roleData { - var val interface{} switch r { case noderoles.Oracle: - val = cache.oracles.Load() + return &cache.oracles case noderoles.StateValidator: - val = cache.stateVals.Load() + return &cache.stateVals case noderoles.NeoFSAlphabet: - val = cache.neofsAlphabet.Load() + return &cache.neofsAlphabet case noderoles.P2PNotary: - val = cache.notaries.Load() - } - if val != nil { - return val.(*roleData) + return &cache.notaries } return nil } @@ -381,7 +357,7 @@ func (s *Designate) DesignateAsRole(ic *interop.Context, r noderoles.Role, pubs } sort.Sort(pubs) nl := NodeList(pubs) - ic.DAO.Store.GetRWCache(s.ID).(*DesignationCache).rolesChangedFlag.Store(true) + ic.DAO.Store.GetRWCache(s.ID).(*DesignationCache).rolesChangedFlag = true err := putConvertibleToDAO(s.ID, ic.DAO, key, &nl) if err != nil { return err @@ -413,6 +389,6 @@ func (s *Designate) getRole(item stackitem.Item) (noderoles.Role, bool) { // InitializeCache invalidates native Designate cache. func (s *Designate) InitializeCache(d *dao.Simple) { cache := &DesignationCache{} - cache.rolesChangedFlag.Store(true) + cache.rolesChangedFlag = true d.Store.SetCache(s.ID, cache) } diff --git a/pkg/core/native/management.go b/pkg/core/native/management.go index 765bb7ed4..00286955b 100644 --- a/pkg/core/native/management.go +++ b/pkg/core/native/management.go @@ -6,7 +6,6 @@ import ( "fmt" "math" "math/big" - "sync" "unicode/utf8" "github.com/nspcc-dev/neo-go/pkg/core/dao" @@ -33,7 +32,6 @@ type Management struct { } type ManagementCache struct { - mtx sync.RWMutex 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{} @@ -188,9 +186,7 @@ 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) { cache := d.Store.GetROCache(m.ID).(*ManagementCache) - cache.mtx.RLock() cs, ok := cache.contracts[hash] - cache.mtx.RUnlock() if !ok { return nil, storage.ErrKeyNotFound } else if cs != nil { @@ -304,10 +300,8 @@ func (m *Management) deployWithData(ic *interop.Context, args []stackitem.Item) func (m *Management) markUpdated(d *dao.Simple, h util.Uint160) { cache := d.Store.GetRWCache(m.ID).(*ManagementCache) - cache.mtx.Lock() // Just set it to nil, to refresh cache in `PostPersist`. cache.contracts[h] = nil - cache.mtx.Unlock() } // Deploy creates contract's hash/ID and saves new contract into the given DAO. @@ -520,9 +514,7 @@ func (m *Management) OnPersist(ic *interop.Context) error { if cache == nil { cache = ic.DAO.Store.GetRWCache(m.ID).(*ManagementCache) } - cache.mtx.Lock() updateContractCache(cache, cs) - cache.mtx.Unlock() } return nil @@ -558,8 +550,6 @@ func (m *Management) InitializeCache(d *dao.Simple) error { // PostPersist implements Contract interface. func (m *Management) PostPersist(ic *interop.Context) error { cache := ic.DAO.Store.GetRWCache(m.ID).(*ManagementCache) - cache.mtx.Lock() - defer cache.mtx.Unlock() for h, cs := range cache.contracts { if cs != nil { continue @@ -582,12 +572,10 @@ func (m *Management) PostPersist(ic *interop.Context) error { // is returned. func (m *Management) GetNEP11Contracts(d *dao.Simple) []util.Uint160 { cache := d.Store.GetROCache(m.ID).(*ManagementCache) - cache.mtx.RLock() result := make([]util.Uint160, 0, len(cache.nep11)) for h := range cache.nep11 { result = append(result, h) } - cache.mtx.RUnlock() return result } @@ -596,12 +584,10 @@ func (m *Management) GetNEP11Contracts(d *dao.Simple) []util.Uint160 { // is returned. func (m *Management) GetNEP17Contracts(d *dao.Simple) []util.Uint160 { cache := d.Store.GetROCache(m.ID).(*ManagementCache) - cache.mtx.RLock() result := make([]util.Uint160, 0, len(cache.nep17)) for h := range cache.nep17 { result = append(result, h) } - cache.mtx.RUnlock() return result } diff --git a/pkg/core/native/native_neo.go b/pkg/core/native/native_neo.go index bf2574121..85ec58fed 100644 --- a/pkg/core/native/native_neo.go +++ b/pkg/core/native/native_neo.go @@ -8,7 +8,6 @@ import ( "math/big" "sort" "strings" - "sync/atomic" "github.com/nspcc-dev/neo-go/pkg/config" "github.com/nspcc-dev/neo-go/pkg/core/dao" @@ -44,22 +43,22 @@ type NEO struct { type NeoCache struct { // 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 + gasPerBlock gasRecord + gasPerBlockChanged bool - registerPrice atomic.Value - registerPriceChanged atomic.Value + registerPrice int64 + registerPriceChanged bool - votesChanged atomic.Value - nextValidators atomic.Value - validators atomic.Value + 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 @@ -134,27 +133,21 @@ func (c *NeoCache) Persist(ps storage.NativeContractCache) (storage.NativeContra } func copyNeoCache(src, dst *NeoCache) { - dst.votesChanged.Store(src.votesChanged.Load()) - dst.nextValidators.Store(src.nextValidators.Load().(keys.PublicKeys).Copy()) - dst.validators.Store(src.validators.Load().(keys.PublicKeys).Copy()) - committeeSrc := src.committee.Load().(keysWithVotes) - committeeDst := make(keysWithVotes, len(committeeSrc)) - copy(committeeDst, committeeSrc) - dst.committee.Store(committeeDst) - dst.committeeHash.Store(src.committeeHash.Load()) - regPriceChanged := src.registerPriceChanged.Load().(bool) - dst.registerPriceChanged.Store(regPriceChanged) - if !regPriceChanged { - dst.registerPrice.Store(src.registerPrice.Load()) - } - gasPerBlockChanged := src.gasPerBlockChanged.Load().(bool) - dst.gasPerBlockChanged.Store(gasPerBlockChanged) - if !gasPerBlockChanged { - recordsSrc := src.gasPerBlock.Load().(gasRecord) - recordsDst := make(gasRecord, len(recordsSrc)) - copy(recordsDst, recordsSrc) - dst.gasPerBlock.Store(recordsDst) - } + dst.votesChanged = src.votesChanged + dst.nextValidators = src.nextValidators.Copy() + dst.validators = src.validators.Copy() + + dst.committee = make(keysWithVotes, len(src.committee)) + copy(dst.committee, src.committee) + dst.committeeHash = src.committeeHash + + dst.registerPriceChanged = src.registerPriceChanged + dst.registerPrice = src.registerPrice + + dst.gasPerBlockChanged = src.gasPerBlockChanged + 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 @@ -262,14 +255,10 @@ func (n *NEO) Initialize(ic *interop.Context) error { } cache := &NeoCache{ - gasPerVoteCache: make(map[string]big.Int), + gasPerVoteCache: make(map[string]big.Int), + votesChanged: true, + registerPriceChanged: true, } - cache.votesChanged.Store(true) - cache.nextValidators.Store(keys.PublicKeys(nil)) - cache.validators.Store(keys.PublicKeys(nil)) - cache.committee.Store(keysWithVotes(nil)) - cache.committeeHash.Store(util.Uint160{}) - cache.registerPriceChanged.Store(true) // We need cache to be present in DAO before the subsequent call to `mint`. ic.DAO.Store.SetCache(n.ID, cache) @@ -294,13 +283,13 @@ func (n *NEO) Initialize(ic *interop.Context) error { n.putGASRecord(ic.DAO, index, value) gr := &gasRecord{{Index: index, GASPerBlock: *value}} - cache.gasPerBlock.Store(*gr) - cache.gasPerBlockChanged.Store(false) + cache.gasPerBlock = *gr + cache.gasPerBlockChanged = false ic.DAO.PutStorageItem(n.ID, []byte{prefixVotersCount}, state.StorageItem{}) setIntWithKey(n.ID, ic.DAO, []byte{prefixRegisterPrice}, DefaultRegisterPrice) - cache.registerPrice.Store(int64(DefaultRegisterPrice)) - cache.registerPriceChanged.Store(false) + cache.registerPrice = int64(DefaultRegisterPrice) + cache.registerPriceChanged = false return nil } @@ -310,16 +299,11 @@ func (n *NEO) Initialize(ic *interop.Context) error { // called only when deploying native contracts. func (n *NEO) InitializeCache(bc interop.Ledger, d *dao.Simple) error { cache := &NeoCache{ - gasPerVoteCache: make(map[string]big.Int), + gasPerVoteCache: make(map[string]big.Int), + votesChanged: true, + registerPriceChanged: true, } - cache.votesChanged.Store(true) - cache.nextValidators.Store(keys.PublicKeys(nil)) - cache.validators.Store(keys.PublicKeys(nil)) - cache.committee.Store(keysWithVotes(nil)) - cache.committeeHash.Store(util.Uint160{}) - cache.registerPriceChanged.Store(true) - var committee = keysWithVotes{} si := d.GetStorageItem(n.ID, prefixCommittee) if err := committee.DecodeBytes(si); err != nil { @@ -329,8 +313,8 @@ func (n *NEO) InitializeCache(bc interop.Ledger, d *dao.Simple) error { return fmt.Errorf("failed to update cache: %w", err) } - cache.gasPerBlock.Store(n.getSortedGASRecordFromDAO(d)) - cache.gasPerBlockChanged.Store(false) + cache.gasPerBlock = n.getSortedGASRecordFromDAO(d) + cache.gasPerBlockChanged = false d.Store.SetCache(n.ID, cache) return nil @@ -345,28 +329,26 @@ func (n *NEO) initConfigCache(cfg config.ProtocolConfiguration) error { } func (n *NEO) updateCache(cache *NeoCache, cvs keysWithVotes, bc interop.Ledger) error { - cache.committee.Store(cvs) + cache.committee = cvs var committee = getCommitteeMembers(cache) script, err := smartcontract.CreateMajorityMultiSigRedeemScript(committee.Copy()) if err != nil { return err } - cache.committeeHash.Store(hash.Hash160(script)) + cache.committeeHash = hash.Hash160(script) // TODO: use block height from interop context for proper historical calls handling. nextVals := committee[:n.cfg.GetNumOfCNs(bc.BlockHeight()+1)].Copy() sort.Sort(nextVals) - cache.nextValidators.Store(nextVals) + cache.nextValidators = nextVals return nil } func (n *NEO) updateCommittee(cache *NeoCache, ic *interop.Context) error { - votesChanged := cache.votesChanged.Load().(bool) - if !votesChanged { + if !cache.votesChanged { // We need to put in storage anyway, as it affects dumps - committee := cache.committee.Load().(keysWithVotes) - ic.DAO.PutStorageItem(n.ID, prefixCommittee, committee.Bytes()) + ic.DAO.PutStorageItem(n.ID, prefixCommittee, cache.committee.Bytes()) return nil } @@ -377,7 +359,7 @@ func (n *NEO) updateCommittee(cache *NeoCache, ic *interop.Context) error { if err := n.updateCache(cache, cvs, ic.Chain); err != nil { return err } - cache.votesChanged.Store(false) + cache.votesChanged = false ic.DAO.PutStorageItem(n.ID, prefixCommittee, cvs.Bytes()) return nil } @@ -386,11 +368,11 @@ func (n *NEO) updateCommittee(cache *NeoCache, ic *interop.Context) error { func (n *NEO) OnPersist(ic *interop.Context) error { if n.cfg.ShouldUpdateCommitteeAt(ic.Block.Index) { cache := ic.DAO.Store.GetRWCache(n.ID).(*NeoCache) - oldKeys := cache.nextValidators.Load().(keys.PublicKeys) - oldCom := cache.committee.Load().(keysWithVotes) + oldKeys := cache.nextValidators + oldCom := cache.committee if n.cfg.GetNumOfCNs(ic.Block.Index) != len(oldKeys) || n.cfg.GetCommitteeSize(ic.Block.Index) != len(oldCom) { - cache.votesChanged.Store(true) + cache.votesChanged = true } if err := n.updateCommittee(cache, ic); err != nil { return err @@ -417,7 +399,7 @@ func (n *NEO) PostPersist(ic *interop.Context) error { voterReward.Div(voterReward, big.NewInt(int64(committeeSize+validatorsCount))) voterReward.Div(voterReward, big100) - var cs = cache.committee.Load().(keysWithVotes) + var cs = cache.committee var key = make([]byte, 38) for i := range cs { if cs[i].Votes.Sign() > 0 { @@ -448,15 +430,15 @@ func (n *NEO) PostPersist(ic *interop.Context) error { } } } - if cache.gasPerBlockChanged.Load().(bool) { - cache.gasPerBlock.Store(n.getSortedGASRecordFromDAO(ic.DAO)) - cache.gasPerBlockChanged.Store(false) + if cache.gasPerBlockChanged { + cache.gasPerBlock = n.getSortedGASRecordFromDAO(ic.DAO) + cache.gasPerBlockChanged = false } - if cache.registerPriceChanged.Load().(bool) { + if cache.registerPriceChanged { p := getIntWithKey(n.ID, ic.DAO, []byte{prefixRegisterPrice}) - cache.registerPrice.Store(p) - cache.registerPriceChanged.Store(false) + cache.registerPrice = p + cache.registerPriceChanged = false } return nil } @@ -581,10 +563,10 @@ func (n *NEO) getSortedGASRecordFromDAO(d *dao.Simple) gasRecord { func (n *NEO) GetGASPerBlock(d *dao.Simple, index uint32) *big.Int { cache := d.Store.GetROCache(n.ID).(*NeoCache) var gr gasRecord - if cache.gasPerBlockChanged.Load().(bool) { + if cache.gasPerBlockChanged { gr = n.getSortedGASRecordFromDAO(d) } else { - gr = cache.gasPerBlock.Load().(gasRecord) + gr = cache.gasPerBlock } for i := len(gr) - 1; i >= 0; i-- { if gr[i].Index <= index { @@ -598,7 +580,7 @@ func (n *NEO) GetGASPerBlock(d *dao.Simple, index uint32) *big.Int { // GetCommitteeAddress returns address of the committee. func (n *NEO) GetCommitteeAddress(d *dao.Simple) util.Uint160 { cache := d.Store.GetROCache(n.ID).(*NeoCache) - return cache.committeeHash.Load().(util.Uint160) + return cache.committeeHash } func (n *NEO) checkCommittee(ic *interop.Context) bool { @@ -627,7 +609,7 @@ func (n *NEO) SetGASPerBlock(ic *interop.Context, index uint32, gas *big.Int) er return errors.New("invalid committee signature") } cache := ic.DAO.Store.GetRWCache(n.ID).(*NeoCache) - cache.gasPerBlockChanged.Store(true) + cache.gasPerBlockChanged = true n.putGASRecord(ic.DAO, index, gas) return nil } @@ -638,8 +620,8 @@ func (n *NEO) getRegisterPrice(ic *interop.Context, _ []stackitem.Item) stackite func (n *NEO) getRegisterPriceInternal(d *dao.Simple) int64 { cache := d.Store.GetROCache(n.ID).(*NeoCache) - if !cache.registerPriceChanged.Load().(bool) { - return cache.registerPrice.Load().(int64) + if !cache.registerPriceChanged { + return cache.registerPrice } return getIntWithKey(n.ID, d, []byte{prefixRegisterPrice}) } @@ -655,7 +637,7 @@ func (n *NEO) setRegisterPrice(ic *interop.Context, args []stackitem.Item) stack setIntWithKey(n.ID, ic.DAO, []byte{prefixRegisterPrice}, price.Int64()) cache := ic.DAO.Store.GetRWCache(n.ID).(*NeoCache) - cache.registerPriceChanged.Store(true) + cache.registerPriceChanged = true return stackitem.Null{} } @@ -726,8 +708,8 @@ func (n *NEO) CalculateNEOHolderReward(d *dao.Simple, value *big.Int, start, end } var gr gasRecord cache := d.Store.GetROCache(n.ID).(*NeoCache) - if !cache.gasPerBlockChanged.Load().(bool) { - gr = cache.gasPerBlock.Load().(gasRecord) + if !cache.gasPerBlockChanged { + gr = cache.gasPerBlock } else { gr = n.getSortedGASRecordFromDAO(d) } @@ -801,7 +783,7 @@ func (n *NEO) UnregisterCandidateInternal(ic *interop.Context, pub *keys.PublicK return nil } cache := ic.DAO.Store.GetRWCache(n.ID).(*NeoCache) - cache.validators.Store(keys.PublicKeys(nil)) + cache.validators = nil c := new(candidate).FromBytes(si) c.Registered = false ok, err := n.dropCandidateIfZero(ic.DAO, cache, pub, c) @@ -881,7 +863,7 @@ func (n *NEO) VoteInternal(ic *interop.Context, h util.Uint160, pub *keys.Public // 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 { cache := d.Store.GetRWCache(n.ID).(*NeoCache) - cache.votesChanged.Store(true) + cache.votesChanged = true if acc.VoteTo != nil { key := makeValidatorKey(acc.VoteTo) si := d.GetStorageItem(n.ID, key) @@ -896,7 +878,7 @@ func (n *NEO) ModifyAccountVotes(acc *state.NEOBalance, d *dao.Simple, value *bi return err } } - cache.validators.Store(keys.PublicKeys(nil)) + cache.validators = nil return putConvertibleToDAO(n.ID, d, key, cd) } return nil @@ -992,7 +974,7 @@ func (n *NEO) getAccountState(ic *interop.Context, args []stackitem.Item) stacki func (n *NEO) ComputeNextBlockValidators(bc interop.Ledger, d *dao.Simple) (keys.PublicKeys, error) { numOfCNs := n.cfg.GetNumOfCNs(bc.BlockHeight() + 1) cache := d.Store.GetRWCache(n.ID).(*NeoCache) - if vals := cache.validators.Load().(keys.PublicKeys); vals != nil && numOfCNs == len(vals) { + if vals := cache.validators; vals != nil && numOfCNs == len(vals) { return vals.Copy(), nil } result, _, err := n.computeCommitteeMembers(bc, d) @@ -1001,7 +983,7 @@ func (n *NEO) ComputeNextBlockValidators(bc interop.Ledger, d *dao.Simple) (keys } result = result[:numOfCNs] sort.Sort(result) - cache.validators.Store(result) + cache.validators = result return result, nil } @@ -1031,7 +1013,7 @@ func (n *NEO) GetCommitteeMembers(d *dao.Simple) keys.PublicKeys { } func getCommitteeMembers(cache *NeoCache) keys.PublicKeys { - var cvs = cache.committee.Load().(keysWithVotes) + var cvs = cache.committee var committee = make(keys.PublicKeys, len(cvs)) var err error for i := range committee { @@ -1107,7 +1089,7 @@ func (n *NEO) getNextBlockValidators(ic *interop.Context, _ []stackitem.Item) st // GetNextBlockValidatorsInternal returns next block validators. func (n *NEO) GetNextBlockValidatorsInternal(d *dao.Simple) keys.PublicKeys { cache := d.Store.GetROCache(n.ID).(*NeoCache) - return cache.nextValidators.Load().(keys.PublicKeys).Copy() + return cache.nextValidators.Copy() } // BalanceOf returns native NEO token balance for the acc. diff --git a/pkg/core/native/notary.go b/pkg/core/native/notary.go index 817a5e73a..325cedce6 100644 --- a/pkg/core/native/notary.go +++ b/pkg/core/native/notary.go @@ -5,7 +5,6 @@ import ( "fmt" "math" "math/big" - "sync" "github.com/nspcc-dev/neo-go/pkg/core/dao" "github.com/nspcc-dev/neo-go/pkg/core/interop" @@ -35,7 +34,6 @@ type Notary struct { } type NotaryCache struct { - 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. @@ -84,9 +82,7 @@ func (c *NotaryCache) Persist(ps storage.NativeContractCache) (storage.NativeCon } func copyNotaryCache(src, dst *NotaryCache) { - dst.isValid = src.isValid - dst.maxNotValidBeforeDelta = src.maxNotValidBeforeDelta - dst.notaryServiceFeePerKey = src.notaryServiceFeePerKey + *dst = *src } // newNotary returns Notary native contract. @@ -224,8 +220,6 @@ func (n *Notary) OnPersist(ic *interop.Context) error { // PostPersist implements Contract interface. func (n *Notary) PostPersist(ic *interop.Context) error { cache := ic.DAO.Store.GetRWCache(n.ID).(*NotaryCache) - cache.lock.Lock() - defer cache.lock.Unlock() if cache.isValid { return nil } @@ -440,8 +434,6 @@ 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 { cache := dao.Store.GetROCache(n.ID).(*NotaryCache) - cache.lock.RLock() - defer cache.lock.RUnlock() if cache.isValid { return cache.maxNotValidBeforeDelta } @@ -460,8 +452,6 @@ func (n *Notary) setMaxNotValidBeforeDelta(ic *interop.Context, args []stackitem panic("invalid committee signature") } cache := ic.DAO.Store.GetRWCache(n.ID).(*NotaryCache) - cache.lock.Lock() - defer cache.lock.Unlock() setIntWithKey(n.ID, ic.DAO, maxNotValidBeforeDeltaKey, int64(value)) cache.isValid = false return stackitem.Null{} @@ -475,8 +465,6 @@ 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 { cache := dao.Store.GetROCache(n.ID).(*NotaryCache) - cache.lock.RLock() - defer cache.lock.RUnlock() if cache.isValid { return cache.notaryServiceFeePerKey } @@ -493,8 +481,6 @@ func (n *Notary) setNotaryServiceFeePerKey(ic *interop.Context, args []stackitem panic("invalid committee signature") } cache := ic.DAO.Store.GetRWCache(n.ID).(*NotaryCache) - cache.lock.Lock() - defer cache.lock.Unlock() setIntWithKey(n.ID, ic.DAO, notaryServiceFeeKey, int64(value)) cache.isValid = false return stackitem.Null{} diff --git a/pkg/core/native/oracle.go b/pkg/core/native/oracle.go index 893b5cebf..f1914cd65 100644 --- a/pkg/core/native/oracle.go +++ b/pkg/core/native/oracle.go @@ -47,8 +47,8 @@ type Oracle struct { } type OracleCache struct { - requestPrice atomic.Value - requestPriceChanged atomic.Value + requestPrice int64 + requestPriceChanged bool } const ( @@ -110,8 +110,7 @@ func (c *OracleCache) Persist(ps storage.NativeContractCache) (storage.NativeCon } func copyOracleCache(src, dst *OracleCache) { - dst.requestPrice.Store(src.requestPrice.Load()) - dst.requestPriceChanged.Store(src.requestPriceChanged.Load()) + *dst = *src } func newOracle() *Oracle { @@ -174,9 +173,9 @@ func (o *Oracle) OnPersist(ic *interop.Context) error { func (o *Oracle) PostPersist(ic *interop.Context) error { p := o.getPriceInternal(ic.DAO) cache := ic.DAO.Store.GetRWCache(o.ID).(*OracleCache) - if cache.requestPriceChanged.Load().(bool) { - cache.requestPrice.Store(p) - cache.requestPriceChanged.Store(false) + if cache.requestPriceChanged { + cache.requestPrice = p + cache.requestPriceChanged = false } var nodes keys.PublicKeys @@ -253,16 +252,16 @@ func (o *Oracle) Initialize(ic *interop.Context) error { setIntWithKey(o.ID, ic.DAO, prefixRequestPrice, DefaultOracleRequestPrice) cache := &OracleCache{} - cache.requestPrice.Store(int64(DefaultOracleRequestPrice)) - cache.requestPriceChanged.Store(false) + cache.requestPrice = int64(DefaultOracleRequestPrice) + cache.requestPriceChanged = false ic.DAO.Store.SetCache(o.ID, cache) return nil } func (o *Oracle) InitializeCache(d *dao.Simple) { cache := &OracleCache{} - cache.requestPrice.Store(getIntWithKey(o.ID, d, prefixRequestPrice)) - cache.requestPriceChanged.Store(false) + cache.requestPrice = getIntWithKey(o.ID, d, prefixRequestPrice) + cache.requestPriceChanged = false d.Store.SetCache(o.ID, cache) } @@ -481,8 +480,8 @@ func (o *Oracle) getPrice(ic *interop.Context, _ []stackitem.Item) stackitem.Ite func (o *Oracle) getPriceInternal(d *dao.Simple) int64 { cache := d.Store.GetROCache(o.ID).(*OracleCache) - if !cache.requestPriceChanged.Load().(bool) { - return cache.requestPrice.Load().(int64) + if !cache.requestPriceChanged { + return cache.requestPrice } return getIntWithKey(o.ID, d, prefixRequestPrice) } @@ -497,7 +496,7 @@ func (o *Oracle) setPrice(ic *interop.Context, args []stackitem.Item) stackitem. } setIntWithKey(o.ID, ic.DAO, prefixRequestPrice, price.Int64()) cache := ic.DAO.Store.GetRWCache(o.ID).(*OracleCache) - cache.requestPriceChanged.Store(true) + cache.requestPriceChanged = true return stackitem.Null{} } diff --git a/pkg/core/native/policy.go b/pkg/core/native/policy.go index d4446e8b5..78b64d652 100644 --- a/pkg/core/native/policy.go +++ b/pkg/core/native/policy.go @@ -5,7 +5,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" @@ -57,7 +56,6 @@ type Policy struct { } type PolicyCache struct { - 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. @@ -215,8 +213,6 @@ func (p *Policy) OnPersist(ic *interop.Context) error { // PostPersist implements Contract interface. func (p *Policy) PostPersist(ic *interop.Context) error { cache := ic.DAO.Store.GetRWCache(p.ID).(*PolicyCache) - cache.lock.Lock() - defer cache.lock.Unlock() if cache.isValid { return nil } @@ -233,8 +229,6 @@ 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 { cache := dao.Store.GetROCache(p.ID).(*PolicyCache) - cache.lock.RLock() - defer cache.lock.RUnlock() if cache.isValid { return cache.feePerByte } @@ -244,8 +238,6 @@ func (p *Policy) GetFeePerByteInternal(dao *dao.Simple) int64 { // GetMaxVerificationGas returns maximum gas allowed to be burned during verificaion. func (p *Policy) GetMaxVerificationGas(dao *dao.Simple) int64 { cache := dao.Store.GetROCache(p.ID).(*PolicyCache) - cache.lock.RLock() - defer cache.lock.RUnlock() if cache.isValid { return cache.maxVerificationGas } @@ -259,8 +251,6 @@ func (p *Policy) getExecFeeFactor(ic *interop.Context, _ []stackitem.Item) stack // GetExecFeeFactorInternal returns current execution fee factor. func (p *Policy) GetExecFeeFactorInternal(d *dao.Simple) int64 { cache := d.Store.GetROCache(p.ID).(*PolicyCache) - cache.lock.RLock() - defer cache.lock.RUnlock() if cache.isValid { return int64(cache.execFeeFactor) } @@ -276,8 +266,6 @@ func (p *Policy) setExecFeeFactor(ic *interop.Context, args []stackitem.Item) st panic("invalid committee signature") } cache := ic.DAO.Store.GetRWCache(p.ID).(*PolicyCache) - cache.lock.Lock() - defer cache.lock.Unlock() setIntWithKey(p.ID, ic.DAO, execFeeFactorKey, int64(value)) cache.isValid = false return stackitem.Null{} @@ -292,8 +280,6 @@ func (p *Policy) isBlocked(ic *interop.Context, args []stackitem.Item) stackitem // IsBlockedInternal checks whether provided account is blocked. func (p *Policy) IsBlockedInternal(dao *dao.Simple, hash util.Uint160) bool { cache := dao.Store.GetROCache(p.ID).(*PolicyCache) - cache.lock.RLock() - defer cache.lock.RUnlock() if cache.isValid { length := len(cache.blockedAccounts) i := sort.Search(length, func(i int) bool { @@ -315,8 +301,6 @@ func (p *Policy) getStoragePrice(ic *interop.Context, _ []stackitem.Item) stacki // GetStoragePriceInternal returns current execution fee factor. func (p *Policy) GetStoragePriceInternal(d *dao.Simple) int64 { cache := d.Store.GetROCache(p.ID).(*PolicyCache) - cache.lock.RLock() - defer cache.lock.RUnlock() if cache.isValid { return int64(cache.storagePrice) } @@ -332,8 +316,6 @@ func (p *Policy) setStoragePrice(ic *interop.Context, args []stackitem.Item) sta panic("invalid committee signature") } cache := ic.DAO.Store.GetRWCache(p.ID).(*PolicyCache) - cache.lock.Lock() - defer cache.lock.Unlock() setIntWithKey(p.ID, ic.DAO, storagePriceKey, int64(value)) cache.isValid = false return stackitem.Null{} @@ -349,8 +331,6 @@ func (p *Policy) setFeePerByte(ic *interop.Context, args []stackitem.Item) stack panic("invalid committee signature") } cache := ic.DAO.Store.GetRWCache(p.ID).(*PolicyCache) - cache.lock.Lock() - defer cache.lock.Unlock() setIntWithKey(p.ID, ic.DAO, feePerByteKey, value) cache.isValid = false return stackitem.Null{} @@ -373,8 +353,6 @@ func (p *Policy) blockAccount(ic *interop.Context, args []stackitem.Item) stacki } key := append([]byte{blockedAccountPrefix}, hash.BytesBE()...) cache := ic.DAO.Store.GetRWCache(p.ID).(*PolicyCache) - cache.lock.Lock() - defer cache.lock.Unlock() ic.DAO.PutStorageItem(p.ID, key, state.StorageItem{}) cache.isValid = false return stackitem.NewBool(true) @@ -392,8 +370,6 @@ func (p *Policy) unblockAccount(ic *interop.Context, args []stackitem.Item) stac } key := append([]byte{blockedAccountPrefix}, hash.BytesBE()...) cache := ic.DAO.Store.GetRWCache(p.ID).(*PolicyCache) - cache.lock.Lock() - defer cache.lock.Unlock() ic.DAO.DeleteStorageItem(p.ID, key) cache.isValid = false return stackitem.NewBool(true) From c8bdd2ad1a8f9e5f4c8597c8440caaa15b6fe5e4 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Tue, 19 Apr 2022 12:39:34 +0300 Subject: [PATCH 10/24] core: remove Persist from NativeCache interface Lower native cache should be assigned to the upper's value during persist. --- pkg/core/native/designate.go | 13 ------------- pkg/core/native/management.go | 15 --------------- pkg/core/native/native_neo.go | 13 ------------- pkg/core/native/notary.go | 13 ------------- pkg/core/native/oracle.go | 13 ------------- pkg/core/native/policy.go | 14 -------------- pkg/core/storage/memcached_store.go | 23 +++-------------------- 7 files changed, 3 insertions(+), 101 deletions(-) diff --git a/pkg/core/native/designate.go b/pkg/core/native/designate.go index 33cda42c2..7700a4af9 100644 --- a/pkg/core/native/designate.go +++ b/pkg/core/native/designate.go @@ -87,19 +87,6 @@ func (c *DesignationCache) Copy() storage.NativeContractCache { return cp } -// Persist implements NativeContractCache interface. -func (c *DesignationCache) Persist(ps storage.NativeContractCache) (storage.NativeContractCache, error) { - if ps == nil { - ps = &DesignationCache{} - } - psCache, ok := ps.(*DesignationCache) - if !ok { - return nil, errors.New("not a Designation native cache") - } - copyDesignationCache(c, psCache) - return psCache, nil -} - func copyDesignationCache(src, dst *DesignationCache) { *dst = *src } diff --git a/pkg/core/native/management.go b/pkg/core/native/management.go index 00286955b..81b79d7bf 100644 --- a/pkg/core/native/management.go +++ b/pkg/core/native/management.go @@ -84,21 +84,6 @@ func (c *ManagementCache) Copy() storage.NativeContractCache { return cp } -// Persist implements NativeContractCache interface. -func (c *ManagementCache) Persist(ps storage.NativeContractCache) (storage.NativeContractCache, error) { - if ps == nil { - ps = &ManagementCache{} - } - psCache, ok := ps.(*ManagementCache) - if !ok { - return nil, errors.New("not a Management native cache") - } - psCache.contracts = c.contracts - psCache.nep17 = c.nep17 - psCache.nep11 = c.nep11 - return psCache, nil -} - // MakeContractKey creates a key from account script hash. func MakeContractKey(h util.Uint160) []byte { return makeUint160Key(prefixContract, h) diff --git a/pkg/core/native/native_neo.go b/pkg/core/native/native_neo.go index 85ec58fed..3f8128e3b 100644 --- a/pkg/core/native/native_neo.go +++ b/pkg/core/native/native_neo.go @@ -119,19 +119,6 @@ func (c *NeoCache) Copy() storage.NativeContractCache { return cp } -// Persist implements NativeContractCache interface. -func (c *NeoCache) Persist(ps storage.NativeContractCache) (storage.NativeContractCache, error) { - if ps == nil { - ps = &NeoCache{} - } - psCache, ok := ps.(*NeoCache) - if !ok { - return nil, errors.New("not a NEO native cache") - } - copyNeoCache(c, psCache) - return psCache, nil -} - func copyNeoCache(src, dst *NeoCache) { dst.votesChanged = src.votesChanged dst.nextValidators = src.nextValidators.Copy() diff --git a/pkg/core/native/notary.go b/pkg/core/native/notary.go index 325cedce6..c5dc4903e 100644 --- a/pkg/core/native/notary.go +++ b/pkg/core/native/notary.go @@ -68,19 +68,6 @@ func (c *NotaryCache) Copy() storage.NativeContractCache { return cp } -// Persist implements NativeContractCache interface. -func (c *NotaryCache) Persist(ps storage.NativeContractCache) (storage.NativeContractCache, error) { - if ps == nil { - ps = &NotaryCache{} - } - psCache, ok := ps.(*NotaryCache) - if !ok { - return nil, errors.New("not a Notary native cache") - } - copyNotaryCache(c, psCache) - return psCache, nil -} - func copyNotaryCache(src, dst *NotaryCache) { *dst = *src } diff --git a/pkg/core/native/oracle.go b/pkg/core/native/oracle.go index f1914cd65..16903397e 100644 --- a/pkg/core/native/oracle.go +++ b/pkg/core/native/oracle.go @@ -96,19 +96,6 @@ func (c *OracleCache) Copy() storage.NativeContractCache { return cp } -// Persist implements NativeContractCache interface. -func (c *OracleCache) Persist(ps storage.NativeContractCache) (storage.NativeContractCache, error) { - if ps == nil { - ps = &OracleCache{} - } - psCache, ok := ps.(*OracleCache) - if !ok { - return nil, errors.New("not an Oracle native cache") - } - copyOracleCache(c, psCache) - return psCache, nil -} - func copyOracleCache(src, dst *OracleCache) { *dst = *src } diff --git a/pkg/core/native/policy.go b/pkg/core/native/policy.go index 78b64d652..1a886ae67 100644 --- a/pkg/core/native/policy.go +++ b/pkg/core/native/policy.go @@ -1,7 +1,6 @@ package native import ( - "errors" "fmt" "math/big" "sort" @@ -79,19 +78,6 @@ func (c *PolicyCache) Copy() storage.NativeContractCache { return cp } -// Persist implements NativeContractCache interface. -func (c *PolicyCache) Persist(ps storage.NativeContractCache) (storage.NativeContractCache, error) { - if ps == nil { - ps = &PolicyCache{} - } - psCache, ok := ps.(*PolicyCache) - if !ok { - return nil, errors.New("not a Policy native cache") - } - copyPolicyCache(c, psCache) - return psCache, nil -} - func copyPolicyCache(src, dst *PolicyCache) { *dst = *src dst.blockedAccounts = make([]util.Uint160, len(src.blockedAccounts)) diff --git a/pkg/core/storage/memcached_store.go b/pkg/core/storage/memcached_store.go index fcb50a346..42fdccf69 100644 --- a/pkg/core/storage/memcached_store.go +++ b/pkg/core/storage/memcached_store.go @@ -3,7 +3,6 @@ package storage import ( "bytes" "context" - "fmt" "sort" "strings" "sync" @@ -33,9 +32,6 @@ type NativeContractCache interface { // Copy returns a copy of native cache item that can safely be changed within // the subsequent DAO operations. Copy() NativeContractCache - // Persist persists changes from upper native cache wrapper to the underlying - // native cache `ps`. The resulting up-to-date cache and an error are returned. - Persist(ps NativeContractCache) (NativeContractCache, error) } type ( @@ -369,11 +365,7 @@ func (s *MemCachedStore) persist(isSync bool) (int, error) { s.stor = nil if cached, ok := s.ps.(*MemCachedStore); ok { for id, nativeCache := range s.nativeCache { - updatedCache, err := nativeCache.Persist(cached.nativeCache[id]) - if err != nil { - return 0, fmt.Errorf("failed to persist native cache changes for private MemCachedStore: %w", err) - } - cached.nativeCache[id] = updatedCache + cached.nativeCache[id] = nativeCache } s.nativeCache = nil } @@ -411,12 +403,7 @@ func (s *MemCachedStore) persist(isSync bool) (int, error) { if isPSCached { cached.nativeCacheLock.Lock() for id, nativeCache := range tempstore.nativeCache { - updatedCache, err := nativeCache.Persist(cached.nativeCache[id]) - if err != nil { - cached.nativeCacheLock.Unlock() - return 0, fmt.Errorf("failed to persist native cache changes: %w", err) - } - cached.nativeCache[id] = updatedCache + cached.nativeCache[id] = nativeCache } cached.nativeCacheLock.Unlock() } @@ -442,11 +429,7 @@ func (s *MemCachedStore) persist(isSync bool) (int, error) { } if isPSCached { for id, nativeCache := range s.nativeCache { - updatedCache, err := nativeCache.Persist(tempstore.nativeCache[id]) - if err != nil { - return 0, fmt.Errorf("failed to persist native cache changes: %w", err) - } - tempstore.nativeCache[id] = updatedCache + tempstore.nativeCache[id] = nativeCache } s.nativeCache = tempstore.nativeCache } From 11ab42d91c04fa394a2399cee1a477c49c1f9a63 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Tue, 19 Apr 2022 14:35:19 +0300 Subject: [PATCH 11/24] core: keep Designation cache always valid and up-to-date Always use cache instead of DAO where possible. Update cache in-place each time new designated node is chosen. --- pkg/core/blockchain.go | 5 +- pkg/core/native/designate.go | 119 +++++++++++------- pkg/core/native/native_test/common_test.go | 32 ++++- pkg/core/native/native_test/designate_test.go | 92 ++++++++++++++ 4 files changed, 201 insertions(+), 47 deletions(-) diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index e53047c1d..f09c73562 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -594,7 +594,10 @@ func (bc *Blockchain) initializeNativeCache(d *dao.Simple) error { if err != nil { return fmt.Errorf("can't init cache for Management native contract: %w", err) } - bc.contracts.Designate.InitializeCache(d) + 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) diff --git a/pkg/core/native/designate.go b/pkg/core/native/designate.go index 7700a4af9..6282c3384 100644 --- a/pkg/core/native/designate.go +++ b/pkg/core/native/designate.go @@ -3,6 +3,7 @@ package native import ( "encoding/binary" "errors" + "fmt" "math" "math/big" "sort" @@ -48,6 +49,8 @@ type roleData struct { } 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 @@ -120,14 +123,33 @@ 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{} - cache.rolesChangedFlag = true ic.DAO.Store.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.Store.SetCache(s.ID, cache) + return nil +} + // OnPersist implements Contract interface. func (s *Designate) OnPersist(ic *interop.Context) error { return nil @@ -136,23 +158,15 @@ func (s *Designate) OnPersist(ic *interop.Context) error { // PostPersist implements Contract interface. func (s *Designate) PostPersist(ic *interop.Context) error { cache := ic.DAO.Store.GetRWCache(s.ID).(*DesignationCache) - if !rolesChanged(cache) { + if !cache.rolesChangedFlag { return nil } - if err := s.updateCachedRoleData(&cache.oracles, ic.DAO, noderoles.Oracle); err != nil { - return err - } - if err := s.updateCachedRoleData(&cache.stateVals, ic.DAO, noderoles.StateValidator); err != nil { - return err - } - if err := s.updateCachedRoleData(&cache.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(&cache.notaries, ic.DAO, noderoles.P2PNotary); err != nil { - return err - } + s.notifyRoleChanged(&cache.notaries, noderoles.P2PNotary) } cache.rolesChangedFlag = false @@ -184,10 +198,6 @@ func (s *Designate) getDesignatedByRole(ic *interop.Context, args []stackitem.It return pubsToArray(pubs) } -func rolesChanged(cache *DesignationCache) bool { - return cache.rolesChangedFlag -} - func (s *Designate) hashFromNodes(r noderoles.Role, nodes keys.PublicKeys) util.Uint160 { if len(nodes) == 0 { return util.Uint160{} @@ -202,29 +212,46 @@ func (s *Designate) hashFromNodes(r noderoles.Role, nodes keys.PublicKeys) util. return hash.Hash160(script) } -func (s *Designate) updateCachedRoleData(v *roleData, 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.nodes = nodeKeys v.addr = s.hashFromNodes(r, nodeKeys) v.height = height + cache.rolesChangedFlag = true + return nil +} + +func (s *Designate) notifyRoleChanged(v *roleData, r noderoles.Role) { switch r { case noderoles.Oracle: if orc, _ := s.OracleService.Load().(services.Oracle); orc != nil { - orc.UpdateOracleNodes(nodeKeys.Copy()) + orc.UpdateOracleNodes(v.nodes.Copy()) } case noderoles.P2PNotary: if ntr, _ := s.NotaryService.Load().(services.Notary); ntr != nil { - ntr.UpdateNotaryNodes(nodeKeys.Copy()) + ntr.UpdateNotaryNodes(v.nodes.Copy()) } case noderoles.StateValidator: if s.StateRootService != nil { - s.StateRootService.UpdateStateValidators(height, nodeKeys.Copy()) + s.StateRootService.UpdateStateValidators(v.height, v.nodes.Copy()) } } - return nil } func getCachedRoleData(cache *DesignationCache, r noderoles.Role) *roleData { @@ -247,17 +274,10 @@ func (s *Designate) GetLastDesignatedHash(d *dao.Simple, r noderoles.Role) (util return util.Uint160{}, ErrInvalidRole } cache := d.Store.GetROCache(s.ID).(*DesignationCache) - if !rolesChanged(cache) { - if val := getCachedRoleData(cache, r); val != nil { - return val.addr, nil - } + 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. @@ -266,11 +286,21 @@ func (s *Designate) GetDesignatedByRole(d *dao.Simple, r noderoles.Role, index u return nil, 0, ErrInvalidRole } cache := d.Store.GetROCache(s.ID).(*DesignationCache) - if !rolesChanged(cache) { - if val := getCachedRoleData(cache, r); val != nil && val.height <= index { + 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 @@ -344,12 +374,18 @@ func (s *Designate) DesignateAsRole(ic *interop.Context, r noderoles.Role, pubs } sort.Sort(pubs) nl := NodeList(pubs) - ic.DAO.Store.GetRWCache(s.ID).(*DesignationCache).rolesChangedFlag = true + err := putConvertibleToDAO(s.ID, ic.DAO, key, &nl) if err != nil { return err } + cache := ic.DAO.Store.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, @@ -372,10 +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(d *dao.Simple) { - cache := &DesignationCache{} - cache.rolesChangedFlag = true - d.Store.SetCache(s.ID, cache) -} diff --git a/pkg/core/native/native_test/common_test.go b/pkg/core/native/native_test/common_test.go index 851b18e16..4cdf975f5 100644 --- a/pkg/core/native/native_test/common_test.go +++ b/pkg/core/native/native_test/common_test.go @@ -8,9 +8,12 @@ import ( "github.com/nspcc-dev/neo-go/pkg/core/native/noderoles" "github.com/nspcc-dev/neo-go/pkg/core/state" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" - + "github.com/nspcc-dev/neo-go/pkg/io" "github.com/nspcc-dev/neo-go/pkg/neotest" "github.com/nspcc-dev/neo-go/pkg/neotest/chain" + "github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag" + "github.com/nspcc-dev/neo-go/pkg/vm/emit" + "github.com/nspcc-dev/neo-go/pkg/vm/opcode" "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" "github.com/stretchr/testify/require" ) @@ -77,6 +80,33 @@ 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()) + committeeInvoker.CheckHalt(t, tx2.Hash(), stackitem.Make(newVal)) +} + func setNodesByRole(t *testing.T, designateInvoker *neotest.ContractInvoker, ok bool, r noderoles.Role, nodes keys.PublicKeys) { pubs := make([]interface{}, len(nodes)) for i := range nodes { diff --git a/pkg/core/native/native_test/designate_test.go b/pkg/core/native/native_test/designate_test.go index bbfac3d84..98d3c362b 100644 --- a/pkg/core/native/native_test/designate_test.go +++ b/pkg/core/native/native_test/designate_test.go @@ -5,8 +5,15 @@ import ( "github.com/nspcc-dev/neo-go/pkg/core/native/nativenames" "github.com/nspcc-dev/neo-go/pkg/core/native/noderoles" + "github.com/nspcc-dev/neo-go/pkg/core/state" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + "github.com/nspcc-dev/neo-go/pkg/io" "github.com/nspcc-dev/neo-go/pkg/neotest" + "github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/vm/emit" + "github.com/nspcc-dev/neo-go/pkg/vm/opcode" + "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" "github.com/stretchr/testify/require" ) @@ -45,3 +52,88 @@ func TestDesignate_DesignateAsRole(t *testing.T) { checkNodeRoles(t, designateInvoker, true, noderoles.NeoFSAlphabet, e.Chain.BlockHeight()+1, pubs) }) } + +type dummyOracle struct { + updateNodes func(k keys.PublicKeys) +} + +// AddRequests processes new requests. +func (o *dummyOracle) AddRequests(map[uint64]*state.OracleRequest) { +} + +// RemoveRequests removes already processed requests. +func (o *dummyOracle) RemoveRequests([]uint64) { + panic("TODO") +} + +// UpdateOracleNodes updates oracle nodes. +func (o *dummyOracle) UpdateOracleNodes(k keys.PublicKeys) { + if o.updateNodes != nil { + o.updateNodes(k) + return + } + panic("TODO") +} + +// UpdateNativeContract updates oracle contract native script and hash. +func (o *dummyOracle) UpdateNativeContract([]byte, []byte, util.Uint160, int) { +} + +// Start runs oracle module. +func (o *dummyOracle) Start() { + panic("TODO") +} + +// Shutdown shutdowns oracle module. +func (o *dummyOracle) Shutdown() { + panic("TODO") +} + +func TestDesignate_Cache(t *testing.T) { + c := newDesignateClient(t) + e := c.Executor + designateInvoker := c.WithSigners(c.Committee) + r := int64(noderoles.Oracle) + var ( + updatedNodes keys.PublicKeys + updateCalled bool + ) + oracleServ := &dummyOracle{ + updateNodes: func(k keys.PublicKeys) { + updatedNodes = k + updateCalled = true + }, + } + privGood, err := keys.NewPrivateKey() + require.NoError(t, err) + pubsGood := []interface{}{privGood.PublicKey().Bytes()} + + privBad, err := keys.NewPrivateKey() + require.NoError(t, err) + pubsBad := []interface{}{privBad.PublicKey().Bytes()} + + // Firstly, designate good Oracle node and check that OracleService callback was called during PostPersist. + e.Chain.SetOracle(oracleServ) + txDesignateGood := designateInvoker.PrepareInvoke(t, "designateAsRole", r, pubsGood) + e.AddNewBlock(t, txDesignateGood) + e.CheckHalt(t, txDesignateGood.Hash(), stackitem.Null{}) + require.True(t, updateCalled) + require.Equal(t, keys.PublicKeys{privGood.PublicKey()}, updatedNodes) + updatedNodes = nil + updateCalled = false + + // Check designated node in a separate block. + checkNodeRoles(t, designateInvoker, true, noderoles.Oracle, e.Chain.BlockHeight()+1, keys.PublicKeys{privGood.PublicKey()}) + + // Designate privBad as oracle node and abort the transaction. Designation cache changes + // shouldn't be persisted to the contract and no notification should be sent. + w := io.NewBufBinWriter() + emit.AppCall(w.BinWriter, designateInvoker.Hash, "designateAsRole", callflag.All, int64(r), pubsBad) + emit.Opcodes(w.BinWriter, opcode.ABORT) + require.NoError(t, w.Err) + script := w.Bytes() + + designateInvoker.InvokeScriptCheckFAULT(t, script, designateInvoker.Signers, "ABORT") + require.Nil(t, updatedNodes) + require.False(t, updateCalled) +} From c0b490c7bf81b31aca6a5eda5e81cbe7faba8968 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Tue, 19 Apr 2022 17:12:03 +0300 Subject: [PATCH 12/24] core: keep Management cache always valid and up-to-date --- pkg/core/native/management.go | 56 +++++++------------ pkg/core/native/management_test.go | 36 +++++++++--- .../native/native_test/management_test.go | 39 +++++++++++++ 3 files changed, 85 insertions(+), 46 deletions(-) diff --git a/pkg/core/native/management.go b/pkg/core/native/management.go index 81b79d7bf..2d5cc7cfd 100644 --- a/pkg/core/native/management.go +++ b/pkg/core/native/management.go @@ -174,20 +174,8 @@ func (m *Management) GetContract(d *dao.Simple, hash util.Uint160) (*state.Contr 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) { @@ -283,10 +271,15 @@ func (m *Management) deployWithData(ic *interop.Context, args []stackitem.Item) return contractToStack(newcontract) } -func (m *Management) markUpdated(d *dao.Simple, h util.Uint160) { +func (m *Management) markUpdated(d *dao.Simple, hash util.Uint160, cs *state.Contract) { cache := d.Store.GetRWCache(m.ID).(*ManagementCache) - // Just set it to nil, to refresh cache in `PostPersist`. - cache.contracts[h] = nil + 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. @@ -322,7 +315,6 @@ func (m *Management) Deploy(d *dao.Simple, sender util.Uint160, neff *nef.File, if err != nil { return nil, err } - m.markUpdated(d, newcontract.Hash) return newcontract, nil } @@ -362,7 +354,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(d, hash) contract.NEF = *neff } // if manifest was provided, update the contract manifest @@ -374,7 +365,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(d, hash) contract.Manifest = *manif } err = checkScriptAndMethods(contract.NEF.Script, contract.Manifest.ABI.Methods) @@ -415,7 +405,7 @@ func (m *Management) Destroy(d *dao.Simple, hash util.Uint160) error { d.DeleteStorageItem(contract.ID, k) return true }) - m.markUpdated(d, hash) + m.markUpdated(d, hash, nil) return nil } @@ -492,7 +482,7 @@ func (m *Management) OnPersist(ic *interop.Context) error { if err := native.Initialize(ic); err != nil { return fmt.Errorf("initializing %s native contract: %w", md.Name, err) } - err := m.PutContractState(ic.DAO, cs) + err := m.putContractState(ic.DAO, cs, false) // Perform cache update manually. if err != nil { return err } @@ -534,21 +524,6 @@ func (m *Management) InitializeCache(d *dao.Simple) error { // PostPersist implements Contract interface. func (m *Management) PostPersist(ic *interop.Context) error { - cache := ic.DAO.Store.GetRWCache(m.ID).(*ManagementCache) - for h, cs := range cache.contracts { - if cs != nil { - continue - } - delete(cache.nep11, h) - delete(cache.nep17, h) - newCs, err := m.getContractFromDAO(ic.DAO, h) - if err != nil { - // Contract was destroyed. - delete(cache.contracts, h) - continue - } - updateContractCache(cache, newCs) - } return nil } @@ -592,11 +567,18 @@ func (m *Management) Initialize(ic *interop.Context) error { // 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(d, cs.Hash) + if updateCache { + m.markUpdated(d, cs.Hash, cs) + } if cs.UpdateCounter != 0 { // Update. return nil } diff --git a/pkg/core/native/management_test.go b/pkg/core/native/management_test.go index 696a97a81..bb669a961 100644 --- a/pkg/core/native/management_test.go +++ b/pkg/core/native/management_test.go @@ -93,6 +93,7 @@ func TestManagement_GetNEP17Contracts(t *testing.T) { require.NoError(t, err) require.Empty(t, mgmt.GetNEP17Contracts(d)) + private := d.GetPrivate() // Deploy NEP-17 contract script := []byte{byte(opcode.RET)} @@ -106,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 + // c1 contract hash should be returned, as private DAO already contains changed cache. + require.Equal(t, []util.Uint160{c1.Hash}, mgmt.GetNEP17Contracts(private)) + + // Lower DAO still shouldn't contain c1, as no Persist was called. require.Empty(t, mgmt.GetNEP17Contracts(d)) - // Call PostPersist, check c1 contract hash is returned - require.NoError(t, mgmt.PostPersist(&interop.Context{DAO: 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. + // 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(d)) + // Call Persist, check c1Updated state is returned from lower. + _, err = private.Persist() + require.NoError(t, err) + require.Equal(t, []util.Uint160{c1.Hash}, mgmt.GetNEP17Contracts(d)) + c1Lower, err = mgmt.GetContract(d, c1.Hash) + require.NoError(t, err) + require.Equal(t, 2, len(c1Lower.Manifest.ABI.Methods)) } diff --git a/pkg/core/native/native_test/management_test.go b/pkg/core/native/native_test/management_test.go index c34b1c8d8..6ff49178a 100644 --- a/pkg/core/native/native_test/management_test.go +++ b/pkg/core/native/native_test/management_test.go @@ -20,6 +20,8 @@ import ( "github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag" "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" "github.com/nspcc-dev/neo-go/pkg/smartcontract/nef" + "github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger" + "github.com/nspcc-dev/neo-go/pkg/vm" "github.com/nspcc-dev/neo-go/pkg/vm/emit" "github.com/nspcc-dev/neo-go/pkg/vm/opcode" "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" @@ -34,6 +36,43 @@ func TestManagement_MinimumDeploymentFee(t *testing.T) { testGetSet(t, newManagementClient(t), "MinimumDeploymentFee", 10_00000000, 0, 0) } +func TestManagement_MinimumDeploymentFeeCache(t *testing.T) { + c := newManagementClient(t) + testGetSetCache(t, c, "MinimumDeploymentFee", 10_00000000) +} + +func TestManagement_ContractCache(t *testing.T) { + c := newManagementClient(t) + managementInvoker := c.WithSigners(c.Committee) + + cs1, _ := contracts.GetTestContractState(t, pathToInternalContracts, 1, 2, c.Committee.ScriptHash()) + manifestBytes, err := json.Marshal(cs1.Manifest) + require.NoError(t, err) + nefBytes, err := cs1.NEF.Bytes() + require.NoError(t, err) + + // Deploy contract, abort the transaction and check that Management cache wasn't persisted + // for FAULTed tx at the same block. + w := io.NewBufBinWriter() + emit.AppCall(w.BinWriter, managementInvoker.Hash, "deploy", callflag.All, nefBytes, manifestBytes) + emit.Opcodes(w.BinWriter, opcode.ABORT) + tx1 := managementInvoker.PrepareInvocation(t, w.Bytes(), managementInvoker.Signers) + tx2 := managementInvoker.PrepareInvoke(t, "getContract", cs1.Hash.BytesBE()) + managementInvoker.AddNewBlock(t, tx1, tx2) + managementInvoker.CheckFault(t, tx1.Hash(), "ABORT") + managementInvoker.CheckHalt(t, tx2.Hash(), stackitem.Null{}) + + // Deploy the contract and check that cache was persisted for HALTed transaction at the same block. + tx1 = managementInvoker.PrepareInvoke(t, "deploy", nefBytes, manifestBytes) + tx2 = managementInvoker.PrepareInvoke(t, "getContract", cs1.Hash.BytesBE()) + managementInvoker.AddNewBlock(t, tx1, tx2) + managementInvoker.CheckHalt(t, tx1.Hash()) + aer, err := managementInvoker.Chain.GetAppExecResults(tx2.Hash(), trigger.Application) + require.NoError(t, err) + require.Equal(t, vm.HaltState, aer[0].VMState, aer[0].FaultException) + require.NotEqual(t, stackitem.Null{}, aer[0].Stack) +} + func TestManagement_ContractDeploy(t *testing.T) { c := newManagementClient(t) managementInvoker := c.WithSigners(c.Committee) From 27b0193da0dbe738ffc0e25deb71c233b31378f0 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Wed, 20 Apr 2022 18:34:56 +0300 Subject: [PATCH 13/24] core: use native cache to check whether the same contract exists on deploy --- pkg/core/native/management.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pkg/core/native/management.go b/pkg/core/native/management.go index 2d5cc7cfd..2643c4f03 100644 --- a/pkg/core/native/management.go +++ b/pkg/core/native/management.go @@ -286,9 +286,8 @@ func (m *Management) markUpdated(d *dao.Simple, hash util.Uint160, cs *state.Con // 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) From 0f6bf33f86d82607a4ebf0ef104c5a9c306d0369 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Tue, 19 Apr 2022 18:32:34 +0300 Subject: [PATCH 14/24] core: keep Notary cache always valid and up-to-date --- pkg/core/native/native_test/notary_test.go | 10 ++++++ pkg/core/native/notary.go | 39 ++++++---------------- 2 files changed, 20 insertions(+), 29 deletions(-) diff --git a/pkg/core/native/native_test/notary_test.go b/pkg/core/native/native_test/notary_test.go index fd948294e..a827d0ed3 100644 --- a/pkg/core/native/native_test/notary_test.go +++ b/pkg/core/native/native_test/notary_test.go @@ -32,11 +32,21 @@ func TestNotary_MaxNotValidBeforeDelta(t *testing.T) { testGetSet(t, c, "MaxNotValidBeforeDelta", 140, int64(c.Chain.GetConfig().ValidatorsCount), int64(c.Chain.GetConfig().MaxValidUntilBlockIncrement/2)) } +func TestNotary_MaxNotValidBeforeDeltaCache(t *testing.T) { + c := newNotaryClient(t) + testGetSetCache(t, c, "MaxNotValidBeforeDelta", 140) +} + func TestNotary_NotaryServiceFeePerKey(t *testing.T) { c := newNotaryClient(t) testGetSet(t, c, "NotaryServiceFeePerKey", 1000_0000, 0, 0) } +func TestNotary_NotaryServiceFeePerKeyCache(t *testing.T) { + c := newNotaryClient(t) + testGetSetCache(t, c, "NotaryServiceFeePerKey", 1000_0000) +} + func TestNotary_Pipeline(t *testing.T) { notaryCommitteeInvoker := newNotaryClient(t) e := notaryCommitteeInvoker.Executor diff --git a/pkg/core/native/notary.go b/pkg/core/native/notary.go index c5dc4903e..79cd648c7 100644 --- a/pkg/core/native/notary.go +++ b/pkg/core/native/notary.go @@ -34,10 +34,6 @@ type Notary struct { } type NotaryCache struct { - // isValid defies whether cached values were changed during the current - // consensus iteration. If false, these values will be updated after - // blockchain DAO persisting. If true, we can safely use cached values. - isValid bool maxNotValidBeforeDelta uint32 notaryServiceFeePerKey int64 } @@ -143,7 +139,6 @@ func (n *Notary) Initialize(ic *interop.Context) error { setIntWithKey(n.ID, ic.DAO, notaryServiceFeeKey, defaultNotaryServiceFeePerKey) cache := &NotaryCache{ - isValid: true, maxNotValidBeforeDelta: defaultMaxNotValidBeforeDelta, notaryServiceFeePerKey: defaultNotaryServiceFeePerKey, } @@ -152,10 +147,10 @@ func (n *Notary) Initialize(ic *interop.Context) error { } func (n *Notary) InitializeCache(d *dao.Simple) error { - cache := &NotaryCache{isValid: true} - - cache.maxNotValidBeforeDelta = uint32(getIntWithKey(n.ID, d, maxNotValidBeforeDeltaKey)) - cache.notaryServiceFeePerKey = getIntWithKey(n.ID, d, notaryServiceFeeKey) + cache := &NotaryCache{ + maxNotValidBeforeDelta: uint32(getIntWithKey(n.ID, d, maxNotValidBeforeDeltaKey)), + notaryServiceFeePerKey: getIntWithKey(n.ID, d, notaryServiceFeeKey), + } d.Store.SetCache(n.ID, cache) return nil @@ -206,14 +201,6 @@ func (n *Notary) OnPersist(ic *interop.Context) error { // PostPersist implements Contract interface. func (n *Notary) PostPersist(ic *interop.Context) error { - cache := ic.DAO.Store.GetRWCache(n.ID).(*NotaryCache) - if cache.isValid { - return nil - } - - cache.maxNotValidBeforeDelta = uint32(getIntWithKey(n.ID, ic.DAO, maxNotValidBeforeDeltaKey)) - cache.notaryServiceFeePerKey = getIntWithKey(n.ID, ic.DAO, notaryServiceFeeKey) - cache.isValid = true return nil } @@ -421,10 +408,7 @@ 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 { cache := dao.Store.GetROCache(n.ID).(*NotaryCache) - if cache.isValid { - return cache.maxNotValidBeforeDelta - } - return uint32(getIntWithKey(n.ID, dao, maxNotValidBeforeDeltaKey)) + return cache.maxNotValidBeforeDelta } // setMaxNotValidBeforeDelta is Notary contract method and sets the maximum NotValidBefore delta. @@ -438,9 +422,9 @@ func (n *Notary) setMaxNotValidBeforeDelta(ic *interop.Context, args []stackitem if !n.NEO.checkCommittee(ic) { panic("invalid committee signature") } - cache := ic.DAO.Store.GetRWCache(n.ID).(*NotaryCache) setIntWithKey(n.ID, ic.DAO, maxNotValidBeforeDeltaKey, int64(value)) - cache.isValid = false + cache := ic.DAO.Store.GetRWCache(n.ID).(*NotaryCache) + cache.maxNotValidBeforeDelta = value return stackitem.Null{} } @@ -452,10 +436,7 @@ 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 { cache := dao.Store.GetROCache(n.ID).(*NotaryCache) - if cache.isValid { - return cache.notaryServiceFeePerKey - } - return getIntWithKey(n.ID, dao, notaryServiceFeeKey) + return cache.notaryServiceFeePerKey } // setNotaryServiceFeePerKey is Notary contract method and sets a reward per notary request key for notary nodes. @@ -467,9 +448,9 @@ func (n *Notary) setNotaryServiceFeePerKey(ic *interop.Context, args []stackitem if !n.NEO.checkCommittee(ic) { panic("invalid committee signature") } - cache := ic.DAO.Store.GetRWCache(n.ID).(*NotaryCache) setIntWithKey(n.ID, ic.DAO, notaryServiceFeeKey, int64(value)) - cache.isValid = false + cache := ic.DAO.Store.GetRWCache(n.ID).(*NotaryCache) + cache.notaryServiceFeePerKey = value return stackitem.Null{} } From 78b584053d7da0f82a42f1810de85a6cbf67c31e Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Tue, 19 Apr 2022 18:36:40 +0300 Subject: [PATCH 15/24] core: keep Oracle cache always valid and up-to-date --- pkg/core/native/native_test/oracle_test.go | 6 +++++- pkg/core/native/oracle.go | 22 ++++++---------------- 2 files changed, 11 insertions(+), 17 deletions(-) diff --git a/pkg/core/native/native_test/oracle_test.go b/pkg/core/native/native_test/oracle_test.go index 77247f80b..1542d6813 100644 --- a/pkg/core/native/native_test/oracle_test.go +++ b/pkg/core/native/native_test/oracle_test.go @@ -28,10 +28,14 @@ func newOracleClient(t *testing.T) *neotest.ContractInvoker { return newNativeClient(t, nativenames.Oracle) } -func TestGetSetPrice(t *testing.T) { +func TestOracle_GetSetPrice(t *testing.T) { testGetSet(t, newOracleClient(t), "Price", native.DefaultOracleRequestPrice, 1, math.MaxInt64) } +func TestOracle_GetSetPriceCache(t *testing.T) { + testGetSetCache(t, newOracleClient(t), "Price", native.DefaultOracleRequestPrice) +} + func putOracleRequest(t *testing.T, oracleInvoker *neotest.ContractInvoker, url string, filter *string, cb string, userData []byte, gas int64, errStr ...string) { var filtItem interface{} diff --git a/pkg/core/native/oracle.go b/pkg/core/native/oracle.go index 16903397e..25273f60f 100644 --- a/pkg/core/native/oracle.go +++ b/pkg/core/native/oracle.go @@ -47,8 +47,7 @@ type Oracle struct { } type OracleCache struct { - requestPrice int64 - requestPriceChanged bool + requestPrice int64 } const ( @@ -159,11 +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) - cache := ic.DAO.Store.GetRWCache(o.ID).(*OracleCache) - if cache.requestPriceChanged { - cache.requestPrice = p - cache.requestPriceChanged = false - } var nodes keys.PublicKeys var reward []big.Int @@ -238,9 +232,9 @@ func (o *Oracle) Initialize(ic *interop.Context) error { setIntWithKey(o.ID, ic.DAO, prefixRequestID, 0) setIntWithKey(o.ID, ic.DAO, prefixRequestPrice, DefaultOracleRequestPrice) - cache := &OracleCache{} - cache.requestPrice = int64(DefaultOracleRequestPrice) - cache.requestPriceChanged = false + cache := &OracleCache{ + requestPrice: int64(DefaultOracleRequestPrice), + } ic.DAO.Store.SetCache(o.ID, cache) return nil } @@ -248,7 +242,6 @@ func (o *Oracle) Initialize(ic *interop.Context) error { func (o *Oracle) InitializeCache(d *dao.Simple) { cache := &OracleCache{} cache.requestPrice = getIntWithKey(o.ID, d, prefixRequestPrice) - cache.requestPriceChanged = false d.Store.SetCache(o.ID, cache) } @@ -467,10 +460,7 @@ func (o *Oracle) getPrice(ic *interop.Context, _ []stackitem.Item) stackitem.Ite func (o *Oracle) getPriceInternal(d *dao.Simple) int64 { cache := d.Store.GetROCache(o.ID).(*OracleCache) - if !cache.requestPriceChanged { - return cache.requestPrice - } - return getIntWithKey(o.ID, d, prefixRequestPrice) + return cache.requestPrice } func (o *Oracle) setPrice(ic *interop.Context, args []stackitem.Item) stackitem.Item { @@ -483,7 +473,7 @@ func (o *Oracle) setPrice(ic *interop.Context, args []stackitem.Item) stackitem. } setIntWithKey(o.ID, ic.DAO, prefixRequestPrice, price.Int64()) cache := ic.DAO.Store.GetRWCache(o.ID).(*OracleCache) - cache.requestPriceChanged = true + cache.requestPrice = price.Int64() return stackitem.Null{} } From 35d160075d02d290623703c28f97b016cad1de92 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Tue, 19 Apr 2022 18:57:46 +0300 Subject: [PATCH 16/24] core: keep Policy cache always valid and up-to-date --- pkg/core/interop/contract/call.go | 4 +- pkg/core/native/native_neo.go | 2 +- pkg/core/native/native_test/policy_test.go | 12 +++ pkg/core/native/policy.go | 112 ++++++++++----------- 4 files changed, 66 insertions(+), 64 deletions(-) diff --git a/pkg/core/interop/contract/call.go b/pkg/core/interop/contract/call.go index 5c3b6eb0a..2afab663d 100644 --- a/pkg/core/interop/contract/call.go +++ b/pkg/core/interop/contract/call.go @@ -18,7 +18,7 @@ import ( ) type policyChecker interface { - IsBlockedInternal(*dao.Simple, util.Uint160) bool + IsBlocked(*dao.Simple, util.Uint160) bool } // LoadToken calls method specified by token id. @@ -97,7 +97,7 @@ func callExFromNative(ic *interop.Context, caller util.Uint160, cs *state.Contra for _, nc := range ic.Natives { if nc.Metadata().Name == nativenames.Policy { var pch = nc.(policyChecker) - if pch.IsBlockedInternal(ic.DAO, cs.Hash) { + if pch.IsBlocked(ic.DAO, cs.Hash) { return fmt.Errorf("contract %s is blocked", cs.Hash.StringLE()) } break diff --git a/pkg/core/native/native_neo.go b/pkg/core/native/native_neo.go index 3f8128e3b..e3d4274cf 100644 --- a/pkg/core/native/native_neo.go +++ b/pkg/core/native/native_neo.go @@ -877,7 +877,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() diff --git a/pkg/core/native/native_test/policy_test.go b/pkg/core/native/native_test/policy_test.go index 9dfc1a5eb..3d4a2c469 100644 --- a/pkg/core/native/native_test/policy_test.go +++ b/pkg/core/native/native_test/policy_test.go @@ -19,14 +19,26 @@ func TestPolicy_FeePerByte(t *testing.T) { testGetSet(t, newPolicyClient(t), "FeePerByte", 1000, 0, 100_000_000) } +func TestPolicy_FeePerByteCache(t *testing.T) { + testGetSetCache(t, newPolicyClient(t), "FeePerByte", 1000) +} + func TestPolicy_ExecFeeFactor(t *testing.T) { testGetSet(t, newPolicyClient(t), "ExecFeeFactor", interop.DefaultBaseExecFee, 1, 1000) } +func TestPolicy_ExecFeeFactorCache(t *testing.T) { + testGetSetCache(t, newPolicyClient(t), "ExecFeeFactor", interop.DefaultBaseExecFee) +} + func TestPolicy_StoragePrice(t *testing.T) { testGetSet(t, newPolicyClient(t), "StoragePrice", native.DefaultStoragePrice, 1, 10000000) } +func TestPolicy_StoragePriceCache(t *testing.T) { + testGetSetCache(t, newPolicyClient(t), "StoragePrice", native.DefaultStoragePrice) +} + func TestPolicy_BlockedAccounts(t *testing.T) { c := newPolicyClient(t) e := c.Executor diff --git a/pkg/core/native/policy.go b/pkg/core/native/policy.go index 1a886ae67..b4a7895ab 100644 --- a/pkg/core/native/policy.go +++ b/pkg/core/native/policy.go @@ -55,10 +55,6 @@ type Policy struct { } type PolicyCache struct { - // isValid defies whether cached values were changed during the current - // consensus iteration. If false, these values will be updated after - // blockchain DAO persisting. If true, we can safely use cached values. - isValid bool execFeeFactor uint32 feePerByte int64 maxVerificationGas int64 @@ -145,13 +141,13 @@ func (p *Policy) Initialize(ic *interop.Context) error { setIntWithKey(p.ID, ic.DAO, execFeeFactorKey, defaultExecFeeFactor) setIntWithKey(p.ID, ic.DAO, storagePriceKey, DefaultStoragePrice) - cache := &PolicyCache{} - cache.isValid = true - cache.execFeeFactor = defaultExecFeeFactor - cache.feePerByte = defaultFeePerByte - cache.maxVerificationGas = defaultMaxVerificationGas - cache.storagePrice = DefaultStoragePrice - cache.blockedAccounts = make([]util.Uint160, 0) + cache := &PolicyCache{ + execFeeFactor: defaultExecFeeFactor, + feePerByte: defaultFeePerByte, + maxVerificationGas: defaultMaxVerificationGas, + storagePrice: DefaultStoragePrice, + blockedAccounts: make([]util.Uint160, 0), + } ic.DAO.Store.SetCache(p.ID, cache) return nil @@ -187,7 +183,6 @@ func (p *Policy) fillCacheFromDAO(cache *PolicyCache, d *dao.Simple) error { if fErr != nil { return fmt.Errorf("failed to initialize blocked accounts: %w", fErr) } - cache.isValid = true return nil } @@ -198,12 +193,7 @@ func (p *Policy) OnPersist(ic *interop.Context) error { // PostPersist implements Contract interface. func (p *Policy) PostPersist(ic *interop.Context) error { - cache := ic.DAO.Store.GetRWCache(p.ID).(*PolicyCache) - if cache.isValid { - return nil - } - - return p.fillCacheFromDAO(cache, ic.DAO) + return nil } // getFeePerByte is Policy contract method and returns required transaction's fee @@ -215,19 +205,13 @@ 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 { cache := dao.Store.GetROCache(p.ID).(*PolicyCache) - if cache.isValid { - return cache.feePerByte - } - return getIntWithKey(p.ID, dao, feePerByteKey) + return cache.feePerByte } // GetMaxVerificationGas returns maximum gas allowed to be burned during verificaion. func (p *Policy) GetMaxVerificationGas(dao *dao.Simple) int64 { cache := dao.Store.GetROCache(p.ID).(*PolicyCache) - if cache.isValid { - return cache.maxVerificationGas - } - return defaultMaxVerificationGas + return cache.maxVerificationGas } func (p *Policy) getExecFeeFactor(ic *interop.Context, _ []stackitem.Item) stackitem.Item { @@ -237,10 +221,7 @@ func (p *Policy) getExecFeeFactor(ic *interop.Context, _ []stackitem.Item) stack // GetExecFeeFactorInternal returns current execution fee factor. func (p *Policy) GetExecFeeFactorInternal(d *dao.Simple) int64 { cache := d.Store.GetROCache(p.ID).(*PolicyCache) - if cache.isValid { - return int64(cache.execFeeFactor) - } - return getIntWithKey(p.ID, d, execFeeFactorKey) + return int64(cache.execFeeFactor) } func (p *Policy) setExecFeeFactor(ic *interop.Context, args []stackitem.Item) stackitem.Item { @@ -251,33 +232,38 @@ func (p *Policy) setExecFeeFactor(ic *interop.Context, args []stackitem.Item) st if !p.NEO.checkCommittee(ic) { panic("invalid committee signature") } - cache := ic.DAO.Store.GetRWCache(p.ID).(*PolicyCache) setIntWithKey(p.ID, ic.DAO, execFeeFactorKey, int64(value)) - cache.isValid = false + cache := ic.DAO.Store.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 { +// 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.Store.GetROCache(p.ID).(*PolicyCache) - if cache.isValid { - 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 true - } - return false + 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 { @@ -287,10 +273,7 @@ func (p *Policy) getStoragePrice(ic *interop.Context, _ []stackitem.Item) stacki // GetStoragePriceInternal returns current execution fee factor. func (p *Policy) GetStoragePriceInternal(d *dao.Simple) int64 { cache := d.Store.GetROCache(p.ID).(*PolicyCache) - if cache.isValid { - return int64(cache.storagePrice) - } - return getIntWithKey(p.ID, d, storagePriceKey) + return int64(cache.storagePrice) } func (p *Policy) setStoragePrice(ic *interop.Context, args []stackitem.Item) stackitem.Item { @@ -301,9 +284,9 @@ func (p *Policy) setStoragePrice(ic *interop.Context, args []stackitem.Item) sta if !p.NEO.checkCommittee(ic) { panic("invalid committee signature") } - cache := ic.DAO.Store.GetRWCache(p.ID).(*PolicyCache) setIntWithKey(p.ID, ic.DAO, storagePriceKey, int64(value)) - cache.isValid = false + cache := ic.DAO.Store.GetRWCache(p.ID).(*PolicyCache) + cache.storagePrice = value return stackitem.Null{} } @@ -316,9 +299,9 @@ func (p *Policy) setFeePerByte(ic *interop.Context, args []stackitem.Item) stack if !p.NEO.checkCommittee(ic) { panic("invalid committee signature") } - cache := ic.DAO.Store.GetRWCache(p.ID).(*PolicyCache) setIntWithKey(p.ID, ic.DAO, feePerByteKey, value) - cache.isValid = false + cache := ic.DAO.Store.GetRWCache(p.ID).(*PolicyCache) + cache.feePerByte = value return stackitem.Null{} } @@ -334,13 +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()...) - cache := ic.DAO.Store.GetRWCache(p.ID).(*PolicyCache) ic.DAO.PutStorageItem(p.ID, key, state.StorageItem{}) - cache.isValid = false + cache := ic.DAO.Store.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) } @@ -351,13 +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()...) - cache := ic.DAO.Store.GetRWCache(p.ID).(*PolicyCache) ic.DAO.DeleteStorageItem(p.ID, key) - cache.isValid = false + cache := ic.DAO.Store.GetRWCache(p.ID).(*PolicyCache) + cache.blockedAccounts = append(cache.blockedAccounts[:i], cache.blockedAccounts[i+1:]...) return stackitem.NewBool(true) } @@ -366,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()) } } From adec635f0e755aca6ac5d89f1b4597afb1d8cdd4 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Tue, 19 Apr 2022 19:40:27 +0300 Subject: [PATCH 17/24] core: don't reset NEO's gasPerBlock cache --- pkg/core/native/native_neo.go | 39 ++++++++-------------- pkg/core/native/native_test/common_test.go | 7 +++- pkg/core/native/native_test/neo_test.go | 4 +++ 3 files changed, 23 insertions(+), 27 deletions(-) diff --git a/pkg/core/native/native_neo.go b/pkg/core/native/native_neo.go index e3d4274cf..c5abe230c 100644 --- a/pkg/core/native/native_neo.go +++ b/pkg/core/native/native_neo.go @@ -41,10 +41,8 @@ type NEO struct { } type NeoCache struct { - // gasPerBlock represents current value of generated gas per block. - // It is append-only and doesn't need to be copied when used. - gasPerBlock gasRecord - gasPerBlockChanged bool + // gasPerBlock represents the history of generated gas per block. + gasPerBlock gasRecord registerPrice int64 registerPriceChanged bool @@ -131,7 +129,9 @@ func copyNeoCache(src, dst *NeoCache) { dst.registerPriceChanged = src.registerPriceChanged dst.registerPrice = src.registerPrice - dst.gasPerBlockChanged = src.gasPerBlockChanged + // 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) @@ -271,7 +271,6 @@ func (n *NEO) Initialize(ic *interop.Context) error { gr := &gasRecord{{Index: index, GASPerBlock: *value}} cache.gasPerBlock = *gr - cache.gasPerBlockChanged = false ic.DAO.PutStorageItem(n.ID, []byte{prefixVotersCount}, state.StorageItem{}) setIntWithKey(n.ID, ic.DAO, []byte{prefixRegisterPrice}, DefaultRegisterPrice) @@ -301,7 +300,6 @@ func (n *NEO) InitializeCache(bc interop.Ledger, d *dao.Simple) error { } cache.gasPerBlock = n.getSortedGASRecordFromDAO(d) - cache.gasPerBlockChanged = false d.Store.SetCache(n.ID, cache) return nil @@ -417,10 +415,6 @@ func (n *NEO) PostPersist(ic *interop.Context) error { } } } - if cache.gasPerBlockChanged { - cache.gasPerBlock = n.getSortedGASRecordFromDAO(ic.DAO) - cache.gasPerBlockChanged = false - } if cache.registerPriceChanged { p := getIntWithKey(n.ID, ic.DAO, []byte{prefixRegisterPrice}) @@ -549,19 +543,14 @@ 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 { cache := d.Store.GetROCache(n.ID).(*NeoCache) - var gr gasRecord - if cache.gasPerBlockChanged { - gr = n.getSortedGASRecordFromDAO(d) - } else { - gr = cache.gasPerBlock - } + 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. @@ -595,9 +584,12 @@ func (n *NEO) SetGASPerBlock(ic *interop.Context, index uint32, gas *big.Int) er if !n.checkCommittee(ic) { return errors.New("invalid committee signature") } - cache := ic.DAO.Store.GetRWCache(n.ID).(*NeoCache) - cache.gasPerBlockChanged = true n.putGASRecord(ic.DAO, index, gas) + cache := ic.DAO.Store.GetRWCache(n.ID).(*NeoCache) + cache.gasPerBlock = append(cache.gasPerBlock, gasIndexPair{ + Index: index, + GASPerBlock: *gas, + }) return nil } @@ -693,13 +685,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 cache := d.Store.GetROCache(n.ID).(*NeoCache) - if !cache.gasPerBlockChanged { - gr = cache.gasPerBlock - } else { - gr = n.getSortedGASRecordFromDAO(d) - } + gr := cache.gasPerBlock var sum, tmp big.Int for i := len(gr) - 1; i >= 0; i-- { if gr[i].Index >= end { diff --git a/pkg/core/native/native_test/common_test.go b/pkg/core/native/native_test/common_test.go index 4cdf975f5..53a927a52 100644 --- a/pkg/core/native/native_test/common_test.go +++ b/pkg/core/native/native_test/common_test.go @@ -104,7 +104,12 @@ func testGetSetCache(t *testing.T, c *neotest.ContractInvoker, name string, defa tx2 = committeeInvoker.PrepareInvoke(t, getName) committeeInvoker.AddNewBlock(t, tx1, tx2) committeeInvoker.CheckHalt(t, tx1.Hash()) - committeeInvoker.CheckHalt(t, tx2.Hash(), stackitem.Make(newVal)) + 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) { diff --git a/pkg/core/native/native_test/neo_test.go b/pkg/core/native/native_test/neo_test.go index 3ade90aa4..61118ff1d 100644 --- a/pkg/core/native/native_test/neo_test.go +++ b/pkg/core/native/native_test/neo_test.go @@ -42,6 +42,10 @@ 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) } From c36448f27e08a159515b1cbea3a8db4676f7cf0e Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Tue, 19 Apr 2022 19:46:50 +0300 Subject: [PATCH 18/24] core: don't reset NEO's registerPrice cache --- pkg/core/native/native_neo.go | 29 +++++++------------------ pkg/core/native/native_test/neo_test.go | 4 ++++ 2 files changed, 12 insertions(+), 21 deletions(-) diff --git a/pkg/core/native/native_neo.go b/pkg/core/native/native_neo.go index c5abe230c..29d7d5c04 100644 --- a/pkg/core/native/native_neo.go +++ b/pkg/core/native/native_neo.go @@ -44,8 +44,7 @@ type NeoCache struct { // gasPerBlock represents the history of generated gas per block. gasPerBlock gasRecord - registerPrice int64 - registerPriceChanged bool + registerPrice int64 votesChanged bool nextValidators keys.PublicKeys @@ -126,7 +125,6 @@ func copyNeoCache(src, dst *NeoCache) { copy(dst.committee, src.committee) dst.committeeHash = src.committeeHash - dst.registerPriceChanged = src.registerPriceChanged dst.registerPrice = src.registerPrice // Can't omit copying because gasPerBlock is append-only, thus to be able to @@ -242,9 +240,8 @@ func (n *NEO) Initialize(ic *interop.Context) error { } cache := &NeoCache{ - gasPerVoteCache: make(map[string]big.Int), - votesChanged: true, - registerPriceChanged: true, + gasPerVoteCache: make(map[string]big.Int), + votesChanged: true, } // We need cache to be present in DAO before the subsequent call to `mint`. @@ -275,7 +272,6 @@ func (n *NEO) Initialize(ic *interop.Context) error { setIntWithKey(n.ID, ic.DAO, []byte{prefixRegisterPrice}, DefaultRegisterPrice) cache.registerPrice = int64(DefaultRegisterPrice) - cache.registerPriceChanged = false return nil } @@ -285,9 +281,8 @@ func (n *NEO) Initialize(ic *interop.Context) error { // called only when deploying native contracts. func (n *NEO) InitializeCache(bc interop.Ledger, d *dao.Simple) error { cache := &NeoCache{ - gasPerVoteCache: make(map[string]big.Int), - votesChanged: true, - registerPriceChanged: true, + gasPerVoteCache: make(map[string]big.Int), + votesChanged: true, } var committee = keysWithVotes{} @@ -300,6 +295,7 @@ func (n *NEO) InitializeCache(bc interop.Ledger, d *dao.Simple) error { } cache.gasPerBlock = n.getSortedGASRecordFromDAO(d) + cache.registerPrice = getIntWithKey(n.ID, d, []byte{prefixRegisterPrice}) d.Store.SetCache(n.ID, cache) return nil @@ -415,12 +411,6 @@ func (n *NEO) PostPersist(ic *interop.Context) error { } } } - - if cache.registerPriceChanged { - p := getIntWithKey(n.ID, ic.DAO, []byte{prefixRegisterPrice}) - cache.registerPrice = p - cache.registerPriceChanged = false - } return nil } @@ -599,10 +589,7 @@ func (n *NEO) getRegisterPrice(ic *interop.Context, _ []stackitem.Item) stackite func (n *NEO) getRegisterPriceInternal(d *dao.Simple) int64 { cache := d.Store.GetROCache(n.ID).(*NeoCache) - if !cache.registerPriceChanged { - return cache.registerPrice - } - return getIntWithKey(n.ID, d, []byte{prefixRegisterPrice}) + return cache.registerPrice } func (n *NEO) setRegisterPrice(ic *interop.Context, args []stackitem.Item) stackitem.Item { @@ -616,7 +603,7 @@ func (n *NEO) setRegisterPrice(ic *interop.Context, args []stackitem.Item) stack setIntWithKey(n.ID, ic.DAO, []byte{prefixRegisterPrice}, price.Int64()) cache := ic.DAO.Store.GetRWCache(n.ID).(*NeoCache) - cache.registerPriceChanged = true + cache.registerPrice = price.Int64() return stackitem.Null{} } diff --git a/pkg/core/native/native_test/neo_test.go b/pkg/core/native/native_test/neo_test.go index 61118ff1d..9314efd45 100644 --- a/pkg/core/native/native_test/neo_test.go +++ b/pkg/core/native/native_test/neo_test.go @@ -50,6 +50,10 @@ 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) From b77b412b04a9a92f8869951d37e19df9872e29b8 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Wed, 20 Apr 2022 12:13:23 +0300 Subject: [PATCH 19/24] core: refactor signature of (*NEO).dropCandidateIfZero It never returns an error. --- pkg/core/native/native_neo.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pkg/core/native/native_neo.go b/pkg/core/native/native_neo.go index 29d7d5c04..b9dedc49f 100644 --- a/pkg/core/native/native_neo.go +++ b/pkg/core/native/native_neo.go @@ -607,9 +607,9 @@ func (n *NEO) setRegisterPrice(ic *interop.Context, args []stackitem.Item) stack return stackitem.Null{} } -func (n *NEO) dropCandidateIfZero(d *dao.Simple, cache *NeoCache, 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)) @@ -620,7 +620,7 @@ func (n *NEO) dropCandidateIfZero(d *dao.Simple, cache *NeoCache, pub *keys.Publ }) delete(cache.gasPerVoteCache, string(voterKey)) - return true, nil + return true } func makeVoterKey(pub []byte, prealloc ...[]byte) []byte { @@ -747,9 +747,9 @@ func (n *NEO) UnregisterCandidateInternal(ic *interop.Context, pub *keys.PublicK cache.validators = nil c := new(candidate).FromBytes(si) c.Registered = false - ok, err := n.dropCandidateIfZero(ic.DAO, cache, pub, c) + ok := n.dropCandidateIfZero(ic.DAO, cache, pub, c) if ok { - return err + return nil } return putConvertibleToDAO(n.ID, ic.DAO, key, c) } @@ -834,9 +834,9 @@ 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, cache, acc.VoteTo, cd) + ok := n.dropCandidateIfZero(d, cache, acc.VoteTo, cd) if ok { - return err + return nil } } cache.validators = nil From 8d2d48f36090ad29350b898acf4bef1f2653b81a Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Wed, 20 Apr 2022 17:47:48 +0300 Subject: [PATCH 20/24] core: move native cache from MemCachedStore to DAO --- pkg/core/dao/dao.go | 112 ++++++++++++++++++++++++++- pkg/core/interop_system_core_test.go | 3 +- pkg/core/native/designate.go | 18 ++--- pkg/core/native/management.go | 20 ++--- pkg/core/native/native_neo.go | 36 ++++----- pkg/core/native/notary.go | 18 ++--- pkg/core/native/oracle.go | 14 ++-- pkg/core/native/policy.go | 30 +++---- pkg/core/storage/memcached_store.go | 100 +----------------------- 9 files changed, 181 insertions(+), 170 deletions(-) diff --git a/pkg/core/dao/dao.go b/pkg/core/dao/dao.go index b178d2aaf..db9bc0807 100644 --- a/pkg/core/dao/dao.go +++ b/pkg/core/dao/dao.go @@ -7,6 +7,7 @@ import ( "errors" "fmt" iocore "io" + "sync" "github.com/nspcc-dev/neo-go/pkg/core/block" "github.com/nspcc-dev/neo-go/pkg/core/state" @@ -34,11 +35,28 @@ var ( type Simple struct { Version Version Store *storage.MemCachedStore + + nativeCacheLock sync.RWMutex + nativeCache map[int32]NativeContractCache + // nativeCachePS is the backend store that provides functionality to store + // and retrieve multi-tier native contract cache. The lowest Simple has its + // nativeCachePS set to nil. + nativeCachePS *Simple + private bool keyBuf []byte dataBuf *io.BufBinWriter } +// NativeContractCache is an interface representing cache for a native contract. +// Cache can be copied to create a wrapper around current DAO layer. Wrapped cache +// can be persisted to the underlying DAO native cache. +type NativeContractCache interface { + // Copy returns a copy of native cache item that can safely be changed within + // the subsequent DAO operations. + Copy() NativeContractCache +} + // NewSimple creates new simple dao using provided backend store. func NewSimple(backend storage.Store, stateRootInHeader bool, p2pSigExtensions bool) *Simple { st := storage.NewMemCachedStore(backend) @@ -52,7 +70,8 @@ func newSimple(st *storage.MemCachedStore, stateRootInHeader bool, p2pSigExtensi StateRootInHeader: stateRootInHeader, P2PSigExtensions: p2pSigExtensions, }, - Store: st, + Store: st, + nativeCache: make(map[int32]NativeContractCache), } } @@ -66,6 +85,7 @@ 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 } @@ -76,6 +96,13 @@ func (dao *Simple) GetPrivate() *Simple { *d = *dao // 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 +836,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 +854,77 @@ func (dao *Simple) Persist() (int, error) { // underlying store. It's a synchronous version of Persist that doesn't allow // other threads to work with DAO while flushing the Store. func (dao *Simple) PersistSync() (int, error) { + if dao.nativeCachePS != nil { + dao.nativeCacheLock.Lock() + dao.nativeCachePS.nativeCacheLock.Lock() + defer func() { + dao.nativeCachePS.nativeCacheLock.Unlock() + dao.nativeCacheLock.Unlock() + }() + dao.persistNativeCache() + } return dao.Store.PersistSync() } + +// persistNativeCache is internal unprotected method for native cache persisting. +// It does NO checks for nativeCachePS is not nil. +func (dao *Simple) persistNativeCache() { + lower := dao.nativeCachePS + for id, nativeCache := range dao.nativeCache { + lower.nativeCache[id] = nativeCache + } + dao.nativeCache = nil +} + +// GetROCache returns native contact cache. The cache CAN NOT be modified by +// the caller. It's the caller's duty to keep it unmodified. +func (dao *Simple) GetROCache(id int32) NativeContractCache { + if !dao.private { + dao.nativeCacheLock.RLock() + defer dao.nativeCacheLock.RUnlock() + } + + return dao.getCache(id, true) +} + +// GetRWCache returns native contact cache. The cache CAN BE safely modified +// by the caller. +func (dao *Simple) GetRWCache(id int32) NativeContractCache { + if !dao.private { + dao.nativeCacheLock.Lock() + defer dao.nativeCacheLock.Unlock() + } + + return dao.getCache(id, false) +} + +// getCache is an internal unlocked representation of GetROCache and GetRWCache. +func (dao *Simple) getCache(k int32, ro bool) NativeContractCache { + if itm, ok := dao.nativeCache[k]; ok { + // Don't need to create itm copy, because its value was already copied + // the first time it was retrieved from loser ps. + return itm + } + + if dao.nativeCachePS != nil { + if ro { + return dao.nativeCachePS.GetROCache(k) + } + v := dao.nativeCachePS.GetRWCache(k) + if v != nil { + // Create a copy here in order not to modify the existing cache. + cp := v.Copy() + dao.nativeCache[k] = cp + return cp + } + } + return nil +} + +// SetCache adds native contract cache to the cache map. +func (dao *Simple) SetCache(id int32, v NativeContractCache) { + dao.nativeCacheLock.Lock() + defer dao.nativeCacheLock.Unlock() + + dao.nativeCache[id] = v +} diff --git a/pkg/core/interop_system_core_test.go b/pkg/core/interop_system_core_test.go index 2407b5e46..2c7b34307 100644 --- a/pkg/core/interop_system_core_test.go +++ b/pkg/core/interop_system_core_test.go @@ -12,7 +12,6 @@ import ( "github.com/nspcc-dev/neo-go/pkg/config" "github.com/nspcc-dev/neo-go/pkg/config/netmode" "github.com/nspcc-dev/neo-go/pkg/core/block" - "github.com/nspcc-dev/neo-go/pkg/core/dao" "github.com/nspcc-dev/neo-go/pkg/core/interop" "github.com/nspcc-dev/neo-go/pkg/core/interop/contract" "github.com/nspcc-dev/neo-go/pkg/core/interop/iterator" @@ -533,7 +532,7 @@ func TestStorageFind(t *testing.T) { func createVM(t testing.TB) (*vm.VM, *interop.Context, *Blockchain) { chain := newTestChain(t) context := chain.newInteropContext(trigger.Application, - dao.NewSimple(chain.dao.Store, chain.config.StateRootInHeader, chain.config.P2PSigExtensions), nil, nil) + chain.dao.GetWrapped(), nil, nil) v := context.SpawnVM() return v, context, chain } diff --git a/pkg/core/native/designate.go b/pkg/core/native/designate.go index 6282c3384..809395c78 100644 --- a/pkg/core/native/designate.go +++ b/pkg/core/native/designate.go @@ -79,12 +79,12 @@ var ( ) var ( - _ interop.Contract = (*Designate)(nil) - _ storage.NativeContractCache = (*DesignationCache)(nil) + _ interop.Contract = (*Designate)(nil) + _ dao.NativeContractCache = (*DesignationCache)(nil) ) // Copy implements NativeContractCache interface. -func (c *DesignationCache) Copy() storage.NativeContractCache { +func (c *DesignationCache) Copy() dao.NativeContractCache { cp := &DesignationCache{} copyDesignationCache(c, cp) return cp @@ -128,7 +128,7 @@ func newDesignate(p2pSigExtensionsEnabled bool) *Designate { // data in the storage. func (s *Designate) Initialize(ic *interop.Context) error { cache := &DesignationCache{} - ic.DAO.Store.SetCache(s.ID, cache) + ic.DAO.SetCache(s.ID, cache) return nil } @@ -146,7 +146,7 @@ func (s *Designate) InitializeCache(d *dao.Simple) error { return fmt.Errorf("failed to get nodes from storage for %d role: %w", r, err) } } - d.Store.SetCache(s.ID, cache) + d.SetCache(s.ID, cache) return nil } @@ -157,7 +157,7 @@ func (s *Designate) OnPersist(ic *interop.Context) error { // PostPersist implements Contract interface. func (s *Designate) PostPersist(ic *interop.Context) error { - cache := ic.DAO.Store.GetRWCache(s.ID).(*DesignationCache) + cache := ic.DAO.GetRWCache(s.ID).(*DesignationCache) if !cache.rolesChangedFlag { return nil } @@ -273,7 +273,7 @@ func (s *Designate) GetLastDesignatedHash(d *dao.Simple, r noderoles.Role) (util if !s.isValidRole(r) { return util.Uint160{}, ErrInvalidRole } - cache := d.Store.GetROCache(s.ID).(*DesignationCache) + cache := d.GetROCache(s.ID).(*DesignationCache) if val := getCachedRoleData(cache, r); val != nil { return val.addr, nil } @@ -285,7 +285,7 @@ func (s *Designate) GetDesignatedByRole(d *dao.Simple, r noderoles.Role, index u if !s.isValidRole(r) { return nil, 0, ErrInvalidRole } - cache := d.Store.GetROCache(s.ID).(*DesignationCache) + cache := d.GetROCache(s.ID).(*DesignationCache) if val := getCachedRoleData(cache, r); val != nil { if val.height <= index { return val.nodes.Copy(), val.height, nil @@ -380,7 +380,7 @@ func (s *Designate) DesignateAsRole(ic *interop.Context, r noderoles.Role, pubs return err } - cache := ic.DAO.Store.GetRWCache(s.ID).(*DesignationCache) + 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) diff --git a/pkg/core/native/management.go b/pkg/core/native/management.go index 2643c4f03..6ed98cd97 100644 --- a/pkg/core/native/management.go +++ b/pkg/core/native/management.go @@ -58,12 +58,12 @@ var ( ) var ( - _ interop.Contract = (*Management)(nil) - _ storage.NativeContractCache = (*ManagementCache)(nil) + _ interop.Contract = (*Management)(nil) + _ dao.NativeContractCache = (*ManagementCache)(nil) ) // Copy implements NativeContractCache interface. -func (c *ManagementCache) Copy() storage.NativeContractCache { +func (c *ManagementCache) Copy() dao.NativeContractCache { cp := &ManagementCache{ contracts: make(map[util.Uint160]*state.Contract), nep11: make(map[util.Uint160]struct{}), @@ -170,7 +170,7 @@ 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) { - cache := d.Store.GetROCache(m.ID).(*ManagementCache) + cache := d.GetROCache(m.ID).(*ManagementCache) cs, ok := cache.contracts[hash] if !ok { return nil, storage.ErrKeyNotFound @@ -272,7 +272,7 @@ func (m *Management) deployWithData(ic *interop.Context, args []stackitem.Item) } func (m *Management) markUpdated(d *dao.Simple, hash util.Uint160, cs *state.Contract) { - cache := d.Store.GetRWCache(m.ID).(*ManagementCache) + cache := d.GetRWCache(m.ID).(*ManagementCache) delete(cache.nep11, hash) delete(cache.nep17, hash) if cs == nil { @@ -486,7 +486,7 @@ func (m *Management) OnPersist(ic *interop.Context) error { return err } if cache == nil { - cache = ic.DAO.Store.GetRWCache(m.ID).(*ManagementCache) + cache = ic.DAO.GetRWCache(m.ID).(*ManagementCache) } updateContractCache(cache, cs) } @@ -517,7 +517,7 @@ func (m *Management) InitializeCache(d *dao.Simple) error { if initErr != nil { return initErr } - d.Store.SetCache(m.ID, cache) + d.SetCache(m.ID, cache) return nil } @@ -530,7 +530,7 @@ func (m *Management) PostPersist(ic *interop.Context) error { // is updated every PostPersist, so until PostPersist is called, the result for the previous block // is returned. func (m *Management) GetNEP11Contracts(d *dao.Simple) []util.Uint160 { - cache := d.Store.GetROCache(m.ID).(*ManagementCache) + cache := d.GetROCache(m.ID).(*ManagementCache) result := make([]util.Uint160, 0, len(cache.nep11)) for h := range cache.nep11 { result = append(result, h) @@ -542,7 +542,7 @@ func (m *Management) GetNEP11Contracts(d *dao.Simple) []util.Uint160 { // is updated every PostPersist, so until PostPersist is called, the result for the previous block // is returned. func (m *Management) GetNEP17Contracts(d *dao.Simple) []util.Uint160 { - cache := d.Store.GetROCache(m.ID).(*ManagementCache) + cache := d.GetROCache(m.ID).(*ManagementCache) result := make([]util.Uint160, 0, len(cache.nep17)) for h := range cache.nep17 { result = append(result, h) @@ -560,7 +560,7 @@ func (m *Management) Initialize(ic *interop.Context) error { nep11: make(map[util.Uint160]struct{}), nep17: make(map[util.Uint160]struct{}), } - ic.DAO.Store.SetCache(m.ID, cache) + ic.DAO.SetCache(m.ID, cache) return nil } diff --git a/pkg/core/native/native_neo.go b/pkg/core/native/native_neo.go index b9dedc49f..f99aa2f07 100644 --- a/pkg/core/native/native_neo.go +++ b/pkg/core/native/native_neo.go @@ -105,12 +105,12 @@ var ( ) var ( - _ interop.Contract = (*NEO)(nil) - _ storage.NativeContractCache = (*NeoCache)(nil) + _ interop.Contract = (*NEO)(nil) + _ dao.NativeContractCache = (*NeoCache)(nil) ) // Copy implements NativeContractCache interface. -func (c *NeoCache) Copy() storage.NativeContractCache { +func (c *NeoCache) Copy() dao.NativeContractCache { cp := &NeoCache{} copyNeoCache(c, cp) return cp @@ -245,7 +245,7 @@ func (n *NEO) Initialize(ic *interop.Context) error { } // We need cache to be present in DAO before the subsequent call to `mint`. - ic.DAO.Store.SetCache(n.ID, cache) + ic.DAO.SetCache(n.ID, cache) committee0 := n.standbyKeys[:n.cfg.GetCommitteeSize(ic.Block.Index)] cvs := toKeysWithVotes(committee0) @@ -297,7 +297,7 @@ func (n *NEO) InitializeCache(bc interop.Ledger, d *dao.Simple) error { cache.gasPerBlock = n.getSortedGASRecordFromDAO(d) cache.registerPrice = getIntWithKey(n.ID, d, []byte{prefixRegisterPrice}) - d.Store.SetCache(n.ID, cache) + d.SetCache(n.ID, cache) return nil } @@ -348,7 +348,7 @@ func (n *NEO) updateCommittee(cache *NeoCache, ic *interop.Context) error { // OnPersist implements Contract interface. func (n *NEO) OnPersist(ic *interop.Context) error { if n.cfg.ShouldUpdateCommitteeAt(ic.Block.Index) { - cache := ic.DAO.Store.GetRWCache(n.ID).(*NeoCache) + cache := ic.DAO.GetRWCache(n.ID).(*NeoCache) oldKeys := cache.nextValidators oldCom := cache.committee if n.cfg.GetNumOfCNs(ic.Block.Index) != len(oldKeys) || @@ -365,7 +365,7 @@ 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) - cache := ic.DAO.Store.GetRWCache(n.ID).(*NeoCache) + cache := ic.DAO.GetRWCache(n.ID).(*NeoCache) pubs := getCommitteeMembers(cache) committeeSize := n.cfg.GetCommitteeSize(ic.Block.Index) index := int(ic.Block.Index) % committeeSize @@ -532,7 +532,7 @@ 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 { - cache := d.Store.GetROCache(n.ID).(*NeoCache) + cache := d.GetROCache(n.ID).(*NeoCache) gr := cache.gasPerBlock for i := len(gr) - 1; i >= 0; i-- { if gr[i].Index <= index { @@ -545,7 +545,7 @@ func (n *NEO) GetGASPerBlock(d *dao.Simple, index uint32) *big.Int { // GetCommitteeAddress returns address of the committee. func (n *NEO) GetCommitteeAddress(d *dao.Simple) util.Uint160 { - cache := d.Store.GetROCache(n.ID).(*NeoCache) + cache := d.GetROCache(n.ID).(*NeoCache) return cache.committeeHash } @@ -575,7 +575,7 @@ func (n *NEO) SetGASPerBlock(ic *interop.Context, index uint32, gas *big.Int) er return errors.New("invalid committee signature") } n.putGASRecord(ic.DAO, index, gas) - cache := ic.DAO.Store.GetRWCache(n.ID).(*NeoCache) + cache := ic.DAO.GetRWCache(n.ID).(*NeoCache) cache.gasPerBlock = append(cache.gasPerBlock, gasIndexPair{ Index: index, GASPerBlock: *gas, @@ -588,7 +588,7 @@ func (n *NEO) getRegisterPrice(ic *interop.Context, _ []stackitem.Item) stackite } func (n *NEO) getRegisterPriceInternal(d *dao.Simple) int64 { - cache := d.Store.GetROCache(n.ID).(*NeoCache) + cache := d.GetROCache(n.ID).(*NeoCache) return cache.registerPrice } @@ -602,7 +602,7 @@ func (n *NEO) setRegisterPrice(ic *interop.Context, args []stackitem.Item) stack } setIntWithKey(n.ID, ic.DAO, []byte{prefixRegisterPrice}, price.Int64()) - cache := ic.DAO.Store.GetRWCache(n.ID).(*NeoCache) + cache := ic.DAO.GetRWCache(n.ID).(*NeoCache) cache.registerPrice = price.Int64() return stackitem.Null{} } @@ -672,7 +672,7 @@ func (n *NEO) CalculateNEOHolderReward(d *dao.Simple, value *big.Int, start, end } else if value.Sign() < 0 { return nil, errors.New("negative value") } - cache := d.Store.GetROCache(n.ID).(*NeoCache) + cache := d.GetROCache(n.ID).(*NeoCache) gr := cache.gasPerBlock var sum, tmp big.Int for i := len(gr) - 1; i >= 0; i-- { @@ -743,7 +743,7 @@ func (n *NEO) UnregisterCandidateInternal(ic *interop.Context, pub *keys.PublicK if si == nil { return nil } - cache := ic.DAO.Store.GetRWCache(n.ID).(*NeoCache) + cache := ic.DAO.GetRWCache(n.ID).(*NeoCache) cache.validators = nil c := new(candidate).FromBytes(si) c.Registered = false @@ -823,7 +823,7 @@ 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 { - cache := d.Store.GetRWCache(n.ID).(*NeoCache) + cache := d.GetRWCache(n.ID).(*NeoCache) cache.votesChanged = true if acc.VoteTo != nil { key := makeValidatorKey(acc.VoteTo) @@ -934,7 +934,7 @@ 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) - cache := d.Store.GetRWCache(n.ID).(*NeoCache) + cache := d.GetRWCache(n.ID).(*NeoCache) if vals := cache.validators; vals != nil && numOfCNs == len(vals) { return vals.Copy(), nil } @@ -969,7 +969,7 @@ 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(d *dao.Simple) keys.PublicKeys { - cache := d.Store.GetROCache(n.ID).(*NeoCache) + cache := d.GetROCache(n.ID).(*NeoCache) return getCommitteeMembers(cache) } @@ -1049,7 +1049,7 @@ func (n *NEO) getNextBlockValidators(ic *interop.Context, _ []stackitem.Item) st // GetNextBlockValidatorsInternal returns next block validators. func (n *NEO) GetNextBlockValidatorsInternal(d *dao.Simple) keys.PublicKeys { - cache := d.Store.GetROCache(n.ID).(*NeoCache) + cache := d.GetROCache(n.ID).(*NeoCache) return cache.nextValidators.Copy() } diff --git a/pkg/core/native/notary.go b/pkg/core/native/notary.go index 79cd648c7..fadf8a372 100644 --- a/pkg/core/native/notary.go +++ b/pkg/core/native/notary.go @@ -53,12 +53,12 @@ var ( ) var ( - _ interop.Contract = (*Notary)(nil) - _ storage.NativeContractCache = (*NotaryCache)(nil) + _ interop.Contract = (*Notary)(nil) + _ dao.NativeContractCache = (*NotaryCache)(nil) ) // Copy implements NativeContractCache interface. -func (c *NotaryCache) Copy() storage.NativeContractCache { +func (c *NotaryCache) Copy() dao.NativeContractCache { cp := &NotaryCache{} copyNotaryCache(c, cp) return cp @@ -142,7 +142,7 @@ func (n *Notary) Initialize(ic *interop.Context) error { maxNotValidBeforeDelta: defaultMaxNotValidBeforeDelta, notaryServiceFeePerKey: defaultNotaryServiceFeePerKey, } - ic.DAO.Store.SetCache(n.ID, cache) + ic.DAO.SetCache(n.ID, cache) return nil } @@ -152,7 +152,7 @@ func (n *Notary) InitializeCache(d *dao.Simple) error { notaryServiceFeePerKey: getIntWithKey(n.ID, d, notaryServiceFeeKey), } - d.Store.SetCache(n.ID, cache) + d.SetCache(n.ID, cache) return nil } @@ -407,7 +407,7 @@ 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 { - cache := dao.Store.GetROCache(n.ID).(*NotaryCache) + cache := dao.GetROCache(n.ID).(*NotaryCache) return cache.maxNotValidBeforeDelta } @@ -423,7 +423,7 @@ func (n *Notary) setMaxNotValidBeforeDelta(ic *interop.Context, args []stackitem panic("invalid committee signature") } setIntWithKey(n.ID, ic.DAO, maxNotValidBeforeDeltaKey, int64(value)) - cache := ic.DAO.Store.GetRWCache(n.ID).(*NotaryCache) + cache := ic.DAO.GetRWCache(n.ID).(*NotaryCache) cache.maxNotValidBeforeDelta = value return stackitem.Null{} } @@ -435,7 +435,7 @@ 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 { - cache := dao.Store.GetROCache(n.ID).(*NotaryCache) + cache := dao.GetROCache(n.ID).(*NotaryCache) return cache.notaryServiceFeePerKey } @@ -449,7 +449,7 @@ func (n *Notary) setNotaryServiceFeePerKey(ic *interop.Context, args []stackitem panic("invalid committee signature") } setIntWithKey(n.ID, ic.DAO, notaryServiceFeeKey, int64(value)) - cache := ic.DAO.Store.GetRWCache(n.ID).(*NotaryCache) + cache := ic.DAO.GetRWCache(n.ID).(*NotaryCache) cache.notaryServiceFeePerKey = value return stackitem.Null{} } diff --git a/pkg/core/native/oracle.go b/pkg/core/native/oracle.go index 25273f60f..7b3914b5c 100644 --- a/pkg/core/native/oracle.go +++ b/pkg/core/native/oracle.go @@ -84,12 +84,12 @@ var ( ) var ( - _ interop.Contract = (*Oracle)(nil) - _ storage.NativeContractCache = (*OracleCache)(nil) + _ interop.Contract = (*Oracle)(nil) + _ dao.NativeContractCache = (*OracleCache)(nil) ) // Copy implements NativeContractCache interface. -func (c *OracleCache) Copy() storage.NativeContractCache { +func (c *OracleCache) Copy() dao.NativeContractCache { cp := &OracleCache{} copyOracleCache(c, cp) return cp @@ -235,14 +235,14 @@ func (o *Oracle) Initialize(ic *interop.Context) error { cache := &OracleCache{ requestPrice: int64(DefaultOracleRequestPrice), } - ic.DAO.Store.SetCache(o.ID, cache) + 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.Store.SetCache(o.ID, cache) + d.SetCache(o.ID, cache) } func getResponse(tx *transaction.Transaction) *transaction.OracleResponse { @@ -459,7 +459,7 @@ func (o *Oracle) getPrice(ic *interop.Context, _ []stackitem.Item) stackitem.Ite } func (o *Oracle) getPriceInternal(d *dao.Simple) int64 { - cache := d.Store.GetROCache(o.ID).(*OracleCache) + cache := d.GetROCache(o.ID).(*OracleCache) return cache.requestPrice } @@ -472,7 +472,7 @@ func (o *Oracle) setPrice(ic *interop.Context, args []stackitem.Item) stackitem. panic("invalid committee signature") } setIntWithKey(o.ID, ic.DAO, prefixRequestPrice, price.Int64()) - cache := ic.DAO.Store.GetRWCache(o.ID).(*OracleCache) + cache := ic.DAO.GetRWCache(o.ID).(*OracleCache) cache.requestPrice = price.Int64() return stackitem.Null{} } diff --git a/pkg/core/native/policy.go b/pkg/core/native/policy.go index b4a7895ab..9dd40876d 100644 --- a/pkg/core/native/policy.go +++ b/pkg/core/native/policy.go @@ -63,12 +63,12 @@ type PolicyCache struct { } var ( - _ interop.Contract = (*Policy)(nil) - _ storage.NativeContractCache = (*PolicyCache)(nil) + _ interop.Contract = (*Policy)(nil) + _ dao.NativeContractCache = (*PolicyCache)(nil) ) // Copy implements NativeContractCache interface. -func (c *PolicyCache) Copy() storage.NativeContractCache { +func (c *PolicyCache) Copy() dao.NativeContractCache { cp := &PolicyCache{} copyPolicyCache(c, cp) return cp @@ -148,7 +148,7 @@ func (p *Policy) Initialize(ic *interop.Context) error { storagePrice: DefaultStoragePrice, blockedAccounts: make([]util.Uint160, 0), } - ic.DAO.Store.SetCache(p.ID, cache) + ic.DAO.SetCache(p.ID, cache) return nil } @@ -159,7 +159,7 @@ func (p *Policy) InitializeCache(d *dao.Simple) error { if err != nil { return err } - d.Store.SetCache(p.ID, cache) + d.SetCache(p.ID, cache) return nil } @@ -204,13 +204,13 @@ 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 { - cache := dao.Store.GetROCache(p.ID).(*PolicyCache) + cache := dao.GetROCache(p.ID).(*PolicyCache) return cache.feePerByte } // GetMaxVerificationGas returns maximum gas allowed to be burned during verificaion. func (p *Policy) GetMaxVerificationGas(dao *dao.Simple) int64 { - cache := dao.Store.GetROCache(p.ID).(*PolicyCache) + cache := dao.GetROCache(p.ID).(*PolicyCache) return cache.maxVerificationGas } @@ -220,7 +220,7 @@ func (p *Policy) getExecFeeFactor(ic *interop.Context, _ []stackitem.Item) stack // GetExecFeeFactorInternal returns current execution fee factor. func (p *Policy) GetExecFeeFactorInternal(d *dao.Simple) int64 { - cache := d.Store.GetROCache(p.ID).(*PolicyCache) + cache := d.GetROCache(p.ID).(*PolicyCache) return int64(cache.execFeeFactor) } @@ -233,7 +233,7 @@ func (p *Policy) setExecFeeFactor(ic *interop.Context, args []stackitem.Item) st panic("invalid committee signature") } setIntWithKey(p.ID, ic.DAO, execFeeFactorKey, int64(value)) - cache := ic.DAO.Store.GetRWCache(p.ID).(*PolicyCache) + cache := ic.DAO.GetRWCache(p.ID).(*PolicyCache) cache.execFeeFactor = value return stackitem.Null{} } @@ -255,7 +255,7 @@ func (p *Policy) IsBlocked(dao *dao.Simple, hash util.Uint160) bool { // 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.Store.GetROCache(p.ID).(*PolicyCache) + cache := dao.GetROCache(p.ID).(*PolicyCache) length := len(cache.blockedAccounts) i := sort.Search(length, func(i int) bool { return !cache.blockedAccounts[i].Less(hash) @@ -272,7 +272,7 @@ func (p *Policy) getStoragePrice(ic *interop.Context, _ []stackitem.Item) stacki // GetStoragePriceInternal returns current execution fee factor. func (p *Policy) GetStoragePriceInternal(d *dao.Simple) int64 { - cache := d.Store.GetROCache(p.ID).(*PolicyCache) + cache := d.GetROCache(p.ID).(*PolicyCache) return int64(cache.storagePrice) } @@ -285,7 +285,7 @@ func (p *Policy) setStoragePrice(ic *interop.Context, args []stackitem.Item) sta panic("invalid committee signature") } setIntWithKey(p.ID, ic.DAO, storagePriceKey, int64(value)) - cache := ic.DAO.Store.GetRWCache(p.ID).(*PolicyCache) + cache := ic.DAO.GetRWCache(p.ID).(*PolicyCache) cache.storagePrice = value return stackitem.Null{} } @@ -300,7 +300,7 @@ func (p *Policy) setFeePerByte(ic *interop.Context, args []stackitem.Item) stack panic("invalid committee signature") } setIntWithKey(p.ID, ic.DAO, feePerByteKey, value) - cache := ic.DAO.Store.GetRWCache(p.ID).(*PolicyCache) + cache := ic.DAO.GetRWCache(p.ID).(*PolicyCache) cache.feePerByte = value return stackitem.Null{} } @@ -323,7 +323,7 @@ func (p *Policy) blockAccount(ic *interop.Context, args []stackitem.Item) stacki } key := append([]byte{blockedAccountPrefix}, hash.BytesBE()...) ic.DAO.PutStorageItem(p.ID, key, state.StorageItem{}) - cache := ic.DAO.Store.GetRWCache(p.ID).(*PolicyCache) + cache := ic.DAO.GetRWCache(p.ID).(*PolicyCache) if len(cache.blockedAccounts) == i { cache.blockedAccounts = append(cache.blockedAccounts, hash) } else { @@ -346,7 +346,7 @@ func (p *Policy) unblockAccount(ic *interop.Context, args []stackitem.Item) stac } key := append([]byte{blockedAccountPrefix}, hash.BytesBE()...) ic.DAO.DeleteStorageItem(p.ID, key) - cache := ic.DAO.Store.GetRWCache(p.ID).(*PolicyCache) + cache := ic.DAO.GetRWCache(p.ID).(*PolicyCache) cache.blockedAccounts = append(cache.blockedAccounts[:i], cache.blockedAccounts[i+1:]...) return stackitem.NewBool(true) } diff --git a/pkg/core/storage/memcached_store.go b/pkg/core/storage/memcached_store.go index 42fdccf69..10461545e 100644 --- a/pkg/core/storage/memcached_store.go +++ b/pkg/core/storage/memcached_store.go @@ -15,9 +15,6 @@ import ( type MemCachedStore struct { MemoryStore - nativeCacheLock sync.RWMutex - nativeCache map[int32]NativeContractCache - private bool // plock protects Persist from double entrance. plock sync.Mutex @@ -25,15 +22,6 @@ type MemCachedStore struct { ps Store } -// 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 -} - type ( // KeyValue represents key-value pair. KeyValue struct { @@ -58,12 +46,8 @@ type ( // NewMemCachedStore creates a new MemCachedStore object. func NewMemCachedStore(lower Store) *MemCachedStore { - // Do not copy cache from ps; instead should create clear map: GetRWCache and - // GetROCache will retrieve cache from the underlying nativeCache if requested. - cache := make(map[int32]NativeContractCache) return &MemCachedStore{ MemoryStore: *NewMemoryStore(), - nativeCache: cache, ps: lower, } } @@ -71,14 +55,8 @@ func NewMemCachedStore(lower Store) *MemCachedStore { // NewPrivateMemCachedStore creates a new private (unlocked) MemCachedStore object. // Private cached stores are closed after Persist. func NewPrivateMemCachedStore(lower Store) *MemCachedStore { - // Do not copy cache from ps; instead should create clear map: GetRWCache and - // GetROCache will retrieve cache from the underlying nativeCache if requested. - // The lowest underlying store MUST have its native cache initialized, otherwise - // GetROCache and GetRWCache won't work properly. - cache := make(map[int32]NativeContractCache) return &MemCachedStore{ MemoryStore: *NewMemoryStore(), - nativeCache: cache, private: true, ps: lower, } @@ -363,23 +341,15 @@ func (s *MemCachedStore) persist(isSync bool) (int, error) { } s.mem = nil s.stor = nil - if cached, ok := s.ps.(*MemCachedStore); ok { - for id, nativeCache := range s.nativeCache { - cached.nativeCache[id] = nativeCache - } - s.nativeCache = nil - } return keys, nil } s.plock.Lock() defer s.plock.Unlock() s.mut.Lock() - s.nativeCacheLock.Lock() keys = len(s.mem) + len(s.stor) if keys == 0 { - s.nativeCacheLock.Unlock() s.mut.Unlock() return 0, nil } @@ -388,30 +358,17 @@ func (s *MemCachedStore) persist(isSync bool) (int, error) { // starts using fresh new maps. This tempstore is only known here and // nothing ever changes it, therefore accesses to it (reads) can go // unprotected while writes are handled by s proper. - var tempstore = &MemCachedStore{MemoryStore: MemoryStore{mem: s.mem, stor: s.stor}, ps: s.ps, nativeCache: s.nativeCache} + var tempstore = &MemCachedStore{MemoryStore: MemoryStore{mem: s.mem, stor: s.stor}, ps: s.ps} s.ps = tempstore s.mem = make(map[string][]byte, len(s.mem)) s.stor = make(map[string][]byte, len(s.stor)) - cached, isPSCached := tempstore.ps.(*MemCachedStore) - if isPSCached { - s.nativeCache = make(map[int32]NativeContractCache) - } if !isSync { - s.nativeCacheLock.Unlock() s.mut.Unlock() } - if isPSCached { - cached.nativeCacheLock.Lock() - for id, nativeCache := range tempstore.nativeCache { - cached.nativeCache[id] = nativeCache - } - cached.nativeCacheLock.Unlock() - } err = tempstore.ps.PutChangeSet(tempstore.mem, tempstore.stor) if !isSync { s.mut.Lock() - s.nativeCacheLock.Lock() } if err == nil { // tempstore.mem and tempstore.del are completely flushed now @@ -427,69 +384,14 @@ func (s *MemCachedStore) persist(isSync bool) (int, error) { for k := range s.stor { put(tempstore.stor, k, s.stor[k]) } - if isPSCached { - for id, nativeCache := range s.nativeCache { - tempstore.nativeCache[id] = nativeCache - } - s.nativeCache = tempstore.nativeCache - } s.ps = tempstore.ps s.mem = tempstore.mem s.stor = tempstore.stor } - s.nativeCacheLock.Unlock() s.mut.Unlock() return keys, err } -// 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 (s *MemCachedStore) GetROCache(id int32) NativeContractCache { - s.nativeCacheLock.RLock() - defer s.nativeCacheLock.RUnlock() - - return s.getCache(id, true) -} - -// GetRWCache returns native contact cache. The cache CAN BE safely modified -// by the caller. -func (s *MemCachedStore) GetRWCache(k int32) NativeContractCache { - s.nativeCacheLock.Lock() - defer s.nativeCacheLock.Unlock() - - return s.getCache(k, false) -} - -func (s *MemCachedStore) getCache(k int32, ro bool) NativeContractCache { - if itm, ok := s.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 cached, ok := s.ps.(*MemCachedStore); ok { - if ro { - return cached.GetROCache(k) - } - v := cached.GetRWCache(k) - if v != nil { - // Create a copy here in order not to modify the existing cache. - cp := v.Copy() - s.nativeCache[k] = cp - return cp - } - } - - return nil -} - -func (s *MemCachedStore) SetCache(k int32, v NativeContractCache) { - s.nativeCacheLock.Lock() - defer s.nativeCacheLock.Unlock() - - s.nativeCache[k] = v -} - // Close implements Store interface, clears up memory and closes the lower layer // Store. func (s *MemCachedStore) Close() error { From a6a0c1eb12e5f2b289fdfb73fe2c4cfc5d8288cc Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Wed, 20 Apr 2022 18:42:49 +0300 Subject: [PATCH 21/24] core: avoid lock copy in private DAO constructor Fix the following linter warning: ``` pkg/core/dao/dao.go:101:7 govet copylocks: assignment copies lock value to *d: github.com/nspcc-dev/neo-go/pkg/core/dao.Simple contains sync.RWMutex ``` --- pkg/core/dao/dao.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pkg/core/dao/dao.go b/pkg/core/dao/dao.go index db9bc0807..af268e679 100644 --- a/pkg/core/dao/dao.go +++ b/pkg/core/dao/dao.go @@ -92,8 +92,11 @@ func (dao *Simple) GetWrapped() *Simple { // 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 From 335c1ee36943c35b8913eb47132d480bca96f92b Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Wed, 27 Apr 2022 18:14:42 +0300 Subject: [PATCH 22/24] core: optimize access to NEO cache Do not copy RW cache each block. Instead we should get RO cache and change it only if there's a necessity. --- pkg/core/native/native_neo.go | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/pkg/core/native/native_neo.go b/pkg/core/native/native_neo.go index f99aa2f07..b735cbe8b 100644 --- a/pkg/core/native/native_neo.go +++ b/pkg/core/native/native_neo.go @@ -365,7 +365,7 @@ 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) - cache := ic.DAO.GetRWCache(n.ID).(*NeoCache) + cache := ic.DAO.GetROCache(n.ID).(*NeoCache) pubs := getCommitteeMembers(cache) committeeSize := n.cfg.GetCommitteeSize(ic.Block.Index) index := int(ic.Block.Index) % committeeSize @@ -380,8 +380,11 @@ func (n *NEO) PostPersist(ic *interop.Context) error { voterReward.Div(voterReward, big.NewInt(int64(committeeSize+validatorsCount))) voterReward.Div(voterReward, big100) - var cs = cache.committee - 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) @@ -405,6 +408,10 @@ func (n *NEO) PostPersist(ic *interop.Context) error { tmp.Add(tmp, r) binary.BigEndian.PutUint32(key[34:], ic.Block.Index+1) + 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)) From 9cc41528ef918fff45ce4eec56125d313ee4c2c3 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Thu, 28 Apr 2022 18:38:13 +0300 Subject: [PATCH 23/24] core: avoid unnecessary NEO cached values copying --- pkg/core/native/native_neo.go | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/pkg/core/native/native_neo.go b/pkg/core/native/native_neo.go index b735cbe8b..dae1bfb25 100644 --- a/pkg/core/native/native_neo.go +++ b/pkg/core/native/native_neo.go @@ -118,11 +118,11 @@ func (c *NeoCache) Copy() dao.NativeContractCache { func copyNeoCache(src, dst *NeoCache) { dst.votesChanged = src.votesChanged - dst.nextValidators = src.nextValidators.Copy() - dst.validators = src.validators.Copy() - - dst.committee = make(keysWithVotes, len(src.committee)) - copy(dst.committee, src.committee) + // 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 @@ -941,10 +941,13 @@ 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) - cache := d.GetRWCache(n.ID).(*NeoCache) + // 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 } + cache = d.GetRWCache(n.ID).(*NeoCache) result, _, err := n.computeCommitteeMembers(bc, d) if err != nil { return nil, err From 473955c2d69424e5c5b50c167a886d18cea11711 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Fri, 29 Apr 2022 18:00:46 +0300 Subject: [PATCH 24/24] core: use proper current block height/hash for interop API --- pkg/core/blockchain.go | 12 ++++++------ pkg/core/interop/context.go | 29 ++++++++++++++++++++++++++++ pkg/core/native/designate.go | 2 +- pkg/core/native/interop.go | 2 +- pkg/core/native/ledger.go | 36 +++++++++++++++++------------------ pkg/core/native/native_neo.go | 25 ++++++++++++------------ pkg/core/native/notary.go | 10 +++++----- 7 files changed, 72 insertions(+), 44 deletions(-) diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index f09c73562..3776b4887 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -433,7 +433,7 @@ func (bc *Blockchain) init() error { return fmt.Errorf("can't init MPT at height %d: %w", bHeight, err) } - err = bc.initializeNativeCache(bc.dao) + err = bc.initializeNativeCache(bc.blockHeight, bc.dao) if err != nil { return fmt.Errorf("can't init natives cache: %w", err) } @@ -570,7 +570,7 @@ func (bc *Blockchain) jumpToStateInternal(p uint32, stage stateJumpStage) error Root: block.PrevStateRoot, }) - err = bc.initializeNativeCache(bc.dao) + err = bc.initializeNativeCache(block.Index, bc.dao) if err != nil { return fmt.Errorf("failed to initialize natives cache: %w", err) } @@ -585,8 +585,8 @@ func (bc *Blockchain) jumpToStateInternal(p uint32, stage stateJumpStage) error return nil } -func (bc *Blockchain) initializeNativeCache(d *dao.Simple) error { - err := bc.contracts.NEO.InitializeCache(bc, d) +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) } @@ -2143,7 +2143,7 @@ func (bc *Blockchain) GetCommittee() (keys.PublicKeys, error) { // 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. @@ -2189,7 +2189,7 @@ func (bc *Blockchain) GetTestHistoricVM(t trigger.Type, tx *transaction.Transact 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(dTrie) + err = bc.initializeNativeCache(b.Index, dTrie) if err != nil { return nil, fmt.Errorf("failed to initialize native cache backed by historic DAO: %w", err) } diff --git a/pkg/core/interop/context.go b/pkg/core/interop/context.go index 9217e8d58..351052918 100644 --- a/pkg/core/interop/context.go +++ b/pkg/core/interop/context.go @@ -13,6 +13,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/core/dao" "github.com/nspcc-dev/neo-go/pkg/core/interop/interopnames" "github.com/nspcc-dev/neo-go/pkg/core/state" + "github.com/nspcc-dev/neo-go/pkg/core/storage" "github.com/nspcc-dev/neo-go/pkg/core/transaction" "github.com/nspcc-dev/neo-go/pkg/crypto/hash" "github.com/nspcc-dev/neo-go/pkg/io" @@ -339,3 +340,31 @@ func (ic *Context) Exec() error { defer ic.Finalize() return ic.VM.Run() } + +// BlockHeight returns current block height got from Context's block if it's set. +func (ic *Context) BlockHeight() uint32 { + if ic.Block != nil { + return ic.Block.Index - 1 // Persisting block is not yet stored. + } + return ic.Chain.BlockHeight() +} + +// CurrentBlockHash returns current block hash got from Context's block if it's set. +func (ic *Context) CurrentBlockHash() util.Uint256 { + if ic.Block != nil { + return ic.Chain.GetHeaderHash(int(ic.Block.Index - 1)) // Persisting block is not yet stored. + } + return ic.Chain.CurrentBlockHash() +} + +// GetBlock returns block if it exists and available at the current Context's height. +func (ic *Context) GetBlock(hash util.Uint256) (*block.Block, error) { + block, err := ic.Chain.GetBlock(hash) + if err != nil { + return nil, err + } + if block.Index > ic.BlockHeight() { + return nil, storage.ErrKeyNotFound + } + return block, nil +} diff --git a/pkg/core/native/designate.go b/pkg/core/native/designate.go index 809395c78..e57e3c598 100644 --- a/pkg/core/native/designate.go +++ b/pkg/core/native/designate.go @@ -188,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)) diff --git a/pkg/core/native/interop.go b/pkg/core/native/interop.go index 3fd2ab57a..28fd6edc4 100644 --- a/pkg/core/native/interop.go +++ b/pkg/core/native/interop.go @@ -31,7 +31,7 @@ func Call(ic *interop.Context) error { if len(history) == 0 { return fmt.Errorf("native contract %s is disabled", c.Metadata().Name) } - if history[0] > ic.Chain.BlockHeight() { + if history[0] > ic.BlockHeight() { return fmt.Errorf("native contract %s is active after height = %d", c.Metadata().Name, history[0]) } m, ok := c.Metadata().GetMethodByOffset(ic.VM.Context().IP()) diff --git a/pkg/core/native/ledger.go b/pkg/core/native/ledger.go index 64506e506..dabd443a8 100644 --- a/pkg/core/native/ledger.go +++ b/pkg/core/native/ledger.go @@ -103,19 +103,19 @@ func (l *Ledger) PostPersist(ic *interop.Context) error { // currentHash implements currentHash SC method. func (l *Ledger) currentHash(ic *interop.Context, _ []stackitem.Item) stackitem.Item { - return stackitem.Make(ic.Chain.CurrentBlockHash().BytesBE()) + return stackitem.Make(ic.CurrentBlockHash().BytesBE()) } // currentIndex implements currentIndex SC method. func (l *Ledger) currentIndex(ic *interop.Context, _ []stackitem.Item) stackitem.Item { - return stackitem.Make(ic.Chain.BlockHeight()) + return stackitem.Make(ic.BlockHeight()) } // getBlock implements getBlock SC method. func (l *Ledger) getBlock(ic *interop.Context, params []stackitem.Item) stackitem.Item { - hash := getBlockHashFromItem(ic.Chain, params[0]) - block, err := ic.Chain.GetBlock(hash) - if err != nil || !isTraceableBlock(ic.Chain, block.Index) { + hash := getBlockHashFromItem(ic, params[0]) + block, err := ic.GetBlock(hash) + if err != nil || !isTraceableBlock(ic, block.Index) { return stackitem.Null{} } return BlockToStackItem(block) @@ -124,7 +124,7 @@ func (l *Ledger) getBlock(ic *interop.Context, params []stackitem.Item) stackite // getTransaction returns transaction to the SC. func (l *Ledger) getTransaction(ic *interop.Context, params []stackitem.Item) stackitem.Item { tx, h, err := getTransactionAndHeight(ic.DAO, params[0]) - if err != nil || !isTraceableBlock(ic.Chain, h) { + if err != nil || !isTraceableBlock(ic, h) { return stackitem.Null{} } return TransactionToStackItem(tx) @@ -133,7 +133,7 @@ func (l *Ledger) getTransaction(ic *interop.Context, params []stackitem.Item) st // getTransactionHeight returns transaction height to the SC. func (l *Ledger) getTransactionHeight(ic *interop.Context, params []stackitem.Item) stackitem.Item { _, h, err := getTransactionAndHeight(ic.DAO, params[0]) - if err != nil || !isTraceableBlock(ic.Chain, h) { + if err != nil || !isTraceableBlock(ic, h) { return stackitem.Make(-1) } return stackitem.Make(h) @@ -142,10 +142,10 @@ func (l *Ledger) getTransactionHeight(ic *interop.Context, params []stackitem.It // getTransactionFromBlock returns transaction with the given index from the // block with height or hash specified. func (l *Ledger) getTransactionFromBlock(ic *interop.Context, params []stackitem.Item) stackitem.Item { - hash := getBlockHashFromItem(ic.Chain, params[0]) + hash := getBlockHashFromItem(ic, params[0]) index := toUint32(params[1]) - block, err := ic.Chain.GetBlock(hash) - if err != nil || !isTraceableBlock(ic.Chain, block.Index) { + block, err := ic.GetBlock(hash) + if err != nil || !isTraceableBlock(ic, block.Index) { return stackitem.Null{} } if index >= uint32(len(block.Transactions)) { @@ -157,7 +157,7 @@ func (l *Ledger) getTransactionFromBlock(ic *interop.Context, params []stackitem // getTransactionSigners returns transaction signers to the SC. func (l *Ledger) getTransactionSigners(ic *interop.Context, params []stackitem.Item) stackitem.Item { tx, h, err := getTransactionAndHeight(ic.DAO, params[0]) - if err != nil || !isTraceableBlock(ic.Chain, h) { + if err != nil || !isTraceableBlock(ic, h) { return stackitem.Null{} } return SignersToStackItem(tx.Signers) @@ -170,7 +170,7 @@ func (l *Ledger) getTransactionVMState(ic *interop.Context, params []stackitem.I panic(err) } h, _, aer, err := ic.DAO.GetTxExecResult(hash) - if err != nil || !isTraceableBlock(ic.Chain, h) { + if err != nil || !isTraceableBlock(ic, h) { return stackitem.Make(vm.NoneState) } return stackitem.Make(aer.VMState) @@ -178,9 +178,9 @@ func (l *Ledger) getTransactionVMState(ic *interop.Context, params []stackitem.I // isTraceableBlock defines whether we're able to give information about // the block with index specified. -func isTraceableBlock(bc interop.Ledger, index uint32) bool { - height := bc.BlockHeight() - MaxTraceableBlocks := bc.GetConfig().MaxTraceableBlocks +func isTraceableBlock(ic *interop.Context, index uint32) bool { + height := ic.BlockHeight() + MaxTraceableBlocks := ic.Chain.GetConfig().MaxTraceableBlocks return index <= height && index+MaxTraceableBlocks > height } @@ -188,17 +188,17 @@ func isTraceableBlock(bc interop.Ledger, index uint32) bool { // Ledger if needed. Interop functions accept both block numbers and // block hashes as parameters, thus this function is needed. It's supposed to // be called within VM context, so it panics if anything goes wrong. -func getBlockHashFromItem(bc interop.Ledger, item stackitem.Item) util.Uint256 { +func getBlockHashFromItem(ic *interop.Context, item stackitem.Item) util.Uint256 { bigindex, err := item.TryInteger() if err == nil && bigindex.IsUint64() { index := bigindex.Uint64() if index > math.MaxUint32 { panic("bad block index") } - if uint32(index) > bc.BlockHeight() { + if uint32(index) > ic.BlockHeight() { panic(fmt.Errorf("no block with index %d", index)) } - return bc.GetHeaderHash(int(index)) + return ic.Chain.GetHeaderHash(int(index)) } hash, err := getUint256FromItem(item) if err != nil { diff --git a/pkg/core/native/native_neo.go b/pkg/core/native/native_neo.go index dae1bfb25..84f66bee0 100644 --- a/pkg/core/native/native_neo.go +++ b/pkg/core/native/native_neo.go @@ -249,7 +249,7 @@ func (n *NEO) Initialize(ic *interop.Context) error { committee0 := n.standbyKeys[:n.cfg.GetCommitteeSize(ic.Block.Index)] cvs := toKeysWithVotes(committee0) - err := n.updateCache(cache, cvs, ic.Chain) + err := n.updateCache(cache, cvs, ic.BlockHeight()) if err != nil { return err } @@ -279,7 +279,7 @@ func (n *NEO) Initialize(ic *interop.Context) error { // 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 { +func (n *NEO) InitializeCache(blockHeight uint32, d *dao.Simple) error { cache := &NeoCache{ gasPerVoteCache: make(map[string]big.Int), votesChanged: true, @@ -290,7 +290,7 @@ func (n *NEO) InitializeCache(bc interop.Ledger, d *dao.Simple) error { if err := committee.DecodeBytes(si); err != nil { return fmt.Errorf("failed to decode committee: %w", err) } - if err := n.updateCache(cache, committee, bc); err != nil { + if err := n.updateCache(cache, committee, blockHeight); err != nil { return fmt.Errorf("failed to update cache: %w", err) } @@ -309,7 +309,7 @@ func (n *NEO) initConfigCache(cfg config.ProtocolConfiguration) error { return err } -func (n *NEO) updateCache(cache *NeoCache, cvs keysWithVotes, bc interop.Ledger) error { +func (n *NEO) updateCache(cache *NeoCache, cvs keysWithVotes, blockHeight uint32) error { cache.committee = cvs var committee = getCommitteeMembers(cache) @@ -319,8 +319,7 @@ func (n *NEO) updateCache(cache *NeoCache, cvs keysWithVotes, bc interop.Ledger) } cache.committeeHash = hash.Hash160(script) - // TODO: use block height from interop context for proper historical calls handling. - nextVals := committee[:n.cfg.GetNumOfCNs(bc.BlockHeight()+1)].Copy() + nextVals := committee[:n.cfg.GetNumOfCNs(blockHeight+1)].Copy() sort.Sort(nextVals) cache.nextValidators = nextVals return nil @@ -333,11 +332,11 @@ func (n *NEO) updateCommittee(cache *NeoCache, ic *interop.Context) error { 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(cache, cvs, ic.Chain); err != nil { + if err := n.updateCache(cache, cvs, ic.BlockHeight()); err != nil { return err } cache.votesChanged = false @@ -939,8 +938,8 @@ 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) +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) @@ -948,7 +947,7 @@ func (n *NEO) ComputeNextBlockValidators(bc interop.Ledger, d *dao.Simple) (keys return vals.Copy(), nil } cache = d.GetRWCache(n.ID).(*NeoCache) - result, _, err := n.computeCommitteeMembers(bc, d) + result, _, err := n.computeCommitteeMembers(blockHeight, d) if err != nil { return nil, err } @@ -1007,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 { @@ -1019,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) diff --git a/pkg/core/native/notary.go b/pkg/core/native/notary.go index fadf8a372..2d5ab29ec 100644 --- a/pkg/core/native/notary.go +++ b/pkg/core/native/notary.go @@ -223,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 { @@ -266,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) @@ -302,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) @@ -416,8 +416,8 @@ 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")