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) }) }