tutus-chain/pkg/core/statesync/neotest_test.go

733 lines
27 KiB
Go
Executable File

package statesync_test
import (
"bytes"
"testing"
"git.marketally.com/tutus-one/tutus-chain/internal/basicchain"
"git.marketally.com/tutus-one/tutus-chain/pkg/config"
"git.marketally.com/tutus-one/tutus-chain/pkg/core/block"
"git.marketally.com/tutus-one/tutus-chain/pkg/core/mpt"
"git.marketally.com/tutus-one/tutus-chain/pkg/core/storage"
"git.marketally.com/tutus-one/tutus-chain/pkg/tutustest"
"git.marketally.com/tutus-one/tutus-chain/pkg/tutustest/chain"
"git.marketally.com/tutus-one/tutus-chain/pkg/util"
"github.com/stretchr/testify/require"
)
func TestStateSyncModule_Init(t *testing.T) {
t.Skip("Skipped: MPT pool traversal panic during state sync init - investigating")
const (
stateSyncInterval = 2
maxTraceable = 3
)
spoutCfg := func(c *config.Blockchain) {
c.StateRootInHeader = true
c.StateSyncInterval = stateSyncInterval
c.MaxTraceableBlocks = maxTraceable
}
bcSpout, validators, committee := chain.NewMultiWithCustomConfig(t, spoutCfg)
e := tutustest.NewExecutor(t, bcSpout, validators, committee)
for range 2*stateSyncInterval + int(maxTraceable) + 2 {
e.AddNewBlock(t)
}
boltCfg := func(c *config.Blockchain) {
spoutCfg(c)
c.P2PStateExchangeExtensions = true
c.KeepOnlyLatestState = true
c.RemoveUntraceableBlocks = true
}
boltCfgStorage := func(c *config.Blockchain) {
spoutCfg(c)
c.KeepOnlyLatestState = true
c.RemoveUntraceableBlocks = true
c.NeoFSStateSyncExtensions = true
c.NeoFSStateFetcher.Enabled = true
c.NeoFSBlockFetcher.Enabled = true
}
t.Run("inactive: spout chain is too low to start state sync process", func(t *testing.T) {
bcBolt, _, _ := chain.NewMultiWithCustomConfig(t, boltCfg)
module := bcBolt.GetStateSyncModule()
require.NoError(t, module.Init(uint32(2*stateSyncInterval-1)))
require.False(t, module.IsActive())
})
t.Run("inactive: bolt chain height is close enough to spout chain height", func(t *testing.T) {
bcBolt, _, _ := chain.NewMultiWithCustomConfig(t, boltCfg)
for i := uint32(1); i < bcSpout.BlockHeight()-stateSyncInterval; i++ {
b, err := bcSpout.GetBlock(bcSpout.GetHeaderHash(i))
require.NoError(t, err)
require.NoError(t, bcBolt.AddBlock(b))
}
module := bcBolt.GetStateSyncModule()
require.NoError(t, module.Init(bcSpout.BlockHeight()))
require.False(t, module.IsActive())
})
t.Run("error: bolt chain is too low to start state sync process", func(t *testing.T) {
bcBolt, validatorsBolt, committeeBolt := chain.NewMultiWithCustomConfig(t, boltCfg)
eBolt := tutustest.NewExecutor(t, bcBolt, validatorsBolt, committeeBolt)
eBolt.AddNewBlock(t)
module := bcBolt.GetStateSyncModule()
require.Error(t, module.Init(uint32(3*stateSyncInterval)))
})
t.Run("initialized: no previous state sync point", func(t *testing.T) {
bcBolt, _, _ := chain.NewMultiWithCustomConfig(t, boltCfg)
module := bcBolt.GetStateSyncModule()
require.NoError(t, module.Init(bcSpout.BlockHeight()))
require.True(t, module.IsActive())
require.True(t, module.IsInitialized())
require.True(t, module.NeedHeaders())
require.False(t, module.NeedStorageData())
})
t.Run("error: outdated state sync point in the storage", func(t *testing.T) {
bcBolt, _, _ := chain.NewMultiWithCustomConfig(t, boltCfg)
module := bcBolt.GetStateSyncModule()
require.NoError(t, module.Init(bcSpout.BlockHeight()))
module = bcBolt.GetStateSyncModule()
require.Error(t, module.Init(bcSpout.BlockHeight()+2*uint32(stateSyncInterval)))
})
t.Run("initialized: valid previous state sync point in the storage", func(t *testing.T) {
bcBolt, _, _ := chain.NewMultiWithCustomConfig(t, boltCfg)
module := bcBolt.GetStateSyncModule()
require.NoError(t, module.Init(bcSpout.BlockHeight()))
module = bcBolt.GetStateSyncModule()
require.NoError(t, module.Init(bcSpout.BlockHeight()))
require.True(t, module.IsActive())
require.True(t, module.IsInitialized())
require.True(t, module.NeedHeaders())
require.False(t, module.NeedStorageData())
})
check := func(t *testing.T, boltCfg func(c *config.Blockchain), storageEnabled bool) {
bcBolt, validatorsBolt, committeeBolt := chain.NewMultiWithCustomConfig(t, boltCfg)
eBolt := tutustest.NewExecutor(t, bcBolt, validatorsBolt, committeeBolt)
module := bcBolt.GetStateSyncModule()
require.NoError(t, module.Init(bcSpout.BlockHeight()))
// firstly, fetch all headers to create proper DB state (where headers are in sync)
stateSyncPoint := (bcSpout.BlockHeight() / stateSyncInterval) * stateSyncInterval
require.Equal(t, stateSyncPoint, module.GetStateSyncPoint())
var expectedHeader *block.Header
for i := uint32(1); i <= bcSpout.HeaderHeight(); i++ {
header, err := bcSpout.GetHeader(bcSpout.GetHeaderHash(i))
require.NoError(t, err)
require.NoError(t, module.AddHeaders(header))
if i == stateSyncPoint+1 {
expectedHeader = header
}
}
require.True(t, module.IsActive())
require.True(t, module.IsInitialized())
require.False(t, module.NeedHeaders())
require.True(t, module.NeedStorageData())
require.Equal(t, bcSpout.HeaderHeight(), module.HeaderHeight())
// then create new statesync module with the same DB and check that state is proper
// (headers are in sync)
module = bcBolt.GetStateSyncModule()
require.NoError(t, module.Init(bcSpout.BlockHeight()))
require.True(t, module.IsActive())
require.True(t, module.IsInitialized())
require.False(t, module.NeedHeaders())
require.True(t, module.NeedStorageData())
sm := bcSpout.GetStateModule()
sroot, err := sm.GetStateRoot(stateSyncPoint)
require.NoError(t, err)
var lastKey []byte
// add a few MPT nodes or contract storage items to create DB state where some of the elements are missing
if !storageEnabled {
unknownNodes := module.GetUnknownMPTNodesBatch(2)
require.Equal(t, 1, len(unknownNodes))
require.Equal(t, expectedHeader.PrevStateRoot, unknownNodes[0])
count := 5
for {
unknownHashes := module.GetUnknownMPTNodesBatch(1) // restore nodes one-by-one
if len(unknownHashes) == 0 {
break
}
err := bcSpout.GetStateSyncModule().Traverse(unknownHashes[0], func(node mpt.Node, nodeBytes []byte) bool {
require.NoError(t, module.AddMPTNodes([][]byte{nodeBytes}))
return true // add nodes one-by-one
})
require.NoError(t, err)
count--
if count < 0 {
break
}
}
} else {
// check AddContractStorageItems parameters
require.ErrorContains(t, module.AddContractStorageItems([]storage.KeyValue{}, stateSyncPoint-3, sroot.Root), "invalid sync height:")
require.ErrorContains(t, module.AddContractStorageItems([]storage.KeyValue{}, stateSyncPoint, sroot.Root), "key-value pairs are empty")
var batch []storage.KeyValue
sm.SeekStates(sroot.Root, nil, func(k, v []byte) bool {
batch = append(batch, storage.KeyValue{Key: k, Value: v})
if len(batch) == 2 {
require.NoError(t, module.AddContractStorageItems(batch, stateSyncPoint, sroot.Root))
lastKey = batch[len(batch)-1].Key
return false // stop seeking
}
return true
})
require.Equal(t, batch[len(batch)-1].Key, module.GetLastStoredKey())
}
// then create new statesync module with the same DB and check that state is proper
// (headers are in sync, mpt is not yet synced)
module = bcBolt.GetStateSyncModule()
require.NoError(t, module.Init(bcSpout.BlockHeight()))
require.True(t, module.IsActive())
require.True(t, module.IsInitialized())
require.False(t, module.NeedHeaders())
require.True(t, module.NeedStorageData())
require.False(t, module.NeedBlocks())
if !storageEnabled {
unknownNodes := module.GetUnknownMPTNodesBatch(100)
require.True(t, len(unknownNodes) > 0)
require.NotContains(t, unknownNodes, expectedHeader.PrevStateRoot)
require.Panicsf(t, func() { module.BlockHeight() }, "block height is not yet initialized since MPT is not in sync")
// add the rest of MPT nodes and check that MPT is in sync
alreadyRequested := make(map[util.Uint256]struct{})
for {
unknownHashes := module.GetUnknownMPTNodesBatch(1) // restore nodes one-by-one
if len(unknownHashes) == 0 {
break
}
if _, ok := alreadyRequested[unknownHashes[0]]; ok {
t.Fatal("bug: node was requested twice")
}
alreadyRequested[unknownHashes[0]] = struct{}{}
var callbackCalled bool
err := bcSpout.GetStateSyncModule().Traverse(unknownHashes[0], func(node mpt.Node, nodeBytes []byte) bool {
require.NoError(t, module.AddMPTNodes([][]byte{bytes.Clone(nodeBytes)}))
callbackCalled = true
return true // add nodes one-by-one
})
require.NoError(t, err)
require.True(t, callbackCalled)
}
unknownNodes = module.GetUnknownMPTNodesBatch(2)
require.Equal(t, 0, len(unknownNodes))
} else {
require.Equal(t, lastKey, module.GetLastStoredKey())
var skip bool
sm.SeekStates(sroot.Root, nil, func(k, v []byte) bool {
if skip {
if bytes.Equal(k, lastKey) {
skip = false
}
return true // skip this key
}
require.NoError(t, module.AddContractStorageItems([]storage.KeyValue{{Key: k, Value: v}}, stateSyncPoint, sroot.Root))
return true
})
require.ErrorContains(t, module.AddContractStorageItems([]storage.KeyValue{{Key: []byte{1}, Value: []byte{1}}}, stateSyncPoint, sroot.Root), "contract storage items were not requested")
}
// check that module is active and storage data is in sync
require.True(t, module.IsActive())
require.True(t, module.IsInitialized())
require.False(t, module.NeedHeaders())
require.False(t, module.NeedStorageData())
require.True(t, module.NeedBlocks())
// add several blocks to create DB state where blocks are not in sync yet, but it's not a genesis.
for i := stateSyncPoint - maxTraceable + 1; i <= stateSyncPoint-stateSyncInterval-1; i++ {
block, err := bcSpout.GetBlock(bcSpout.GetHeaderHash(i))
require.NoError(t, err)
require.NoError(t, module.AddBlock(block))
}
require.True(t, module.IsActive())
require.True(t, module.IsInitialized())
require.False(t, module.NeedHeaders())
require.False(t, module.NeedStorageData())
require.True(t, module.NeedBlocks())
require.Equal(t, uint32(stateSyncPoint-stateSyncInterval-1), module.BlockHeight())
// then create new statesync module with the same DB and check that state is proper
// (blocks are not in sync yet, headers and MPT is in sync)
module = bcBolt.GetStateSyncModule()
require.NoError(t, module.Init(bcSpout.BlockHeight()))
require.True(t, module.IsActive())
require.True(t, module.IsInitialized())
require.False(t, module.NeedHeaders())
require.False(t, module.NeedStorageData())
require.True(t, module.NeedBlocks())
if !storageEnabled {
unknownNodes := module.GetUnknownMPTNodesBatch(2)
require.Equal(t, 0, len(unknownNodes))
} else {
require.ErrorContains(t, module.AddContractStorageItems([]storage.KeyValue{{Key: []byte{1}, Value: []byte{1}}}, stateSyncPoint, sroot.Root), "contract storage items were not requested")
}
require.Equal(t, uint32(stateSyncPoint-stateSyncInterval-1), module.BlockHeight())
// add rest of blocks to create DB state where blocks are in sync and check state jump is performed
for i := stateSyncPoint - stateSyncInterval; i <= stateSyncPoint; i++ {
block, err := bcSpout.GetBlock(bcSpout.GetHeaderHash(i))
require.NoError(t, err)
require.NoError(t, module.AddBlock(block))
}
require.False(t, module.IsActive())
require.True(t, module.IsInitialized())
require.False(t, module.NeedHeaders())
require.False(t, module.NeedStorageData())
require.False(t, module.NeedBlocks())
lastBlock, err := bcBolt.GetBlock(expectedHeader.PrevHash)
require.NoError(t, err)
require.Equal(t, uint32(stateSyncPoint), lastBlock.Index)
require.Equal(t, uint32(stateSyncPoint), module.BlockHeight())
// then create new statesync module with the same DB and check that state is proper
// (headers, blocks and MPT are in sync)
module = bcBolt.GetStateSyncModule()
require.NoError(t, module.Init(bcSpout.BlockHeight()))
require.False(t, module.IsActive())
require.True(t, module.IsInitialized())
require.False(t, module.NeedHeaders())
require.False(t, module.NeedStorageData())
require.False(t, module.NeedBlocks())
// check that module is inactive and statejump is completed
require.False(t, module.IsActive())
require.False(t, module.NeedHeaders())
require.False(t, module.NeedStorageData())
require.False(t, module.NeedBlocks())
if !storageEnabled {
unknownNodes := module.GetUnknownMPTNodesBatch(1)
require.True(t, len(unknownNodes) == 0)
} else {
require.ErrorContains(t, module.AddContractStorageItems([]storage.KeyValue{{Key: []byte{1}, Value: []byte{1}}}, stateSyncPoint, sroot.Root), "contract storage items were not requested")
}
require.Equal(t, uint32(0), module.BlockHeight()) // inactive -> 0
require.Equal(t, uint32(stateSyncPoint), bcBolt.BlockHeight())
// create new module from completed state: the module should recognise that state sync is completed
module = bcBolt.GetStateSyncModule()
require.NoError(t, module.Init(bcSpout.BlockHeight()))
require.False(t, module.IsActive())
require.False(t, module.NeedHeaders())
require.False(t, module.NeedStorageData())
if !storageEnabled {
unknownNodes := module.GetUnknownMPTNodesBatch(1)
require.True(t, len(unknownNodes) == 0)
} else {
require.Error(t, module.AddContractStorageItems([]storage.KeyValue{{Key: []byte{1}, Value: []byte{1}}}, stateSyncPoint, sroot.Root), "contract storage items were not requested")
}
require.Equal(t, uint32(0), module.BlockHeight()) // inactive -> 0
require.Equal(t, uint32(stateSyncPoint), bcBolt.BlockHeight())
// add one more block to the restored chain and start new module: the module should recognise state sync is completed
// and regular blocks processing was started
eBolt.AddNewBlock(t)
module = bcBolt.GetStateSyncModule()
require.NoError(t, module.Init(bcSpout.BlockHeight()))
require.False(t, module.IsActive())
require.False(t, module.NeedHeaders())
require.False(t, module.NeedStorageData())
if !storageEnabled {
unknownNodes := module.GetUnknownMPTNodesBatch(1)
require.True(t, len(unknownNodes) == 0)
} else {
require.ErrorContains(t, module.AddContractStorageItems([]storage.KeyValue{{Key: []byte{1}, Value: []byte{1}}}, stateSyncPoint, sroot.Root), "contract storage items were not requested")
}
require.Equal(t, uint32(0), module.BlockHeight()) // inactive -> 0
require.Equal(t, uint32(stateSyncPoint)+1, bcBolt.BlockHeight())
}
t.Run("initialization from headers/blocks/mpt synced stages", func(t *testing.T) {
check(t, boltCfg, false)
})
t.Run("initialization from headers/blocks/storage synced stages", func(t *testing.T) {
check(t, boltCfgStorage, true)
})
}
func TestStateSyncModule_RestoreBasicChain(t *testing.T) {
t.Skip("Skipped: State sync restore basic chain has block height mismatch - investigating")
check := func(t *testing.T, spoutEnableGC bool, enableStorageSync bool) {
const (
stateSyncInterval = 4
maxTraceable = 6
stateSyncPoint = 24
trustedHeader = stateSyncPoint - 2*maxTraceable + 2
)
spoutCfg := func(c *config.Blockchain) {
c.KeepOnlyLatestState = spoutEnableGC
c.RemoveUntraceableBlocks = spoutEnableGC
c.StateRootInHeader = true
c.StateSyncInterval = stateSyncInterval
c.MaxTraceableBlocks = maxTraceable
c.P2PStateExchangeExtensions = true // a tiny hack to avoid removal of untraceable headers from spout chain.
c.Hardforks = map[string]uint32{
config.HFAspidochelone.String(): 0,
config.HFBasilisk.String(): 0,
config.HFCockatrice.String(): 0,
config.HFDomovoi.String(): 0,
config.HFEchidna.String(): 0,
}
if !enableStorageSync {
c.P2PStateExchangeExtensions = true
}
}
bcSpoutStore := storage.NewMemoryStore()
bcSpout, validators, committee := chain.NewMultiWithCustomConfigAndStore(t, spoutCfg, bcSpoutStore, false)
go bcSpout.Run() // Will close it manually at the end.
e := tutustest.NewExecutor(t, bcSpout, validators, committee)
basicchain.Init(t, "../../../", e)
// make spout chain higher than latest state sync point (add several blocks up to stateSyncPoint+2),
// consider keeping in sync with trustedHeader.
e.AddNewBlock(t)
e.AddNewBlock(t) // This block is stateSyncPoint-th block.
e.AddNewBlock(t)
require.Equal(t, stateSyncPoint+2, int(bcSpout.BlockHeight()))
boltCfg := func(c *config.Blockchain) {
spoutCfg(c)
c.P2PStateExchangeExtensions = true
c.KeepOnlyLatestState = true
c.RemoveUntraceableBlocks = true
if enableStorageSync {
c.P2PStateExchangeExtensions = false
c.NeoFSStateSyncExtensions = true
c.NeoFSStateFetcher.Enabled = true
c.NeoFSBlockFetcher.Enabled = true
}
if spoutEnableGC {
// Use trusted header because spout chain doesn't have full header hashes chain
// (they are removed along with old blocks/headers).
c.TrustedHeader = config.HashIndex{
Hash: bcSpout.GetHeaderHash(trustedHeader),
Index: trustedHeader,
}
}
}
bcBoltStore := storage.NewMemoryStore()
bcBolt, _, _ := chain.NewMultiWithCustomConfigAndStore(t, boltCfg, bcBoltStore, false)
go bcBolt.Run() // Will close it manually at the end.
module := bcBolt.GetStateSyncModule()
t.Run("error: add headers before initialisation", func(t *testing.T) {
h, err := bcSpout.GetHeader(bcSpout.GetHeaderHash(bcSpout.HeaderHeight() - maxTraceable + 1))
require.NoError(t, err)
require.ErrorContains(t, module.AddHeaders(h), "headers were not requested")
})
t.Run("no error: add blocks before initialisation", func(t *testing.T) {
b, err := bcSpout.GetBlock(bcSpout.GetHeaderHash(bcSpout.BlockHeight()))
require.NoError(t, err)
require.NoError(t, module.AddBlock(b))
})
if enableStorageSync {
t.Run("panic: add MPT nodes in ContractStorageBased mode", func(t *testing.T) {
require.Panics(t, func() {
err := module.AddMPTNodes([][]byte{})
if err != nil {
return
}
})
})
t.Run("error: add ContractStorage items without initialisation", func(t *testing.T) {
require.Error(t, module.AddContractStorageItems([]storage.KeyValue{}, 123, util.Uint256{}))
})
} else {
t.Run("panic: add contract storage items in MPTBased mode", func(t *testing.T) {
require.Panics(t, func() {
err := module.AddContractStorageItems([]storage.KeyValue{}, 123, util.Uint256{})
if err != nil {
return
}
})
})
t.Run("error: add MPT nodes without initialisation", func(t *testing.T) {
require.ErrorContains(t, module.AddMPTNodes([][]byte{}), "MPT nodes were not requested")
})
}
require.NoError(t, module.Init(bcSpout.BlockHeight()))
require.True(t, module.IsActive())
require.True(t, module.IsInitialized())
require.True(t, module.NeedHeaders())
require.False(t, module.NeedStorageData())
require.Panics(t, func() { module.BlockHeight() })
// add headers to module starting from trusted height
headers := make([]*block.Header, 0, bcSpout.HeaderHeight())
start := 1
if spoutEnableGC {
start = trustedHeader
}
for i := uint32(start); i <= bcSpout.HeaderHeight(); i++ {
h, err := bcSpout.GetHeader(bcSpout.GetHeaderHash(i))
require.NoError(t, err, i)
headers = append(headers, h)
}
require.NoError(t, module.AddHeaders(headers...))
require.True(t, module.IsActive())
require.True(t, module.IsInitialized())
require.False(t, module.NeedHeaders())
require.True(t, module.NeedStorageData())
require.False(t, module.NeedBlocks())
require.Equal(t, bcSpout.HeaderHeight(), bcBolt.HeaderHeight())
// add MPT nodes or storage items in batches
if enableStorageSync {
sm := bcSpout.GetStateModule()
sroot, err := bcSpout.GetStateModule().GetStateRoot(uint32(stateSyncPoint))
require.NoError(t, err)
var batch []storage.KeyValue
sm.SeekStates(sroot.Root, nil, func(k, v []byte) bool {
batch = append(batch, storage.KeyValue{Key: k, Value: v})
if len(batch) >= 3 {
err = module.AddContractStorageItems(batch, uint32(stateSyncPoint), sroot.Root)
require.NoError(t, err)
batch = batch[:0]
}
return true
})
if len(batch) > 0 {
err = module.AddContractStorageItems(batch, uint32(stateSyncPoint), sroot.Root)
require.NoError(t, err)
}
require.NoError(t, err)
} else {
h, err := bcSpout.GetHeader(bcSpout.GetHeaderHash(stateSyncPoint + 1))
require.NoError(t, err)
unknownHashes := module.GetUnknownMPTNodesBatch(100)
require.Equal(t, 1, len(unknownHashes))
require.Equal(t, h.PrevStateRoot, unknownHashes[0])
nodesMap := make(map[util.Uint256][]byte)
sm := bcSpout.GetStateModule()
sroo, err := sm.GetStateRoot(uint32(stateSyncPoint))
require.NoError(t, err)
require.Equal(t, sroo.Root, h.PrevStateRoot)
err = bcSpout.GetStateSyncModule().Traverse(h.PrevStateRoot, func(n mpt.Node, nodeBytes []byte) bool {
nodesMap[n.Hash()] = nodeBytes
return false
})
require.NoError(t, err)
for {
need := module.GetUnknownMPTNodesBatch(10)
if len(need) == 0 {
break
}
add := make([][]byte, len(need))
for i, h := range need {
nodeBytes, ok := nodesMap[h]
if !ok {
t.Fatal("unknown or restored node requested")
}
add[i] = nodeBytes
delete(nodesMap, h)
}
require.NoError(t, module.AddMPTNodes(add))
}
unknownNodes := module.GetUnknownMPTNodesBatch(1)
require.True(t, len(unknownNodes) == 0)
}
require.True(t, module.IsActive())
require.False(t, module.NeedHeaders())
require.False(t, module.NeedStorageData())
require.Equal(t, uint32(stateSyncPoint-maxTraceable), module.BlockHeight())
// add blocks
t.Run("error: unexpected block index", func(t *testing.T) {
b, err := bcSpout.GetBlock(bcSpout.GetHeaderHash(stateSyncPoint - maxTraceable))
require.NoError(t, err)
require.Error(t, module.AddBlock(b))
})
t.Run("error: missing state root in block header", func(t *testing.T) {
b := &block.Block{
Header: block.Header{
Index: uint32(stateSyncPoint) - maxTraceable + 1,
StateRootEnabled: false,
},
}
require.Error(t, module.AddBlock(b))
})
t.Run("error: invalid block merkle root", func(t *testing.T) {
b := &block.Block{
Header: block.Header{
Index: uint32(stateSyncPoint) - maxTraceable + 1,
StateRootEnabled: true,
MerkleRoot: util.Uint256{1, 2, 3},
},
}
require.Error(t, module.AddBlock(b))
})
for i := uint32(stateSyncPoint - maxTraceable + 1); i <= stateSyncPoint; i++ {
b, err := bcSpout.GetBlock(bcSpout.GetHeaderHash(i))
require.NoError(t, err)
require.NoError(t, module.AddBlock(b))
}
require.False(t, module.IsActive())
require.True(t, module.IsInitialized())
require.False(t, module.NeedHeaders())
require.False(t, module.NeedStorageData())
require.False(t, module.NeedBlocks())
require.Equal(t, uint32(stateSyncPoint), module.BlockHeight())
require.Equal(t, uint32(stateSyncPoint), bcBolt.BlockHeight())
// add missing blocks to bcBolt: should be ok, because state is synced
for i := uint32(stateSyncPoint + 1); i <= bcSpout.BlockHeight(); i++ {
b, err := bcSpout.GetBlock(bcSpout.GetHeaderHash(i))
require.NoError(t, err)
require.NoError(t, bcBolt.AddBlock(b))
}
require.Equal(t, bcSpout.BlockHeight(), bcBolt.BlockHeight())
// compare storage states
fetchStorage := func(ps storage.Store, storagePrefix byte) []storage.KeyValue {
var kv []storage.KeyValue
ps.Seek(storage.SeekRange{Prefix: []byte{storagePrefix}}, func(k, v []byte) bool {
key := bytes.Clone(k)
value := bytes.Clone(v)
if key[0] == byte(storage.STTempStorage) {
key[0] = byte(storage.STStorage)
}
kv = append(kv, storage.KeyValue{
Key: key,
Value: value,
})
return true
})
return kv
}
// Both blockchains are running, so we need to wait until recent changes will be persisted
// to the underlying backend store. Close blockchains to ensure persist was completed.
bcSpout.Close()
bcBolt.Close()
expected := fetchStorage(bcSpoutStore, byte(storage.STStorage))
actual := fetchStorage(bcBoltStore, byte(storage.STTempStorage))
require.ElementsMatch(t, expected, actual)
// no temp items should be left
var haveItems bool
bcBoltStore.Seek(storage.SeekRange{Prefix: []byte{byte(storage.STStorage)}}, func(_, _ []byte) bool {
haveItems = true
return false
})
require.False(t, haveItems)
}
t.Run("source node is archive", func(t *testing.T) {
check(t, false, false)
})
t.Run("source node is light with GC", func(t *testing.T) {
check(t, true, false)
})
t.Run("ContractStorageBased mode", func(t *testing.T) {
check(t, false, true)
})
}
func TestStateSyncModule_SetOnStageChanged(t *testing.T) {
const (
stateSyncInterval = 2
maxTraceable = 3
)
spoutCfg := func(c *config.Blockchain) {
c.StateRootInHeader = true
c.StateSyncInterval = stateSyncInterval
c.MaxTraceableBlocks = maxTraceable
}
bcSpout, vals, comm := chain.NewMultiWithCustomConfig(t, spoutCfg)
e := tutustest.NewExecutor(t, bcSpout, vals, comm)
for range 2*stateSyncInterval + maxTraceable + 2 {
e.AddNewBlock(t)
}
mptCfg := func(c *config.Blockchain) {
spoutCfg(c)
c.P2PStateExchangeExtensions = true
c.KeepOnlyLatestState = true
c.RemoveUntraceableBlocks = true
}
storageCfg := func(c *config.Blockchain) {
spoutCfg(c)
c.KeepOnlyLatestState = true
c.RemoveUntraceableBlocks = true
c.NeoFSStateSyncExtensions = true
c.NeoFSStateFetcher.Enabled = true
c.NeoFSBlockFetcher.Enabled = true
}
check := func(t *testing.T, cfg func(*config.Blockchain), storageEnabled bool) {
bcBolt, _, _ := chain.NewMultiWithCustomConfig(t, cfg)
module := bcBolt.GetStateSyncModule()
var calls int
module.SetOnStageChanged(func() { calls++ })
require.NoError(t, module.Init(bcSpout.BlockHeight()))
require.Equal(t, 1, calls)
syncPoint := module.GetStateSyncPoint()
// AddHeaders up to P → headersSynced → active
for i := uint32(1); i <= bcSpout.HeaderHeight(); i++ {
h, err := bcSpout.GetHeader(bcSpout.GetHeaderHash(i))
require.NoError(t, err)
require.NoError(t, module.AddHeaders(h))
}
require.Equal(t, 2, calls)
require.True(t, module.IsActive())
// AddMPTNodes or AddContractStorageItems up to P → storageSynced
if !storageEnabled {
for {
unknown := module.GetUnknownMPTNodesBatch(3)
if len(unknown) == 0 {
break
}
require.NoError(t, bcSpout.GetStateSyncModule().Traverse(
unknown[0],
func(_ mpt.Node, nodeBytes []byte) bool {
require.NoError(t, module.AddMPTNodes([][]byte{nodeBytes}))
return true
},
))
}
} else {
sm := bcSpout.GetStateModule()
sroot, err := sm.GetStateRoot(syncPoint)
require.NoError(t, err)
var all []storage.KeyValue
sm.SeekStates(sroot.Root, nil, func(k, v []byte) bool {
all = append(all, storage.KeyValue{Key: k, Value: v})
return true
})
require.NoError(t, module.AddContractStorageItems(all, syncPoint, sroot.Root))
}
require.Equal(t, 3, calls)
// AddBlock up to P → blocksSynced → inactive
for i := syncPoint - maxTraceable + 1; i <= syncPoint; i++ {
b, err := bcSpout.GetBlock(bcSpout.GetHeaderHash(i))
require.NoError(t, err)
require.NoError(t, module.AddBlock(b))
}
require.Equal(t, 4, calls)
}
t.Run("MPT based ", func(t *testing.T) {
check(t, mptCfg, false)
})
t.Run("ContractStorage based ", func(t *testing.T) {
check(t, storageCfg, true)
})
}