Skip to content

Commit e8021b7

Browse files
lpetrovic05abies
andauthored
feat: MOP ancestor search & DeGen (#22316)
Signed-off-by: Lazar Petrovic <[email protected]> Co-authored-by: Artur Biesiadowski <[email protected]>
1 parent 85f2e92 commit e8021b7

File tree

10 files changed

+497
-233
lines changed

10 files changed

+497
-233
lines changed

platform-sdk/consensus-model/src/testFixtures/java/org/hiero/consensus/model/test/fixtures/event/TestingEventBuilder.java

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,14 @@
1111
import edu.umd.cs.findbugs.annotations.Nullable;
1212
import java.time.Instant;
1313
import java.util.ArrayList;
14+
import java.util.HexFormat;
1415
import java.util.List;
1516
import java.util.Objects;
1617
import java.util.Optional;
1718
import java.util.Random;
1819
import java.util.stream.Stream;
20+
import org.hiero.base.crypto.DigestType;
21+
import org.hiero.base.crypto.Hash;
1922
import org.hiero.base.crypto.SignatureType;
2023
import org.hiero.base.crypto.test.fixtures.CryptoRandomUtils;
2124
import org.hiero.base.utility.test.fixtures.RandomUtils;
@@ -138,6 +141,9 @@ public class TestingEventBuilder {
138141
*/
139142
private long nGen = NonDeterministicGeneration.GENERATION_UNDEFINED;
140143

144+
/** The hash to use for the event */
145+
private Hash hash = null;
146+
141147
/**
142148
* Constructor
143149
*
@@ -366,6 +372,23 @@ public TestingEventBuilder(@NonNull final Random random) {
366372
return this;
367373
}
368374

375+
/**
376+
* Set a custom hash for the event. This is useful for having a human-readable hash for debugging purposes.
377+
*
378+
* @param hexString the hash as a hex string
379+
* @return this instance
380+
*/
381+
public @NonNull TestingEventBuilder setHash(@NonNull final String hexString) {
382+
final byte[] parsedHex = HexFormat.of().parseHex(hexString.toLowerCase());
383+
if (parsedHex.length > DigestType.SHA_384.digestLength()) {
384+
throw new IllegalArgumentException("Hash length is too long");
385+
}
386+
final byte[] hash = new byte[DigestType.SHA_384.digestLength()];
387+
System.arraycopy(parsedHex, 0, hash, 0, parsedHex.length);
388+
this.hash = new Hash(hash);
389+
return this;
390+
}
391+
369392
/**
370393
* Generate transactions based on the settings provided.
371394
* <p>
@@ -494,7 +517,7 @@ private EventDescriptorWrapper createDescriptorFromParent(
494517

495518
final PlatformEvent platformEvent = new PlatformEvent(unsignedEvent, Bytes.wrap(signature));
496519

497-
platformEvent.setHash(CryptoRandomUtils.randomHash(random));
520+
platformEvent.setHash(hash != null ? hash : CryptoRandomUtils.randomHash(random));
498521

499522
platformEvent.setNGen(nGen);
500523

platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/consensus/AncestorIterator.java

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@ public class AncestorIterator implements Iterator<EventImpl> {
6161
private IteratorState state;
6262
/** is curr a self ancestor of the judge? */
6363
private boolean selfAncestor;
64+
/** stack of the index of the next other parent to try to traverse into */
65+
private final Deque<Integer> stackNextOtherParentIndex = new ArrayDeque<>(INITIAL_STACK_SIZE);
66+
/** the index of the next other parent to try to traverse into */
67+
private int nextOtherParentIndex = 0;
6468

6569
private enum IteratorState {
6670
TRAVERSING_SELF_PARENT,
@@ -96,6 +100,7 @@ public void initializeSearch(@NonNull final EventImpl root, @NonNull final Predi
96100
hasNext = true;
97101
state = IteratorState.TRAVERSING_SELF_PARENT;
98102
selfAncestor = true;
103+
nextOtherParentIndex = 0;
99104
}
100105

101106
private void clear() {
@@ -148,10 +153,16 @@ public boolean hasNext() {
148153
switch (state) {
149154
case TRAVERSING_SELF_PARENT -> { // try to traverse into selfParent
150155
final EventImpl parent = curr.getSelfParent();
151-
state = IteratorState.TRAVERSING_OTHER_PARENT;
156+
if (curr.getOtherParents().isEmpty()) {
157+
state = IteratorState.BOTTOM;
158+
} else {
159+
state = IteratorState.TRAVERSING_OTHER_PARENT;
160+
nextOtherParentIndex = 0;
161+
}
152162
if (mark.isNotVisited(parent) && valid.test(parent)) {
153163
stackRef.push(curr);
154164
stackState.push(state);
165+
stackNextOtherParentIndex.push(nextOtherParentIndex);
155166
stackSelfAncestor.push(selfAncestor);
156167
stackTime.push(timeReachedRoot);
157168
curr = parent;
@@ -162,11 +173,17 @@ public boolean hasNext() {
162173
} // there is no selfParent, or it was already visited, or it was not valid
163174
}
164175
case TRAVERSING_OTHER_PARENT -> { // try to traverse into otherParent
165-
final EventImpl parent = curr.getOtherParent();
166-
state = IteratorState.BOTTOM;
176+
final EventImpl parent = curr.getOtherParents().get(nextOtherParentIndex);
177+
if (nextOtherParentIndex + 1 >= curr.getOtherParents().size()) {
178+
state = IteratorState.BOTTOM;
179+
} else {
180+
state = IteratorState.TRAVERSING_OTHER_PARENT;
181+
nextOtherParentIndex++;
182+
}
167183
if (mark.isNotVisited(parent) && valid.test(parent)) {
168184
stackRef.push(curr);
169185
stackState.push(state);
186+
stackNextOtherParentIndex.push(nextOtherParentIndex);
170187
stackSelfAncestor.push(selfAncestor);
171188
stackTime.push(timeReachedRoot);
172189
curr = parent;
@@ -184,6 +201,7 @@ public boolean hasNext() {
184201
final EventImpl toReturn = curr; // else we are done with all the descendents, so backtrack
185202
curr = stackRef.pop();
186203
state = stackState.pop();
204+
nextOtherParentIndex = stackNextOtherParentIndex.pop();
187205
selfAncestor = stackSelfAncestor.pop();
188206
timeReachedRoot = stackTime.pop();
189207
return toReturn; // return the child of the vertex we just backtracked to

platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/consensus/DeGen.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,10 @@ public class DeGen {
2525
* @param event the event to set the DeGen value for
2626
*/
2727
public static void calculateDeGen(@NonNull final EventImpl event) {
28-
final int maxParentDeGen = Math.max(parentDeGen(event.getSelfParent()), parentDeGen(event.getOtherParent()));
28+
final int maxParentDeGen = event.getAllParents().stream()
29+
.mapToInt(DeGen::parentDeGen)
30+
.max()
31+
.orElse(GENERATION_UNDEFINED);
2932
if (maxParentDeGen == GENERATION_UNDEFINED) {
3033
event.setDeGen(FIRST_GENERATION);
3134
} else {

platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/consensus/AncestorSearchTest.java

Lines changed: 94 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -2,74 +2,125 @@
22
package com.swirlds.platform.consensus;
33

44
import static org.junit.jupiter.api.Assertions.assertEquals;
5-
import static org.junit.jupiter.api.Assertions.assertNull;
5+
import static org.junit.jupiter.api.Assertions.assertNotNull;
66
import static org.junit.jupiter.api.Assertions.assertSame;
77
import static org.junit.jupiter.api.Assertions.assertTrue;
88

9+
import com.swirlds.common.test.fixtures.Randotron;
910
import com.swirlds.platform.internal.EventImpl;
11+
import com.swirlds.platform.test.fixtures.graph.SimpleGraph;
1012
import com.swirlds.platform.test.fixtures.graph.SimpleGraphs;
13+
import edu.umd.cs.findbugs.annotations.NonNull;
1114
import java.time.Instant;
1215
import java.util.HashSet;
1316
import java.util.List;
14-
import java.util.Map;
17+
import java.util.Set;
1518
import java.util.Spliterators;
19+
import java.util.function.Predicate;
1620
import java.util.stream.Collectors;
17-
import java.util.stream.IntStream;
1821
import java.util.stream.StreamSupport;
1922
import org.hiero.base.crypto.Hash;
20-
import org.hiero.base.utility.test.fixtures.RandomUtils;
21-
import org.junit.jupiter.api.RepeatedTest;
23+
import org.junit.jupiter.api.Assertions;
2224
import org.junit.jupiter.api.Test;
25+
import org.junit.jupiter.params.ParameterizedTest;
26+
import org.junit.jupiter.params.provider.ValueSource;
2327

28+
/**
29+
* Tests for the {@link AncestorSearch}
30+
*/
2431
class AncestorSearchTest {
32+
/** A predicate that matches all events */
33+
private static final Predicate<EventImpl> ALL_EVENTS = e -> true;
34+
/** A predicate that matches only non-consensus events */
35+
private static final Predicate<EventImpl> NON_CONSENSUS_EVENTS = e -> !e.isConsensus();
2536

26-
final EventVisitedMark mark = new EventVisitedMark();
27-
final AncestorSearch search = new AncestorSearch(mark);
28-
final List<EventImpl> events = SimpleGraphs.graph9e3n(RandomUtils.getRandomPrintSeed());
29-
final EventImpl root = events.get(8);
37+
/**
38+
* Tests the graph with multiple other-parents
39+
*/
40+
@Test
41+
void mopGraph() {
42+
final SimpleGraph graph = SimpleGraphs.mopGraph(Randotron.create());
43+
final AncestorSearch search = new AncestorSearch();
3044

31-
@RepeatedTest(3)
32-
void basicTest() {
33-
searchAndAssert();
34-
}
45+
assertEquals(graph.hashes(1, 2, 3, 6), getAncestors(search, graph.impl(6)));
46+
assertEquals(graph.hashes(0, 4, 8), getAncestors(search, graph.impl(8)));
47+
assertEquals(graph.hashes(0, 1, 2, 3, 4, 5, 6, 9), getAncestors(search, graph.impl(9)));
48+
assertEquals(graph.hashes(0, 1, 2, 3, 5, 6, 7, 10), getAncestors(search, graph.impl(10)));
49+
assertEquals(graph.hashes(3, 7, 11), getAncestors(search, graph.impl(11)));
3550

36-
@Test
37-
void markWraparound() {
38-
mark.setMark(-1);
39-
searchAndAssert();
40-
}
51+
assertEquals(
52+
graph.hashes(0, 1, 2, 3, 5, 6),
53+
search.commonAncestorsOf(graph.impls(9, 10), ALL_EVENTS).stream()
54+
.map(EventImpl::getBaseHash)
55+
.collect(Collectors.toSet()));
56+
// clear the recTimes so they don't interfere with the next search
57+
graph.impls().forEach(e -> e.setRecTimes(null));
4158

42-
@Test
43-
void markOverflow() {
44-
mark.setMark(Integer.MAX_VALUE);
45-
searchAndAssert();
59+
assertEquals(
60+
graph.hashes(0),
61+
search.commonAncestorsOf(graph.impls(8, 9, 10), ALL_EVENTS).stream()
62+
.map(EventImpl::getBaseHash)
63+
.collect(Collectors.toSet()));
4664
}
4765

48-
@Test
49-
void commonAncestors() {
50-
final List<EventImpl> ancestors =
51-
search.commonAncestorsOf(List.of(events.get(5), events.get(6), events.get(7)), e -> true);
66+
/**
67+
* Tests the graph with 9 events and 3 nodes. This test is parameterized to run with different starting marks to
68+
* ensure that the marking system works correctly. It also validates that the recTimes are set correctly on the
69+
* common ancestor.
70+
*/
71+
@ParameterizedTest
72+
@ValueSource(ints = {1, -1, Integer.MAX_VALUE})
73+
void graph9e3n(final int startingMark) {
74+
final EventVisitedMark mark = new EventVisitedMark();
75+
final AncestorSearch search = new AncestorSearch(mark);
76+
final SimpleGraph graph = SimpleGraphs.graph9e3n(Randotron.create());
77+
78+
// we test various starting marks to ensure that the marking system works correctly
79+
mark.setMark(startingMark);
80+
81+
// test getting ancestors of the latest event(8)
82+
assertEquals(graph.hashes(2, 3, 4, 6, 7, 8), getAncestors(search, graph.impl(8), NON_CONSENSUS_EVENTS));
83+
84+
// test getting common ancestors of events 5,6 & 7
85+
final List<EventImpl> ancestors = search.commonAncestorsOf(graph.impls(5, 6, 7), ALL_EVENTS);
86+
// we expect only one common ancestor: event 1
5287
assertEquals(1, ancestors.size());
53-
assertSame(events.get(1), ancestors.get(0));
54-
final HashSet<Instant> recTimes = new HashSet<>(events.get(1).getRecTimes());
88+
assertSame(graph.impl(1), ancestors.getFirst());
89+
90+
// verify that recTimes are correct
91+
final EventImpl commonAncestor = ancestors.getFirst();
92+
assertNotNull(commonAncestor.getRecTimes());
93+
final HashSet<Instant> recTimes = new HashSet<>(commonAncestor.getRecTimes());
5594
assertEquals(3, recTimes.size());
56-
assertTrue(recTimes.contains(events.get(3).getTimeCreated()));
57-
assertTrue(recTimes.contains(events.get(6).getTimeCreated()));
58-
assertTrue(recTimes.contains(events.get(7).getTimeCreated()));
95+
assertTrue(recTimes.contains(graph.impl(3).getTimeCreated()));
96+
assertTrue(recTimes.contains(graph.impl(6).getTimeCreated()));
97+
assertTrue(recTimes.contains(graph.impl(7).getTimeCreated()));
98+
99+
// verify that other events' recTimes are still null
100+
graph.impls(0, 2, 3, 4, 5, 6, 7, 8).stream().map(EventImpl::getRecTimes).forEach(Assertions::assertNull);
101+
}
59102

60-
IntStream.of(0, 2, 3, 4, 5, 6, 7, 8)
61-
.forEach(i -> assertNull(events.get(i).getRecTimes()));
62-
events.get(1).setRecTimes(null);
103+
/**
104+
* Same as {@link #getAncestors(AncestorSearch, EventImpl, Predicate)} with a predicate that matches all events
105+
*/
106+
private Set<Hash> getAncestors(@NonNull final AncestorSearch search, @NonNull final EventImpl event) {
107+
return getAncestors(search, event, ALL_EVENTS);
63108
}
64109

65-
private void searchAndAssert() {
66-
// look for non-consensus ancestors of 8
67-
final Map<Hash, EventImpl> ancestors = StreamSupport.stream(
68-
Spliterators.spliteratorUnknownSize(search.initializeSearch(root, e -> !e.isConsensus()), 0),
69-
false)
70-
.collect(Collectors.toMap(EventImpl::getBaseHash, e -> e));
71-
assertEquals(6, ancestors.size());
72-
IntStream.of(2, 3, 4, 6, 7, 8)
73-
.forEach(i -> assertTrue(ancestors.containsKey(events.get(i).getBaseHash())));
110+
/**
111+
* Get the ancestors of an event that match the given predicate
112+
* @param search the ancestor search instance to use
113+
* @param event the event whose ancestors to find
114+
* @param predicate the predicate to filter ancestors
115+
* @return the set of ancestor hashes that match the predicate
116+
*/
117+
private Set<Hash> getAncestors(
118+
@NonNull final AncestorSearch search,
119+
@NonNull final EventImpl event,
120+
@NonNull final Predicate<EventImpl> predicate) {
121+
final AncestorIterator ancestorIterator = search.initializeSearch(event, predicate);
122+
return StreamSupport.stream(Spliterators.spliteratorUnknownSize(ancestorIterator, 0), false)
123+
.map(EventImpl::getBaseHash)
124+
.collect(Collectors.toSet());
74125
}
75126
}

0 commit comments

Comments
 (0)