Merge pull request #2994 from nspcc-dev/fix-init-cache

core: rework natives cache initialization
This commit is contained in:
Roman Khimov 2023-04-26 14:54:52 +03:00 committed by GitHub
commit 1bd22adcb3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 125 additions and 35 deletions

View file

@ -1011,28 +1011,16 @@ func (bc *Blockchain) resetStateInternal(height uint32, stage stateChangeStage)
}
func (bc *Blockchain) initializeNativeCache(blockHeight uint32, d *dao.Simple) error {
err := bc.contracts.NEO.InitializeCache(blockHeight, d)
for _, c := range bc.contracts.Contracts {
for _, h := range c.Metadata().UpdateHistory {
if blockHeight >= h { // check that contract was deployed.
err := c.InitializeCache(blockHeight, d)
if err != nil {
return fmt.Errorf("can't init cache for NEO native contract: %w", err)
return fmt.Errorf("failed to initialize cache for %s: %w", c.Metadata().Name, err)
}
err = bc.contracts.Management.InitializeCache(d)
if err != nil {
return fmt.Errorf("can't init cache for Management native contract: %w", err)
}
err = bc.contracts.Designate.InitializeCache(d)
if err != nil {
return fmt.Errorf("can't init cache for Designation native contract: %w", err)
}
bc.contracts.Oracle.InitializeCache(d)
if bc.P2PSigExtensionsEnabled() {
err = bc.contracts.Notary.InitializeCache(d)
if err != nil {
return fmt.Errorf("can't init cache for Notary native contract: %w", err)
break
}
}
err = bc.contracts.Policy.InitializeCache(d)
if err != nil {
return fmt.Errorf("can't init cache for Policy native contract: %w", err)
}
return nil
}
@ -2579,7 +2567,10 @@ func (bc *Blockchain) verifyTxAttributes(d *dao.Simple, tx *transaction.Transact
nvb := tx.Attributes[i].Value.(*transaction.NotValidBefore).Height
curHeight := bc.BlockHeight()
if isPartialTx {
maxNVBDelta := bc.contracts.Notary.GetMaxNotValidBeforeDelta(bc.dao)
maxNVBDelta, err := bc.GetMaxNotValidBeforeDelta()
if err != nil {
return fmt.Errorf("%w: failed to retrieve MaxNotValidBeforeDelta value from native Notary contract: %v", ErrInvalidAttribute, err)
}
if curHeight+maxNVBDelta < nvb {
return fmt.Errorf("%w: NotValidBefore (%d) bigger than MaxNVBDelta (%d) allows at height %d", ErrInvalidAttribute, nvb, maxNVBDelta, curHeight)
}
@ -2984,11 +2975,14 @@ func (bc *Blockchain) GetMaxVerificationGAS() int64 {
}
// GetMaxNotValidBeforeDelta returns maximum NotValidBeforeDelta Notary limit.
func (bc *Blockchain) GetMaxNotValidBeforeDelta() uint32 {
func (bc *Blockchain) GetMaxNotValidBeforeDelta() (uint32, error) {
if !bc.config.P2PSigExtensions {
panic("disallowed call to Notary")
panic("disallowed call to Notary") // critical error, thus panic.
}
return bc.contracts.Notary.GetMaxNotValidBeforeDelta(bc.dao)
if bc.contracts.Notary.Metadata().UpdateHistory[0] > bc.BlockHeight() {
return 0, fmt.Errorf("native Notary is active starting from %d", bc.contracts.Notary.Metadata().UpdateHistory[0])
}
return bc.contracts.Notary.GetMaxNotValidBeforeDelta(bc.dao), nil
}
// GetStoragePrice returns current storage price.

View file

@ -299,6 +299,69 @@ func TestBlockchain_StartFromExistingDB(t *testing.T) {
})
}
// This test enables Notary native contract at non-zero height and checks that no
// Notary cache initialization is performed before that height on node restart.
func TestBlockchain_InitializeNativeCacheWrtNativeActivations(t *testing.T) {
const notaryEnabledHeight = 3
ps, path := newLevelDBForTestingWithPath(t, "")
customConfig := func(c *config.Blockchain) {
c.P2PSigExtensions = true
c.NativeUpdateHistories = make(map[string][]uint32)
for _, n := range []string{
nativenames.Neo,
nativenames.Gas,
nativenames.Designation,
nativenames.Management,
nativenames.CryptoLib,
nativenames.Ledger,
nativenames.Management,
nativenames.Oracle,
nativenames.Policy,
nativenames.StdLib,
nativenames.Notary,
} {
if n == nativenames.Notary {
c.NativeUpdateHistories[n] = []uint32{notaryEnabledHeight}
} else {
c.NativeUpdateHistories[n] = []uint32{0}
}
}
}
bc, validators, committee, err := chain.NewMultiWithCustomConfigAndStoreNoCheck(t, customConfig, ps)
require.NoError(t, err)
go bc.Run()
e := neotest.NewExecutor(t, bc, validators, committee)
e.AddNewBlock(t)
bc.Close() // Ensure persist is done and persistent store is properly closed.
ps, _ = newLevelDBForTestingWithPath(t, path)
// If NativeActivations are not taken into account during native cache initialization,
// bs.init() will panic on Notary cache initialization as it's not deployed yet.
require.NotPanics(t, func() {
bc, _, _, err = chain.NewMultiWithCustomConfigAndStoreNoCheck(t, customConfig, ps)
require.NoError(t, err)
})
go bc.Run()
defer bc.Close()
e = neotest.NewExecutor(t, bc, validators, committee)
h := e.Chain.BlockHeight()
// Notary isn't initialized yet, so accessing Notary cache should return error.
_, err = e.Chain.GetMaxNotValidBeforeDelta()
require.Error(t, err)
// Ensure Notary will be properly initialized and accessing Notary cache works
// as expected.
for i := 0; i < notaryEnabledHeight; i++ {
require.NotPanics(t, func() {
e.AddNewBlock(t)
}, h+uint32(i)+1)
}
_, err = e.Chain.GetMaxNotValidBeforeDelta()
require.NoError(t, err)
}
func TestBlockchain_AddHeaders(t *testing.T) {
bc, acc := chain.NewSingleWithCustomConfig(t, func(c *config.Blockchain) {
c.StateRootInHeader = true
@ -1884,11 +1947,15 @@ func TestBlockchain_VerifyTx(t *testing.T) {
require.Error(t, bc.PoolTxWithData(tx, 5, mp, bc, verificationF))
})
t.Run("bad NVB: too big", func(t *testing.T) {
tx := getPartiallyFilledTx(bc.BlockHeight()+bc.GetMaxNotValidBeforeDelta()+1, bc.BlockHeight()+1)
maxNVB, err := bc.GetMaxNotValidBeforeDelta()
require.NoError(t, err)
tx := getPartiallyFilledTx(bc.BlockHeight()+maxNVB+1, bc.BlockHeight()+1)
require.True(t, errors.Is(bc.PoolTxWithData(tx, 5, mp, bc, verificationF), core.ErrInvalidAttribute))
})
t.Run("bad ValidUntilBlock: too small", func(t *testing.T) {
tx := getPartiallyFilledTx(bc.BlockHeight(), bc.BlockHeight()+bc.GetMaxNotValidBeforeDelta()+1)
maxNVB, err := bc.GetMaxNotValidBeforeDelta()
require.NoError(t, err)
tx := getPartiallyFilledTx(bc.BlockHeight(), bc.BlockHeight()+maxNVB+1)
require.True(t, errors.Is(bc.PoolTxWithData(tx, 5, mp, bc, verificationF), core.ErrInvalidAttribute))
})
t.Run("good", func(t *testing.T) {

View file

@ -153,6 +153,11 @@ type MethodAndPrice struct {
// Contract is an interface for all native contracts.
type Contract interface {
Initialize(*Context) error
// InitializeCache aimed to initialize contract's cache when the contract has
// been deployed, but in-memory cached data were lost due to the node reset.
// It should be called each time after node restart iff the contract was
// deployed and no Initialize method was called.
InitializeCache(blockHeight uint32, d *dao.Simple) error
Metadata() *ContractMD
OnPersist(*Context) error
PostPersist(*Context) error

View file

@ -10,6 +10,7 @@ import (
bls12381 "github.com/consensys/gnark-crypto/ecc/bls12-381"
"github.com/consensys/gnark-crypto/ecc/bls12-381/fr"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
"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/native/nativenames"
"github.com/nspcc-dev/neo-go/pkg/crypto/hash"
@ -467,6 +468,11 @@ func (c *Crypto) Initialize(ic *interop.Context) error {
return nil
}
// InitializeCache implements the Contract interface.
func (c *Crypto) InitializeCache(blockHeight uint32, d *dao.Simple) error {
return nil
}
// OnPersist implements the Contract interface.
func (c *Crypto) OnPersist(ic *interop.Context) error {
return nil

View file

@ -132,7 +132,7 @@ func (s *Designate) Initialize(ic *interop.Context) error {
// 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 {
func (s *Designate) InitializeCache(blockHeight uint32, d *dao.Simple) error {
cache := &DesignationCache{}
roles := []noderoles.Role{noderoles.Oracle, noderoles.NeoFSAlphabet, noderoles.StateValidator}
if s.p2pSigExtensionsEnabled {

View file

@ -85,6 +85,11 @@ func (l *Ledger) Initialize(ic *interop.Context) error {
return nil
}
// InitializeCache implements the Contract interface.
func (l *Ledger) InitializeCache(blockHeight uint32, d *dao.Simple) error {
return nil
}
// OnPersist implements the Contract interface.
func (l *Ledger) OnPersist(ic *interop.Context) error {
// Actual block/tx processing is done in Blockchain.storeBlock().

View file

@ -610,7 +610,7 @@ func (m *Management) OnPersist(ic *interop.Context) error {
// InitializeCache initializes contract cache with the proper values from storage.
// Cache initialization should be done apart from Initialize because Initialize is
// called only when deploying native contracts.
func (m *Management) InitializeCache(d *dao.Simple) error {
func (m *Management) InitializeCache(blockHeight uint32, d *dao.Simple) error {
cache := &ManagementCache{
contracts: make(map[util.Uint160]*state.Contract),
nep11: make(map[util.Uint160]struct{}),

View file

@ -82,7 +82,7 @@ func TestManagement_Initialize(t *testing.T) {
t.Run("good", func(t *testing.T) {
d := dao.NewSimple(storage.NewMemoryStore(), false, false)
mgmt := newManagement()
require.NoError(t, mgmt.InitializeCache(d))
require.NoError(t, mgmt.InitializeCache(0, d))
})
/* See #2801
t.Run("invalid contract state", func(t *testing.T) {
@ -101,7 +101,7 @@ func TestManagement_GetNEP17Contracts(t *testing.T) {
err := mgmt.Initialize(&interop.Context{DAO: d})
require.NoError(t, err)
require.NoError(t, mgmt.Policy.Initialize(&interop.Context{DAO: d}))
err = mgmt.InitializeCache(d)
err = mgmt.InitializeCache(0, d)
require.NoError(t, err)
require.Empty(t, mgmt.GetNEP17Contracts(d))

View file

@ -99,6 +99,11 @@ func (g *GAS) Initialize(ic *interop.Context) error {
return nil
}
// InitializeCache implements the Contract interface.
func (g *GAS) InitializeCache(blockHeight uint32, d *dao.Simple) error {
return nil
}
// OnPersist implements the Contract interface.
func (g *GAS) OnPersist(ic *interop.Context) error {
if len(ic.Block.Transactions) == 0 {

View file

@ -305,7 +305,8 @@ func (n *NEO) Initialize(ic *interop.Context) error {
// InitializeCache initializes all NEO cache with the proper values from the storage.
// Cache initialization should be done apart from Initialize because Initialize is
// called only when deploying native contracts.
// called only when deploying native contracts. InitializeCache implements the Contract
// interface.
func (n *NEO) InitializeCache(blockHeight uint32, d *dao.Simple) error {
cache := &NeoCache{
gasPerVoteCache: make(map[string]big.Int),

View file

@ -152,7 +152,7 @@ func (n *Notary) Initialize(ic *interop.Context) error {
return nil
}
func (n *Notary) InitializeCache(d *dao.Simple) error {
func (n *Notary) InitializeCache(blockHeight uint32, d *dao.Simple) error {
cache := &NotaryCache{
maxNotValidBeforeDelta: uint32(getIntWithKey(n.ID, d, maxNotValidBeforeDeltaKey)),
notaryServiceFeePerKey: getIntWithKey(n.ID, d, notaryServiceFeeKey),

View file

@ -251,10 +251,11 @@ func (o *Oracle) Initialize(ic *interop.Context) error {
return nil
}
func (o *Oracle) InitializeCache(d *dao.Simple) {
func (o *Oracle) InitializeCache(blockHeight uint32, d *dao.Simple) error {
cache := &OracleCache{}
cache.requestPrice = getIntWithKey(o.ID, d, prefixRequestPrice)
d.SetCache(o.ID, cache)
return nil
}
func getResponse(tx *transaction.Transaction) *transaction.OracleResponse {

View file

@ -153,7 +153,7 @@ func (p *Policy) Initialize(ic *interop.Context) error {
return nil
}
func (p *Policy) InitializeCache(d *dao.Simple) error {
func (p *Policy) InitializeCache(blockHeight uint32, d *dao.Simple) error {
cache := &PolicyCache{}
err := p.fillCacheFromDAO(cache, d)
if err != nil {

View file

@ -9,6 +9,7 @@ import (
"strings"
"github.com/mr-tron/base58"
"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/native/nativenames"
base58neogo "github.com/nspcc-dev/neo-go/pkg/encoding/base58"
@ -429,6 +430,11 @@ func (s *Std) Initialize(ic *interop.Context) error {
return nil
}
// InitializeCache implements the Contract interface.
func (s *Std) InitializeCache(blockHeight uint32, d *dao.Simple) error {
return nil
}
// OnPersist implements the Contract interface.
func (s *Std) OnPersist(ic *interop.Context) error {
return nil