Skip to content

Commit 3a35686

Browse files
authored
Merge pull request #7924 from multiversx/sync-convergence-tests
Sync convergence tests
2 parents 6c8f76a + 8a84872 commit 3a35686

9 files changed

Lines changed: 832 additions & 24 deletions

File tree

factory/processing/processComponents.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,8 @@ import (
8787
"github.com/multiversx/mx-chain-go/update/trigger"
8888
)
8989

90-
// timeSpanForBadHeaders is the expiry time for an added block header hash
90+
// timeSpanForBadHeaders is the expiry time for an added block header hash; it is a wall-clock
91+
// network-healing window (fetch retries, partition heal), intentionally NOT scaled with round duration
9192
var timeSpanForBadHeaders = time.Minute * 2
9293

9394
// processComponents struct holds the process components
Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
package basicSync
2+
3+
import (
4+
"testing"
5+
"time"
6+
7+
"github.com/multiversx/mx-chain-core-go/core"
8+
"github.com/multiversx/mx-chain-core-go/data"
9+
"github.com/stretchr/testify/assert"
10+
"github.com/stretchr/testify/require"
11+
12+
"github.com/multiversx/mx-chain-go/integrationTests"
13+
)
14+
15+
// convergence scenario: two disconnected islands of sync nodes are split on
16+
// same-nonce siblings -- clean A (round = parent round + 1) vs contended B (later round,
17+
// same parent). Once the lower-round sibling and its proof reach the island holding B,
18+
// fork detection plus the V3 switch converge everyone on A without crossing any final
19+
// checkpoint. Delivery between islands is direct pool injection: a deterministic
20+
// stand-in for gossip and the proof pull (which are unit-tested separately).
21+
func TestSupernovaSync_NodesSplitOnSiblings_ConvergeOnLowerRound(t *testing.T) {
22+
if testing.Short() {
23+
t.Skip("this is not a short test")
24+
}
25+
26+
maxShards := uint32(1)
27+
shardId := uint32(0)
28+
29+
enableEpochs := integrationTests.CreateEnableEpochsConfig()
30+
enableEpochs.AndromedaEnableEpoch = uint32(0)
31+
enableEpochs.SupernovaEnableEpoch = uint32(0)
32+
roundsConfig := integrationTests.GetSupernovaRoundConfigActivatedAt(2)
33+
34+
newShardNode := func() *integrationTests.TestProcessorNode {
35+
return integrationTests.NewTestProcessorNode(integrationTests.ArgTestProcessorNode{
36+
MaxShards: maxShards,
37+
NodeShardId: shardId,
38+
TxSignPrivKeyShardId: shardId,
39+
WithSync: true,
40+
EpochsConfig: &enableEpochs,
41+
RoundsConfig: &roundsConfig,
42+
})
43+
}
44+
45+
pA := newShardNode() // island 1 proposer
46+
obsA := newShardNode() // island 1 observer
47+
pB := newShardNode() // island 2 proposer
48+
obsB := newShardNode() // island 2 observer
49+
metaNode := integrationTests.NewTestProcessorNode(integrationTests.ArgTestProcessorNode{
50+
MaxShards: maxShards,
51+
NodeShardId: core.MetachainShardId,
52+
TxSignPrivKeyShardId: shardId,
53+
WithSync: true,
54+
EpochsConfig: &enableEpochs,
55+
RoundsConfig: &roundsConfig,
56+
})
57+
58+
island1 := []*integrationTests.TestProcessorNode{pA, obsA, metaNode}
59+
island2 := []*integrationTests.TestProcessorNode{pB, obsB}
60+
allNodes := []*integrationTests.TestProcessorNode{pA, obsA, metaNode, pB, obsB}
61+
shardNodes := []*integrationTests.TestProcessorNode{pA, obsA, pB, obsB}
62+
63+
integrationTests.ConnectNodes([]integrationTests.Connectable{pA, obsA, metaNode})
64+
integrationTests.ConnectNodes([]integrationTests.Connectable{pB, obsB})
65+
66+
defer func() {
67+
for _, n := range allNodes {
68+
n.Close()
69+
}
70+
}()
71+
72+
for _, n := range allNodes {
73+
_ = n.StartSync()
74+
}
75+
time.Sleep(integrationTests.P2pBootstrapDelay)
76+
77+
// injectBlock simulates targeted delivery of a committed block and its proof
78+
injectBlock := func(target *integrationTests.TestProcessorNode, header data.HeaderHandler, hash []byte, proof data.HeaderProofHandler) {
79+
target.DataPool.Headers().AddHeader(hash, header)
80+
_ = target.DataPool.Proofs().AddProof(proof)
81+
}
82+
// mirrorCurrentBlock delivers src's current committed block (with proof) to target
83+
mirrorCurrentBlock := func(target *integrationTests.TestProcessorNode, src *integrationTests.TestProcessorNode) {
84+
header := src.BlockChain.GetCurrentBlockHeader()
85+
hash := src.BlockChain.GetCurrentBlockHeaderHash()
86+
require.NotNil(t, header)
87+
proof, err := src.DataPool.Proofs().GetProof(header.GetShardID(), hash)
88+
require.Nil(t, err)
89+
injectBlock(target, header, hash, proof)
90+
}
91+
92+
round := uint64(0)
93+
nonce := uint64(0)
94+
round = integrationTests.IncrementAndPrintRound(round)
95+
integrationTests.UpdateRound(allNodes, round)
96+
nonce++
97+
98+
// common prefix, rounds 1..4: island 1 produces, island 2 receives by injection
99+
numPrefixBlocks := 4
100+
for i := 0; i < numPrefixBlocks; i++ {
101+
integrationTests.ProposeBlockWithProof(island1, []*integrationTests.TestProcessorNode{pA, metaNode}, round, nonce)
102+
time.Sleep(integrationTests.SyncDelay)
103+
104+
for _, target := range island2 {
105+
mirrorCurrentBlock(target, pA)
106+
mirrorCurrentBlock(target, metaNode)
107+
}
108+
time.Sleep(integrationTests.SyncDelay)
109+
110+
round = integrationTests.IncrementAndPrintRound(round)
111+
integrationTests.UpdateRound(allNodes, round)
112+
nonce++
113+
}
114+
115+
for _, n := range shardNodes {
116+
require.NotNil(t, n.BlockChain.GetCurrentBlockHeader())
117+
require.Equal(t, uint64(numPrefixBlocks), n.BlockChain.GetCurrentBlockHeader().GetNonce())
118+
}
119+
120+
// round 5: island 1 commits the clean sibling A (nonce 5, round = parent round + 1)
121+
integrationTests.ProposeBlockWithProof(island1, []*integrationTests.TestProcessorNode{pA}, round, nonce)
122+
time.Sleep(integrationTests.SyncDelay)
123+
124+
headerA := pA.BlockChain.GetCurrentBlockHeader()
125+
hashA := pA.BlockChain.GetCurrentBlockHeaderHash()
126+
require.Equal(t, uint64(5), headerA.GetNonce())
127+
proofA, err := pA.DataPool.Proofs().GetProof(shardId, hashA)
128+
require.Nil(t, err)
129+
130+
// two silent rounds so the island 2 sibling lands with a round gap (contended)
131+
round = integrationTests.IncrementAndPrintRound(round)
132+
integrationTests.UpdateRound(allNodes, round)
133+
round = integrationTests.IncrementAndPrintRound(round)
134+
integrationTests.UpdateRound(allNodes, round)
135+
136+
// round 7: island 2, still on nonce 4, commits the contended sibling B
137+
integrationTests.ProposeBlockWithProof(island2, []*integrationTests.TestProcessorNode{pB}, round, nonce)
138+
time.Sleep(integrationTests.SyncDelay)
139+
140+
headerB := pB.BlockChain.GetCurrentBlockHeader()
141+
hashB := pB.BlockChain.GetCurrentBlockHeaderHash()
142+
require.Equal(t, uint64(5), headerB.GetNonce())
143+
require.NotEqual(t, string(hashA), string(hashB))
144+
require.Equal(t, headerA.GetPrevHash(), headerB.GetPrevHash())
145+
proofB, err := pB.DataPool.Proofs().GetProof(shardId, hashB)
146+
require.Nil(t, err)
147+
148+
// the clean sibling finalized instantly on its committers, the contended one did not
149+
assert.Equal(t, uint64(5), pA.ForkDetector.GetHighestFinalBlockNonce())
150+
assert.Equal(t, uint64(5), obsA.ForkDetector.GetHighestFinalBlockNonce())
151+
assert.Equal(t, uint64(4), pB.ForkDetector.GetHighestFinalBlockNonce())
152+
assert.Equal(t, uint64(4), obsB.ForkDetector.GetHighestFinalBlockNonce())
153+
154+
round = integrationTests.IncrementAndPrintRound(round)
155+
integrationTests.UpdateRound(allNodes, round)
156+
157+
// convergence: island 2 learns the lower-round sibling;
158+
// island 1 learns B and must NOT switch away from its finalized lower-round block
159+
for _, n := range island2 {
160+
injectBlock(n, headerA, hashA, proofA)
161+
}
162+
injectBlock(pA, headerB, hashB, proofB)
163+
injectBlock(obsA, headerB, hashB, proofB)
164+
165+
// the switch executes on sync-loop iterations, which advance with rounds: tick
166+
// empty rounds until everyone sits on A
167+
allOnBlockA := func() bool {
168+
for _, n := range shardNodes {
169+
currentHeader := n.BlockChain.GetCurrentBlockHeader()
170+
if currentHeader == nil || currentHeader.GetNonce() != 5 {
171+
return false
172+
}
173+
if string(n.BlockChain.GetCurrentBlockHeaderHash()) != string(hashA) {
174+
return false
175+
}
176+
}
177+
return true
178+
}
179+
maxConvergenceRounds := 6
180+
for i := 0; i < maxConvergenceRounds && !allOnBlockA(); i++ {
181+
round = integrationTests.IncrementAndPrintRound(round)
182+
integrationTests.UpdateRound(allNodes, round)
183+
time.Sleep(integrationTests.SyncDelay)
184+
}
185+
186+
require.True(t, allOnBlockA(), "nodes did not converge on the lower-round sibling within the bounded number of rounds")
187+
for _, n := range shardNodes {
188+
assert.Equal(t, uint64(5), n.ForkDetector.GetHighestFinalBlockNonce())
189+
}
190+
191+
// the chain stays usable after the switch: island 1 extends A, island 2 follows
192+
nonce++
193+
numExtensionBlocks := 2
194+
for i := 0; i < numExtensionBlocks; i++ {
195+
round = integrationTests.IncrementAndPrintRound(round)
196+
integrationTests.UpdateRound(allNodes, round)
197+
198+
integrationTests.ProposeBlockWithProof(island1, []*integrationTests.TestProcessorNode{pA}, round, nonce)
199+
time.Sleep(integrationTests.SyncDelay)
200+
201+
for _, target := range island2 {
202+
mirrorCurrentBlock(target, pA)
203+
}
204+
time.Sleep(integrationTests.SyncDelay)
205+
nonce++
206+
}
207+
208+
expectedNonce := uint64(5 + numExtensionBlocks)
209+
expectedHash := pA.BlockChain.GetCurrentBlockHeaderHash()
210+
allExtended := func() bool {
211+
for _, n := range shardNodes {
212+
currentHeader := n.BlockChain.GetCurrentBlockHeader()
213+
if currentHeader == nil || currentHeader.GetNonce() != expectedNonce {
214+
return false
215+
}
216+
if string(n.BlockChain.GetCurrentBlockHeaderHash()) != string(expectedHash) {
217+
return false
218+
}
219+
}
220+
return true
221+
}
222+
maxCatchUpRounds := 4
223+
for i := 0; i < maxCatchUpRounds && !allExtended(); i++ {
224+
round = integrationTests.IncrementAndPrintRound(round)
225+
integrationTests.UpdateRound(allNodes, round)
226+
time.Sleep(integrationTests.SyncDelay)
227+
}
228+
229+
require.True(t, allExtended(), "nodes did not extend the adopted branch together")
230+
// the first extension block carries a round gap (rounds ticked during convergence),
231+
// so it is contended: finality holds at the fork nonce and never regresses; it would
232+
// catch up only through meta notarization (covered by the chain-simulator suite)
233+
for _, n := range shardNodes {
234+
assert.Equal(t, uint64(5), n.ForkDetector.GetHighestFinalBlockNonce())
235+
}
236+
}

0 commit comments

Comments
 (0)