9
9
import com .swirlds .config .extensions .test .fixtures .TestConfigBuilder ;
10
10
import com .swirlds .platform .internal .EventImpl ;
11
11
import com .swirlds .platform .test .fixtures .consensus .TestIntake ;
12
- import com .swirlds .platform .test .fixtures .consensus .framework .validation .RoundInternalEqualityValidation ;
12
+ import com .swirlds .platform .test .fixtures .consensus .framework .validation .ConsensusRoundValidator ;
13
13
import com .swirlds .platform .test .fixtures .event .generator .StandardGraphGenerator ;
14
14
import com .swirlds .platform .test .fixtures .event .source .EventSource ;
15
15
import com .swirlds .platform .test .fixtures .event .source .StandardEventSource ;
16
16
import com .swirlds .platform .test .fixtures .graph .OtherParentMatrixFactory ;
17
- import java .util .Iterator ;
17
+ import java .util .Arrays ;
18
18
import java .util .LinkedList ;
19
19
import java .util .List ;
20
20
import java .util .stream .Stream ;
21
- import org .hiero .consensus .model .event .EventConstants ;
21
+ import org .hiero .base .crypto .Hash ;
22
+ import org .hiero .consensus .config .EventConfig_ ;
23
+ import org .hiero .consensus .model .event .PlatformEvent ;
22
24
import org .hiero .consensus .model .hashgraph .ConsensusRound ;
23
- import org .junit .jupiter .api .Test ;
25
+ import org .hiero .consensus .model .hashgraph .EventWindow ;
26
+ import org .junit .jupiter .params .ParameterizedTest ;
27
+ import org .junit .jupiter .params .provider .ValueSource ;
28
+
29
+ class AncientParentsTest {
30
+
31
+ public static final int FIRST_BATCH_SIZE = 5000 ;
32
+ public static final int SECOND_BATCH_SIZE = 1000 ;
24
33
25
- class IntakeAndConsensusTests {
26
34
/**
27
- * Reproduces #5635
35
+ * Tests the scenario where some stale events are added to different nodes at different points in consensus time.
36
+ * Depending on the point in consensus time, an event might have ancient parents on one node, but not on the other.
37
+ * Both nodes should have the same consensus output.
28
38
* <p>
29
- * This test creates a graph with two partitions, where one partition is small enough that it is not needed for
39
+ * This test creates a graph with two partitions, where one partition is small enough that it is not required for
30
40
* consensus. Because the small partition does not affect consensus, we can delay inserting those events and still
31
41
* reach consensus. We delay adding the small partition events until the first of these events becomes ancient. This
32
42
* would lead to at least one subsequent small partition event being non-ancient, but not having only ancient
@@ -35,26 +45,24 @@ class IntakeAndConsensusTests {
35
45
* that new events will be descendants of some small partition events. This means that the small partition events
36
46
* will now be needed for consensus. If the small partition events are not inserted into one of the nodes correctly,
37
47
* it will not be able to reach consensus.
38
- * <p>
39
- * Tests the workaround described in #5762
40
48
*/
41
- @ Test
42
- void nonAncientEventWithMissingParents () {
49
+ @ ParameterizedTest
50
+ @ ValueSource (booleans = {true , false })
51
+ void nonAncientEventWithMissingParents (final boolean useBirthRounds ) {
43
52
final long seed = 0 ;
44
53
final int numNodes = 10 ;
45
54
final List <Integer > partitionNodes = List .of (0 , 1 );
46
55
47
56
final Configuration configuration = new TestConfigBuilder ()
48
57
.withValue (ConsensusConfig_ .ROUNDS_NON_ANCIENT , 25 )
49
58
.withValue (ConsensusConfig_ .ROUNDS_EXPIRED , 25 )
59
+ .withValue (EventConfig_ .USE_BIRTH_ROUND_ANCIENT_THRESHOLD , Boolean .toString (useBirthRounds ))
50
60
.getOrCreateConfig ();
51
61
52
62
final PlatformContext platformContext = TestPlatformContextBuilder .create ()
53
63
.withConfiguration (configuration )
54
64
.build ();
55
65
56
- // the generated events are first fed into consensus so that round created is calculated before we start
57
- // using them
58
66
final List <EventSource > eventSources = Stream .generate (StandardEventSource ::new )
59
67
.map (ses -> (EventSource ) ses )
60
68
.limit (numNodes )
@@ -63,10 +71,8 @@ void nonAncientEventWithMissingParents() {
63
71
final TestIntake node1 = new TestIntake (platformContext , generator .getRoster ());
64
72
final TestIntake node2 = new TestIntake (platformContext , generator .getRoster ());
65
73
66
- // first we generate events regularly, until we have some ancient rounds
67
- final int firstBatchSize = 5000 ;
68
- List <EventImpl > batch = generator .generateEvents (firstBatchSize );
69
- for (final EventImpl event : batch ) {
74
+ // first, we generate events regularly, until we have some ancient rounds
75
+ for (final EventImpl event : generator .generateEvents (FIRST_BATCH_SIZE )) {
70
76
node1 .addEvent (event .getBaseEvent ().copyGossipedData ());
71
77
node2 .addEvent (event .getBaseEvent ().copyGossipedData ());
72
78
}
@@ -80,24 +86,31 @@ void nonAncientEventWithMissingParents() {
80
86
// during the partition, we will not insert the minority partition events into consensus
81
87
// we generate just enough events to make the first event of the partition ancient, but we don't insert the
82
88
// last event into the second consensus
83
- long partitionMinGen = EventConstants .GENERATION_UNDEFINED ;
84
- long partitionMaxGen = EventConstants .GENERATION_UNDEFINED ;
85
89
final List <EventImpl > partitionedEvents = new LinkedList <>();
86
90
boolean succeeded = false ;
87
91
EventImpl lastEvent = null ;
92
+ EventImpl firstEventInPartition = null ;
88
93
while (!succeeded ) {
89
- batch = generator .generateEvents (1 );
90
- lastEvent = batch .getFirst ();
94
+ lastEvent = generator .generateEvents (1 ).getFirst ();
91
95
if (partitionNodes .contains ((int ) lastEvent .getCreatorId ().id ())) {
92
- partitionMinGen = partitionMinGen == EventConstants .GENERATION_UNDEFINED
93
- ? lastEvent .getGeneration ()
94
- : Math .min (partitionMinGen , lastEvent .getGeneration ());
95
- partitionMaxGen = Math .max (partitionMaxGen , lastEvent .getGeneration ());
96
+ // we have generated an event in the minority partition
97
+
98
+ if (firstEventInPartition == null ) {
99
+ // this is the first event in the partition
100
+ firstEventInPartition = lastEvent ;
101
+ }
102
+
103
+ // we don't add these events to consensus yet, we will add them later
96
104
partitionedEvents .add (lastEvent );
97
105
} else {
106
+ // this is an event in the majority partition
107
+ // we add it to node 1 always
98
108
node1 .addEvent (lastEvent .getBaseEvent ().copyGossipedData ());
99
- final long node1NonAncGen = node1 .getOutput ().getEventWindow ().getAncientThreshold ();
100
- if (partitionMaxGen > node1NonAncGen && partitionMinGen < node1NonAncGen ) {
109
+
110
+ // if this event caused the first event in the partition to become ancient, then we exit this loop.
111
+ // we will add this event to node 2 later, after we add the partitioned events
112
+ final EventWindow node1Window = node1 .getOutput ().getEventWindow ();
113
+ if (firstEventInPartition != null && node1Window .isAncient (firstEventInPartition .getBaseEvent ())) {
101
114
succeeded = true ;
102
115
} else {
103
116
node2 .addEvent (lastEvent .getBaseEvent ().copyGossipedData ());
@@ -115,32 +128,47 @@ void nonAncientEventWithMissingParents() {
115
128
node2 .addEvent (lastEvent .getBaseEvent ().copyGossipedData ());
116
129
final long consRoundBeforeLastBatch =
117
130
node1 .getConsensusRounds ().getLast ().getRoundNum ();
131
+ // we wanted the first event in the partition to become ancient, so it should never reach consensus
132
+ assertEventDidNotReachConsensus (firstEventInPartition , node1 , node2 );
118
133
assertConsensusEvents (node1 , node2 );
119
134
120
135
// now the partitions rejoin
121
136
generator .setOtherParentAffinity (OtherParentMatrixFactory .createBalancedOtherParentMatrix (numNodes ));
122
137
123
138
// now we generate more events and expect consensus to be the same
124
- final int secondBatchSize = 1000 ;
125
- batch = generator .generateEvents (secondBatchSize );
126
- for (final EventImpl event : batch ) {
139
+ for (final EventImpl event : generator .generateEvents (SECOND_BATCH_SIZE )) {
127
140
node1 .addEvent (event .getBaseEvent ().copyGossipedData ());
128
141
node2 .addEvent (event .getBaseEvent ().copyGossipedData ());
129
142
}
130
143
assertThat (node1 .getConsensusRounds ().getLast ().getRoundNum ())
131
- .isGreaterThan (consRoundBeforeLastBatch )
132
- .withFailMessage ("consensus did not advance after the partition rejoined" );
144
+ .withFailMessage ("consensus did not advance after the partition rejoined" )
145
+ .isGreaterThan (consRoundBeforeLastBatch );
146
+ assertEventDidNotReachConsensus (firstEventInPartition , node1 , node2 );
133
147
assertConsensusEvents (node1 , node2 );
134
148
}
135
149
136
- private static void assertConsensusEvents (final TestIntake node1 , final TestIntake node2 ) {
137
- final RoundInternalEqualityValidation roundInternalEqualityValidation = new RoundInternalEqualityValidation ();
150
+ /**
151
+ * Assert that the supplied event did not reach consensus in any of the nodes.
152
+ *
153
+ * @param event the event to check
154
+ * @param nodes the nodes to check
155
+ */
156
+ private static void assertEventDidNotReachConsensus (final EventImpl event , final TestIntake ... nodes ) {
157
+ final Hash eventHash = event .getBaseHash ();
158
+ final boolean found = Arrays .stream (nodes )
159
+ .map (TestIntake ::getConsensusRounds )
160
+ .flatMap (List ::stream )
161
+ .map (ConsensusRound ::getConsensusEvents )
162
+ .flatMap (List ::stream )
163
+ .map (PlatformEvent ::getHash )
164
+ .anyMatch (eventHash ::equals );
165
+ assertThat (found )
166
+ .withFailMessage ("Event was not supposed to reach consensus, but it did" )
167
+ .isFalse ();
168
+ }
138
169
139
- final Iterator <ConsensusRound > iterator1 = node1 .getConsensusRounds ().iterator ();
140
- final Iterator <ConsensusRound > iterator2 = node2 .getConsensusRounds ().iterator ();
141
- while (iterator1 .hasNext () && iterator2 .hasNext ()) {
142
- roundInternalEqualityValidation .validate (iterator1 .next (), iterator2 .next ());
143
- }
170
+ private static void assertConsensusEvents (final TestIntake node1 , final TestIntake node2 ) {
171
+ new ConsensusRoundValidator ().validate (node1 .getConsensusRounds (), node2 .getConsensusRounds ());
144
172
node1 .getConsensusRounds ().clear ();
145
173
node2 .getConsensusRounds ().clear ();
146
174
}
0 commit comments