Skip to content

Commit

Permalink
Merge pull request #2370 from nspcc-dev/nonzero-blockchain-start
Browse files Browse the repository at this point in the history
core: add tests for non-zero blockchain start
  • Loading branch information
roman-khimov committed Mar 1, 2022
2 parents 26b76ed + 49c995e commit 473d11d
Show file tree
Hide file tree
Showing 4 changed files with 287 additions and 35 deletions.
21 changes: 12 additions & 9 deletions pkg/core/blockchain.go
Original file line number Diff line number Diff line change
Expand Up @@ -332,22 +332,22 @@ func (bc *Blockchain) init() error {
return bc.storeBlock(genesisBlock, nil)
}
if ver.Value != version {
return fmt.Errorf("storage version mismatch betweeen %s and %s", version, ver.Value)
return fmt.Errorf("storage version mismatch (expected=%s, actual=%s)", version, ver.Value)
}
if ver.StateRootInHeader != bc.config.StateRootInHeader {
return fmt.Errorf("StateRootInHeader setting mismatch (config=%t, db=%t)",
ver.StateRootInHeader, bc.config.StateRootInHeader)
bc.config.StateRootInHeader, ver.StateRootInHeader)
}
if ver.P2PSigExtensions != bc.config.P2PSigExtensions {
return fmt.Errorf("P2PSigExtensions setting mismatch (old=%t, new=%t",
return fmt.Errorf("P2PSigExtensions setting mismatch (old=%t, new=%t)",
ver.P2PSigExtensions, bc.config.P2PSigExtensions)
}
if ver.P2PStateExchangeExtensions != bc.config.P2PStateExchangeExtensions {
return fmt.Errorf("P2PStateExchangeExtensions setting mismatch (old=%t, new=%t",
return fmt.Errorf("P2PStateExchangeExtensions setting mismatch (old=%t, new=%t)",
ver.P2PStateExchangeExtensions, bc.config.P2PStateExchangeExtensions)
}
if ver.KeepOnlyLatestState != bc.config.KeepOnlyLatestState {
return fmt.Errorf("KeepOnlyLatestState setting mismatch: old=%v, new=%v",
return fmt.Errorf("KeepOnlyLatestState setting mismatch (old=%v, new=%v)",
ver.KeepOnlyLatestState, bc.config.KeepOnlyLatestState)
}
bc.dao.Version = ver
Expand All @@ -367,7 +367,7 @@ func (bc *Blockchain) init() error {

currHeaderHeight, currHeaderHash, err := bc.dao.GetCurrentHeaderHeight()
if err != nil {
return err
return fmt.Errorf("failed to retrieve current header info: %w", err)
}
if bc.storedHeaderCount == 0 && currHeaderHeight == 0 {
bc.headerHashes = append(bc.headerHashes, currHeaderHash)
Expand Down Expand Up @@ -425,7 +425,7 @@ func (bc *Blockchain) init() error {

bHeight, err := bc.dao.GetCurrentBlockHeight()
if err != nil {
return err
return fmt.Errorf("failed to retrieve current block height: %w", err)
}
bc.blockHeight = bHeight
bc.persistedHeight = bHeight
Expand All @@ -448,13 +448,16 @@ func (bc *Blockchain) init() error {
// contract state from DAO via high-level bc API.
for _, c := range bc.contracts.Contracts {
md := c.Metadata()
storedCS := bc.GetContractState(md.Hash)
history := md.UpdateHistory
if len(history) == 0 || history[0] > bHeight {
if storedCS != nil {
return fmt.Errorf("native contract %s is already stored, but marked as inactive for height %d in config", md.Name, bHeight)
}
continue
}
storedCS := bc.GetContractState(md.Hash)
if storedCS == nil {
return fmt.Errorf("native contract %s is not stored", md.Name)
return fmt.Errorf("native contract %s is not stored, but should be active at height %d according to config", md.Name, bHeight)
}
storedCSBytes, err := stackitem.SerializeConvertible(storedCS)
if err != nil {
Expand Down
249 changes: 240 additions & 9 deletions pkg/core/blockchain_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -1578,7 +1580,7 @@ func TestDumpAndRestore(t *testing.T) {
})
t.Run("with state root", func(t *testing.T) {
testDumpAndRestore(t, func(c *config.Config) {
c.ProtocolConfiguration.StateRootInHeader = false
c.ProtocolConfiguration.StateRootInHeader = true
}, nil)
})
t.Run("remove untraceable", func(t *testing.T) {
Expand Down Expand Up @@ -1834,6 +1836,7 @@ func TestBlockchain_InitWithIncompleteStateJump(t *testing.T) {
c.ProtocolConfiguration.P2PStateExchangeExtensions = true
c.ProtocolConfiguration.StateSyncInterval = stateSyncInterval
c.ProtocolConfiguration.MaxTraceableBlocks = maxTraceable
c.ProtocolConfiguration.KeepOnlyLatestState = true
}
bcSpout := newTestChainWithCustomCfg(t, spountCfg)
initBasicChain(t, bcSpout)
Expand Down Expand Up @@ -1865,14 +1868,15 @@ func TestBlockchain_InitWithIncompleteStateJump(t *testing.T) {
_, err := batch.Persist()
require.NoError(t, err)

checkNewBlockchainErr := func(t *testing.T, cfg func(c *config.Config), store storage.Store, shouldFail bool) {
checkNewBlockchainErr := func(t *testing.T, cfg func(c *config.Config), store storage.Store, errText string) {
unitTestNetCfg, err := config.Load("../../config", testchain.Network())
require.NoError(t, err)
cfg(&unitTestNetCfg)
log := zaptest.NewLogger(t)
_, err = NewBlockchain(store, unitTestNetCfg.ProtocolConfiguration, log)
if shouldFail {
if len(errText) != 0 {
require.Error(t, err)
require.True(t, strings.Contains(err.Error(), errText))
} else {
require.NoError(t, err)
}
Expand All @@ -1888,31 +1892,34 @@ func TestBlockchain_InitWithIncompleteStateJump(t *testing.T) {
checkNewBlockchainErr(t, func(c *config.Config) {
boltCfg(c)
c.ProtocolConfiguration.RemoveUntraceableBlocks = false
}, bcSpout.dao.Store, true)
}, bcSpout.dao.Store, "state jump was not completed, but P2PStateExchangeExtensions are disabled or archival node capability is on")
})
t.Run("invalid state jump stage format", func(t *testing.T) {
bcSpout.dao.Store.Put(bPrefix, []byte{0x01, 0x02})
checkNewBlockchainErr(t, boltCfg, bcSpout.dao.Store, true)
checkNewBlockchainErr(t, boltCfg, bcSpout.dao.Store, "invalid state jump stage format")
})
t.Run("missing state sync point", func(t *testing.T) {
bcSpout.dao.Store.Put(bPrefix, []byte{byte(stateJumpStarted)})
checkNewBlockchainErr(t, boltCfg, bcSpout.dao.Store, true)
checkNewBlockchainErr(t, boltCfg, bcSpout.dao.Store, "failed to get state sync point from the storage")
})
t.Run("invalid state sync point", func(t *testing.T) {
bcSpout.dao.Store.Put(bPrefix, []byte{byte(stateJumpStarted)})
point := make([]byte, 4)
binary.LittleEndian.PutUint32(point, uint32(len(bcSpout.headerHashes)))
bcSpout.dao.Store.Put([]byte{byte(storage.SYSStateSyncPoint)}, point)
checkNewBlockchainErr(t, boltCfg, bcSpout.dao.Store, true)
checkNewBlockchainErr(t, boltCfg, bcSpout.dao.Store, "invalid state sync point")
})
for _, stage := range []stateJumpStage{stateJumpStarted, newStorageItemsAdded, genesisStateRemoved, 0x03} {
t.Run(fmt.Sprintf("state jump stage %d", stage), func(t *testing.T) {
bcSpout.dao.Store.Put(bPrefix, []byte{byte(stage)})
point := make([]byte, 4)
binary.LittleEndian.PutUint32(point, uint32(stateSyncPoint))
bcSpout.dao.Store.Put([]byte{byte(storage.SYSStateSyncPoint)}, point)
shouldFail := stage == 0x03 // unknown stage
checkNewBlockchainErr(t, spountCfg, bcSpout.dao.Store, shouldFail)
var errText string
if stage == 0x03 {
errText = "unknown state jump stage"
}
checkNewBlockchainErr(t, spountCfg, bcSpout.dao.Store, errText)
})
}
}
Expand Down Expand Up @@ -1998,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)
})
}
Loading

0 comments on commit 473d11d

Please sign in to comment.