diff --git a/pkg/core/blockchain_test.go b/pkg/core/blockchain_test.go index 7bc8a9560..fd4bfb7d0 100644 --- a/pkg/core/blockchain_test.go +++ b/pkg/core/blockchain_test.go @@ -17,10 +17,12 @@ import ( "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/chaindump" + "github.com/nspcc-dev/neo-go/pkg/core/dao" "github.com/nspcc-dev/neo-go/pkg/core/fee" "github.com/nspcc-dev/neo-go/pkg/core/interop/interopnames" "github.com/nspcc-dev/neo-go/pkg/core/mempool" "github.com/nspcc-dev/neo-go/pkg/core/native" + "github.com/nspcc-dev/neo-go/pkg/core/native/nativenames" "github.com/nspcc-dev/neo-go/pkg/core/native/nativeprices" "github.com/nspcc-dev/neo-go/pkg/core/native/noderoles" "github.com/nspcc-dev/neo-go/pkg/core/state" @@ -2003,3 +2005,227 @@ func setSigner(tx *transaction.Transaction, h util.Uint160) { Scopes: transaction.Global, }} } + +func TestBlockchain_StartFromExistingDB(t *testing.T) { + ps, path := newLevelDBForTestingWithPath(t, "") + customConfig := func(c *config.Config) { + c.ProtocolConfiguration.StateRootInHeader = true // Need for P2PStateExchangeExtensions check. + } + bc := initTestChain(t, ps, customConfig) + go bc.Run() + initBasicChain(t, bc) + require.True(t, bc.BlockHeight() > 5, "ensure that basic chain is correctly initialised") + + // Information for further tests. + h := bc.BlockHeight() + cryptoLibHash, err := bc.GetNativeContractScriptHash(nativenames.CryptoLib) + require.NoError(t, err) + cryptoLibState := bc.GetContractState(cryptoLibHash) + require.NotNil(t, cryptoLibState) + var ( + managementID = -1 + managementContractPrefix = 8 + ) + + bc.Close() // Ensure persist is done and persistent store is properly closed. + + newPS := func(t *testing.T) storage.Store { + ps, _ = newLevelDBForTestingWithPath(t, path) + t.Cleanup(func() { require.NoError(t, ps.Close()) }) + return ps + } + t.Run("mismatch storage version", func(t *testing.T) { + ps = newPS(t) + cache := storage.NewMemCachedStore(ps) // Extra wrapper to avoid good DB corruption. + d := dao.NewSimple(cache, bc.config.StateRootInHeader, bc.config.P2PStateExchangeExtensions) + d.PutVersion(dao.Version{ + Value: "0.0.0", + }) + _, err := d.Persist() // Persist to `cache` wrapper. + require.NoError(t, err) + _, err = initTestChainNoCheck(t, cache, customConfig) + require.Error(t, err) + require.True(t, strings.Contains(err.Error(), "storage version mismatch")) + }) + t.Run("mismatch StateRootInHeader", func(t *testing.T) { + ps = newPS(t) + _, err := initTestChainNoCheck(t, ps, func(c *config.Config) { + customConfig(c) + c.ProtocolConfiguration.StateRootInHeader = false + }) + require.Error(t, err) + require.True(t, strings.Contains(err.Error(), "StateRootInHeader setting mismatch")) + }) + t.Run("mismatch P2PSigExtensions", func(t *testing.T) { + ps = newPS(t) + _, err := initTestChainNoCheck(t, ps, func(c *config.Config) { + customConfig(c) + c.ProtocolConfiguration.P2PSigExtensions = false + }) + require.Error(t, err) + require.True(t, strings.Contains(err.Error(), "P2PSigExtensions setting mismatch")) + }) + t.Run("mismatch P2PStateExchangeExtensions", func(t *testing.T) { + ps = newPS(t) + _, err := initTestChainNoCheck(t, ps, func(c *config.Config) { + customConfig(c) + c.ProtocolConfiguration.StateRootInHeader = true + c.ProtocolConfiguration.P2PStateExchangeExtensions = true + }) + require.Error(t, err) + require.True(t, strings.Contains(err.Error(), "P2PStateExchangeExtensions setting mismatch")) + }) + t.Run("mismatch KeepOnlyLatestState", func(t *testing.T) { + ps = newPS(t) + _, err := initTestChainNoCheck(t, ps, func(c *config.Config) { + customConfig(c) + c.ProtocolConfiguration.KeepOnlyLatestState = true + }) + require.Error(t, err) + require.True(t, strings.Contains(err.Error(), "KeepOnlyLatestState setting mismatch")) + }) + t.Run("corrupted headers", func(t *testing.T) { + ps = newPS(t) + + // Corrupt headers hashes batch. + cache := storage.NewMemCachedStore(ps) // Extra wrapper to avoid good DB corruption. + key := make([]byte, 5) + key[0] = byte(storage.IXHeaderHashList) + binary.BigEndian.PutUint32(key[1:], 1) + cache.Put(key, []byte{1, 2, 3}) + + _, err := initTestChainNoCheck(t, cache, customConfig) + require.Error(t, err) + require.True(t, strings.Contains(err.Error(), "failed to read batch of 2000")) + }) + t.Run("corrupted current header height", func(t *testing.T) { + ps = newPS(t) + + // Remove current header. + cache := storage.NewMemCachedStore(ps) // Extra wrapper to avoid good DB corruption. + cache.Delete([]byte{byte(storage.SYSCurrentHeader)}) + + _, err := initTestChainNoCheck(t, cache, customConfig) + require.Error(t, err) + require.True(t, strings.Contains(err.Error(), "failed to retrieve current header")) + }) + t.Run("missing last batch of 2000 headers and missing last header", func(t *testing.T) { + ps = newPS(t) + + // Remove latest headers hashes batch and current header. + cache := storage.NewMemCachedStore(ps) // Extra wrapper to avoid good DB corruption. + cache.Delete([]byte{byte(storage.IXHeaderHashList)}) + currHeaderInfo, err := cache.Get([]byte{byte(storage.SYSCurrentHeader)}) + require.NoError(t, err) + currHeaderHash, err := util.Uint256DecodeBytesLE(currHeaderInfo[:32]) + require.NoError(t, err) + cache.Delete(append([]byte{byte(storage.DataExecutable)}, currHeaderHash.BytesBE()...)) + + _, err = initTestChainNoCheck(t, cache, customConfig) + require.Error(t, err) + require.True(t, strings.Contains(err.Error(), "could not get header")) + }) + t.Run("missing last block", func(t *testing.T) { + ps = newPS(t) + + // Remove current block from storage. + cache := storage.NewMemCachedStore(ps) // Extra wrapper to avoid good DB corruption. + cache.Delete([]byte{byte(storage.SYSCurrentBlock)}) + + _, err := initTestChainNoCheck(t, cache, customConfig) + require.Error(t, err) + require.True(t, strings.Contains(err.Error(), "failed to retrieve current block height")) + }) + t.Run("missing last stateroot", func(t *testing.T) { + ps = newPS(t) + + // Remove latest stateroot from storage. + cache := storage.NewMemCachedStore(ps) // Extra wrapper to avoid good DB corruption. + key := make([]byte, 5) + key[0] = byte(storage.DataMPTAux) + binary.BigEndian.PutUint32(key, h) + cache.Delete(key) + + _, err := initTestChainNoCheck(t, cache, customConfig) + require.Error(t, err) + require.True(t, strings.Contains(err.Error(), "can't init MPT at height")) + }) + t.Run("failed native Management initialisation", func(t *testing.T) { + ps = newPS(t) + + // Corrupt serialised CryptoLib state. + cache := storage.NewMemCachedStore(ps) // Extra wrapper to avoid good DB corruption. + key := make([]byte, 1+4+1+20) + key[0] = byte(storage.STStorage) + binary.LittleEndian.PutUint32(key[1:], uint32(managementID)) + key[5] = byte(managementContractPrefix) + copy(key[6:], cryptoLibHash.BytesBE()) + cache.Put(key, []byte{1, 2, 3}) + + _, err := initTestChainNoCheck(t, cache, customConfig) + require.Error(t, err) + require.True(t, strings.Contains(err.Error(), "can't init cache for Management native contract")) + }) + t.Run("invalid native contract deactivation", func(t *testing.T) { + ps = newPS(t) + _, err := initTestChainNoCheck(t, ps, func(c *config.Config) { + customConfig(c) + c.ProtocolConfiguration.NativeUpdateHistories = map[string][]uint32{ + nativenames.Policy: {0}, + nativenames.Neo: {0}, + nativenames.Gas: {0}, + nativenames.Designation: {0}, + nativenames.StdLib: {0}, + nativenames.Management: {0}, + nativenames.Oracle: {0}, + nativenames.Ledger: {0}, + nativenames.Notary: {0}, + nativenames.CryptoLib: {h + 10}, + } + }) + require.Error(t, err) + require.True(t, strings.Contains(err.Error(), fmt.Sprintf("native contract %s is already stored, but marked as inactive for height %d in config", nativenames.CryptoLib, h))) + }) + t.Run("invalid native contract activation", func(t *testing.T) { + ps = newPS(t) + + // Remove CryptoLib from storage. + cache := storage.NewMemCachedStore(ps) // Extra wrapper to avoid good DB corruption. + key := make([]byte, 1+4+1+20) + key[0] = byte(storage.STStorage) + binary.LittleEndian.PutUint32(key[1:], uint32(managementID)) + key[5] = byte(managementContractPrefix) + copy(key[6:], cryptoLibHash.BytesBE()) + cache.Delete(key) + + _, err := initTestChainNoCheck(t, cache, customConfig) + require.Error(t, err) + require.True(t, strings.Contains(err.Error(), fmt.Sprintf("native contract %s is not stored, but should be active at height %d according to config", nativenames.CryptoLib, h))) + }) + t.Run("stored and autogenerated native contract's states mismatch", func(t *testing.T) { + ps = newPS(t) + + // Change stored CryptoLib state. + cache := storage.NewMemCachedStore(ps) // Extra wrapper to avoid good DB corruption. + key := make([]byte, 1+4+1+20) + key[0] = byte(storage.STStorage) + binary.LittleEndian.PutUint32(key[1:], uint32(managementID)) + key[5] = byte(managementContractPrefix) + copy(key[6:], cryptoLibHash.BytesBE()) + cs := *cryptoLibState + cs.ID = -123 + csBytes, err := stackitem.SerializeConvertible(&cs) + require.NoError(t, err) + cache.Put(key, csBytes) + + _, err = initTestChainNoCheck(t, cache, customConfig) + require.Error(t, err) + require.True(t, strings.Contains(err.Error(), fmt.Sprintf("native %s: version mismatch (stored contract state differs from autogenerated one)", nativenames.CryptoLib))) + }) + + t.Run("good", func(t *testing.T) { + ps = newPS(t) + _, err := initTestChainNoCheck(t, ps, customConfig) + require.NoError(t, err) + }) +} diff --git a/pkg/core/helper_test.go b/pkg/core/helper_test.go index 2b31e3391..927932bf3 100644 --- a/pkg/core/helper_test.go +++ b/pkg/core/helper_test.go @@ -68,24 +68,44 @@ func newTestChainWithCustomCfgAndStore(t testing.TB, st storage.Store, f func(*c } func newLevelDBForTesting(t testing.TB) storage.Store { - ldbDir := t.TempDir() - dbOptions := storage.LevelDBOptions{ - DataDirectoryPath: ldbDir, - } - newLevelStore, err := storage.NewLevelDBStore(dbOptions) - require.Nil(t, err, "NewLevelDBStore error") + newLevelStore, _ := newLevelDBForTestingWithPath(t, "") return newLevelStore } +func newLevelDBForTestingWithPath(t testing.TB, dbPath string) (storage.Store, string) { + if dbPath == "" { + dbPath = t.TempDir() + } + dbOptions := storage.LevelDBOptions{ + DataDirectoryPath: dbPath, + } + newLevelStore, err := storage.NewLevelDBStore(dbOptions) + require.Nil(t, err, "NewLevelDBStore error") + return newLevelStore, dbPath +} + func newBoltStoreForTesting(t testing.TB) storage.Store { - d := t.TempDir() - testFileName := filepath.Join(d, "test_bolt_db") - boltDBStore, err := storage.NewBoltDBStore(storage.BoltDBOptions{FilePath: testFileName}) - require.NoError(t, err) + boltDBStore, _ := newBoltStoreForTestingWithPath(t, "") return boltDBStore } +func newBoltStoreForTestingWithPath(t testing.TB, dbPath string) (storage.Store, string) { + if dbPath == "" { + d := t.TempDir() + dbPath = filepath.Join(d, "test_bolt_db") + } + boltDBStore, err := storage.NewBoltDBStore(storage.BoltDBOptions{FilePath: dbPath}) + require.NoError(t, err) + return boltDBStore, dbPath +} + func initTestChain(t testing.TB, st storage.Store, f func(*config.Config)) *Blockchain { + chain, err := initTestChainNoCheck(t, st, f) + require.NoError(t, err) + return chain +} + +func initTestChainNoCheck(t testing.TB, st storage.Store, f func(*config.Config)) (*Blockchain, error) { unitTestNetCfg, err := config.Load("../../config", testchain.Network()) require.NoError(t, err) if f != nil { @@ -98,9 +118,7 @@ func initTestChain(t testing.TB, st storage.Store, f func(*config.Config)) *Bloc if _, ok := t.(*testing.B); ok { log = zap.NewNop() } - chain, err := NewBlockchain(st, unitTestNetCfg.ProtocolConfiguration, log) - require.NoError(t, err) - return chain + return NewBlockchain(st, unitTestNetCfg.ProtocolConfiguration, log) } func (bc *Blockchain) newBlock(txs ...*transaction.Transaction) *block.Block {