Skip to content

Commit a2c46e5

Browse files
authored
feat: Support multiple other parents in the event creator (#22423)
Signed-off-by: Artur Biesiadowski <[email protected]>
1 parent 37f05c7 commit a2c46e5

File tree

7 files changed

+264
-84
lines changed

7 files changed

+264
-84
lines changed

platform-sdk/consensus-event-creator-impl/src/main/java/org/hiero/consensus/event/creator/impl/tipset/TipsetAdvancementWeight.java

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616
* each zero weight node that advances. Does not contribute to meeting the threshold
1717
* required to advance the current snapshot.
1818
*/
19-
public record TipsetAdvancementWeight(long advancementWeight, long zeroWeightAdvancementCount) {
19+
public record TipsetAdvancementWeight(long advancementWeight, long zeroWeightAdvancementCount)
20+
implements Comparable<TipsetAdvancementWeight> {
2021

2122
/**
2223
* Zero advancement weight. For convenience.
@@ -68,9 +69,7 @@ public TipsetAdvancementWeight plus(@NonNull final TipsetAdvancementWeight that)
6869
* @return true if this weight is greater than the given weight
6970
*/
7071
public boolean isGreaterThan(@NonNull final TipsetAdvancementWeight that) {
71-
return advancementWeight > that.advancementWeight
72-
|| (advancementWeight == that.advancementWeight
73-
&& zeroWeightAdvancementCount > that.zeroWeightAdvancementCount);
72+
return compareTo(that) > 0;
7473
}
7574

7675
/**
@@ -79,4 +78,13 @@ public boolean isGreaterThan(@NonNull final TipsetAdvancementWeight that) {
7978
public boolean isNonZero() {
8079
return advancementWeight != 0 || zeroWeightAdvancementCount != 0;
8180
}
81+
82+
@Override
83+
public int compareTo(final TipsetAdvancementWeight o) {
84+
if (advancementWeight == o.advancementWeight) {
85+
return Double.compare(zeroWeightAdvancementCount, o.zeroWeightAdvancementCount);
86+
} else {
87+
return Double.compare(advancementWeight, o.advancementWeight);
88+
}
89+
}
8290
}

platform-sdk/consensus-event-creator-impl/src/main/java/org/hiero/consensus/event/creator/impl/tipset/TipsetEventCreator.java

Lines changed: 113 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22
package org.hiero.consensus.event.creator.impl.tipset;
33

44
import static com.swirlds.logging.legacy.LogMarker.EXCEPTION;
5-
import static org.hiero.consensus.event.creator.impl.tipset.TipsetAdvancementWeight.ZERO_ADVANCEMENT_WEIGHT;
65

76
import com.hedera.hapi.node.state.roster.Roster;
87
import com.swirlds.base.time.Time;
8+
import com.swirlds.base.utility.Pair;
99
import com.swirlds.common.utility.throttle.RateLimitedLogger;
1010
import com.swirlds.config.api.Configuration;
1111
import com.swirlds.logging.legacy.LogMarker;
@@ -16,8 +16,10 @@
1616
import java.time.Duration;
1717
import java.time.Instant;
1818
import java.util.ArrayList;
19+
import java.util.Arrays;
1920
import java.util.Collection;
2021
import java.util.Collections;
22+
import java.util.Comparator;
2123
import java.util.List;
2224
import java.util.Objects;
2325
import java.util.function.Function;
@@ -53,6 +55,7 @@ public class TipsetEventCreator implements EventCreator {
5355
private final TipsetWeightCalculator tipsetWeightCalculator;
5456
private final ChildlessEventTracker childlessOtherEventTracker;
5557
private final EventTransactionSupplier transactionSupplier;
58+
private final int maxOtherParents;
5659
private EventWindow eventWindow;
5760
/** The wall-clock time when the event window was last updated */
5861
private Instant lastReceivedEventWindow;
@@ -147,6 +150,7 @@ public TipsetEventCreator(
147150
eventWindow = EventWindow.getGenesisEventWindow();
148151
lastReceivedEventWindow = time.now();
149152
eventHasher = new PbjStreamHasher();
153+
maxOtherParents = eventCreationConfig.maxOtherParents();
150154
}
151155

152156
/**
@@ -229,12 +233,13 @@ public PlatformEvent maybeCreateEvent() {
229233
}
230234

231235
/**
232-
* For simplicity, we will always create an event based only on single self parent; this is a special rare case
233-
* and it will unblock the network
236+
* For simplicity, we will always create an event based only on single self parent; this is a special rare case and
237+
* it will unblock the network
238+
*
234239
* @return new event based only on self parent
235240
*/
236241
private UnsignedEvent createQuiescenceBreakEvent() {
237-
return buildAndProcessEvent(null);
242+
return buildAndProcessEvent();
238243
}
239244

240245
@Nullable
@@ -243,56 +248,52 @@ private UnsignedEvent maybeCreateUnsignedEvent() {
243248
// Special case: network of size 1.
244249
// We can always create a new event, no need to run the tipset algorithm.
245250
// In a network of size one, we create events without other parents.
246-
return buildAndProcessEvent(null);
251+
return buildAndProcessEvent();
247252
}
248253

249-
final long selfishness = tipsetWeightCalculator.getMaxSelfishnessScore();
250-
tipsetMetrics.getSelfishnessMetric().update(selfishness);
251-
252-
// Never bother with anti-selfishness techniques if we have a selfishness score of 1.
253-
// We are pretty much guaranteed to be selfish to ~1/3 of other nodes by a score of 1.
254-
final double beNiceChance = (selfishness - 1) / antiSelfishnessFactor;
255-
256-
if (beNiceChance > 0 && random.nextDouble() < beNiceChance) {
257-
return createEventToReduceSelfishness();
258-
} else {
259-
return createEventByOptimizingAdvancementWeight();
260-
}
254+
return createEventCombinedAlgorithm();
261255
}
262256

263257
private PlatformEvent signEvent(final UnsignedEvent event) {
264258
return new PlatformEvent(event, signer.sign(event.getHash().getBytes()));
265259
}
266260

267261
/**
268-
* Create an event using the other parent with the best tipset advancement weight.
262+
* Create an event using the other parent with the best tipset advancement weight, possibly mixing a single other
263+
* parent to reduce selfishness.
269264
*
270265
* @return the new event, or null if it is not legal to create a new event
271266
*/
272267
@Nullable
273-
private UnsignedEvent createEventByOptimizingAdvancementWeight() {
268+
private UnsignedEvent createEventCombinedAlgorithm() {
274269
final List<PlatformEvent> possibleOtherParents =
275270
new ArrayList<>(childlessOtherEventTracker.getChildlessEvents());
276271
Collections.shuffle(possibleOtherParents, random);
277272

278-
PlatformEvent bestOtherParent = null;
279-
TipsetAdvancementWeight bestAdvancementWeight = ZERO_ADVANCEMENT_WEIGHT;
280-
for (final PlatformEvent otherParent : possibleOtherParents) {
281-
final List<EventDescriptorWrapper> parents = new ArrayList<>(2);
282-
parents.add(otherParent.getDescriptor());
283-
if (lastSelfEvent != null) {
284-
parents.add(lastSelfEvent.getDescriptor());
285-
}
273+
final List<PlatformEvent> bestParents = possibleOtherParents.stream()
274+
.map(op -> {
275+
final List<EventDescriptorWrapper> parents = new ArrayList<>(2);
276+
parents.add(op.getDescriptor());
277+
if (lastSelfEvent != null) {
278+
parents.add(lastSelfEvent.getDescriptor());
279+
}
280+
return new Pair<>(op, tipsetWeightCalculator.getTheoreticalAdvancementWeight(parents));
281+
})
282+
.filter(p -> p.right().isNonZero())
283+
.sorted(Comparator.comparing(Pair::right))
284+
.map(Pair::left)
285+
.toList();
286286

287-
final TipsetAdvancementWeight advancementWeight =
288-
tipsetWeightCalculator.getTheoreticalAdvancementWeight(parents);
289-
if (advancementWeight.isGreaterThan(bestAdvancementWeight)) {
290-
bestOtherParent = otherParent;
291-
bestAdvancementWeight = advancementWeight;
292-
}
287+
final PlatformEvent[] chosenBestParents;
288+
if (bestParents.size() > maxOtherParents) {
289+
chosenBestParents = bestParents
290+
.subList(bestParents.size() - maxOtherParents, bestParents.size())
291+
.toArray(new PlatformEvent[0]);
292+
} else {
293+
chosenBestParents = bestParents.toArray(new PlatformEvent[0]);
293294
}
294295

295-
if (bestOtherParent == null) {
296+
if (chosenBestParents.length == 0) {
296297
// If there are no available other parents, it is only legal to create a new event if we are
297298
// creating a genesis event. In order to create a genesis event, we must have never created
298299
// an event before and the current event window must have never been advanced.
@@ -302,20 +303,66 @@ private UnsignedEvent createEventByOptimizingAdvancementWeight() {
302303
}
303304

304305
// we are creating a genesis event, so we can use a null other parent
305-
return buildAndProcessEvent(null);
306+
return buildAndProcessEvent();
307+
}
308+
309+
final long selfishness = tipsetWeightCalculator.getMaxSelfishnessScore();
310+
tipsetMetrics.getSelfishnessMetric().update(selfishness);
311+
312+
// Never bother with anti-selfishness techniques if we have a selfishness score of 1.
313+
// We are pretty much guaranteed to be selfish to ~1/3 of other nodes by a score of 1.
314+
final double beNiceChance = (selfishness - 1) / antiSelfishnessFactor;
315+
316+
boolean replacedBestParentForSelfishness = false;
317+
318+
if (beNiceChance > 0 && random.nextDouble() < beNiceChance) {
319+
// replace one of the best parents with the one chosen to reduce selfishness
320+
final PlatformEvent selflessParent = selectParentToReduceSelfishness();
321+
// if we already contain that event, everything is good
322+
if (!contains(chosenBestParents, selflessParent)) {
323+
// otherwise, replace the least important parent with one we have chosen to reduce selfishness
324+
// please note in case of single-parent events, this will replace the only parent
325+
chosenBestParents[chosenBestParents.length - 1] = selflessParent;
326+
replacedBestParentForSelfishness = true;
327+
}
306328
}
307329

308-
tipsetMetrics.getTipsetParentMetric(bestOtherParent.getCreatorId()).cycle();
309-
return buildAndProcessEvent(bestOtherParent);
330+
for (int i = 0; i < chosenBestParents.length; i++) {
331+
if (replacedBestParentForSelfishness && i == chosenBestParents.length - 1) {
332+
tipsetMetrics
333+
.getPityParentMetric(chosenBestParents[i].getCreatorId())
334+
.cycle();
335+
} else {
336+
tipsetMetrics
337+
.getTipsetParentMetric(chosenBestParents[i].getCreatorId())
338+
.cycle();
339+
}
340+
}
341+
342+
return buildAndProcessEvent(chosenBestParents);
343+
}
344+
345+
/**
346+
* Check if given element is present inside the array
347+
* @param array collection to check
348+
* @param element element to look for
349+
* @return true if element is contained in array, false otherwise
350+
*/
351+
private static <T> boolean contains(@NonNull final T[] array, @NonNull final T element) {
352+
for (final T entry : array) {
353+
if (element.equals(entry)) {
354+
return true;
355+
}
356+
}
357+
return false;
310358
}
311359

312360
/**
313-
* Create an event that reduces the selfishness score.
361+
* Select a parent event that reduces the selfishness score.
314362
*
315-
* @return the new event, or null if it is not legal to create a new event
363+
* @return parent to reduce selfishness
316364
*/
317-
@Nullable
318-
private UnsignedEvent createEventToReduceSelfishness() {
365+
private @NonNull PlatformEvent selectParentToReduceSelfishness() {
319366
final Collection<PlatformEvent> possibleOtherParents = childlessOtherEventTracker.getChildlessEvents();
320367
final List<PlatformEvent> ignoredNodes = new ArrayList<>(possibleOtherParents.size());
321368

@@ -374,8 +421,7 @@ private UnsignedEvent createEventToReduceSelfishness() {
374421
runningSum += selfishnessScores.get(i);
375422
if (choice < runningSum) {
376423
final PlatformEvent ignoredNode = ignoredNodes.get(i);
377-
tipsetMetrics.getPityParentMetric(ignoredNode.getCreatorId()).cycle();
378-
return buildAndProcessEvent(ignoredNode);
424+
return ignoredNode;
379425
}
380426
}
381427

@@ -384,14 +430,22 @@ private UnsignedEvent createEventToReduceSelfishness() {
384430
}
385431

386432
/**
387-
* Given an other parent, build the next self event and process it.
433+
* Given the list of other parents, build the next self event and process it.
388434
*
389-
* @param otherParent the other parent, or null if there is no other parent
435+
* @param otherParents the other parents, or zero length arglist if there is no other parents
390436
* @return the new event
391437
*/
392-
private UnsignedEvent buildAndProcessEvent(@Nullable final PlatformEvent otherParent) {
393-
final EventDescriptorWrapper otherParentDescriptor = otherParent == null ? null : otherParent.getDescriptor();
394-
final UnsignedEvent event = assembleEventObject(otherParent);
438+
private UnsignedEvent buildAndProcessEvent(@NonNull final PlatformEvent... otherParents) {
439+
final List<EventDescriptorWrapper> otherParentDescriptors;
440+
441+
if (otherParents.length == 0) {
442+
otherParentDescriptors = List.of();
443+
} else {
444+
otherParentDescriptors = Arrays.stream(otherParents)
445+
.map(PlatformEvent::getDescriptor)
446+
.toList();
447+
}
448+
final UnsignedEvent event = assembleEventObject(otherParents);
395449

396450
tipsetTracker.addSelfEvent(event.getDescriptor(), event.getMetadata().getAllParents());
397451
final TipsetAdvancementWeight advancementWeight =
@@ -400,31 +454,30 @@ private UnsignedEvent buildAndProcessEvent(@Nullable final PlatformEvent otherPa
400454
/ (double) tipsetWeightCalculator.getMaximumPossibleAdvancementWeight();
401455
tipsetMetrics.getTipsetAdvancementMetric().update(weightRatio);
402456

403-
if (otherParent != null) {
404-
childlessOtherEventTracker.registerSelfEventParents(List.of(otherParentDescriptor));
405-
}
457+
childlessOtherEventTracker.registerSelfEventParents(otherParentDescriptors);
406458

407459
return event;
408460
}
409461

410462
/**
411463
* Given the parents, assemble the event object.
412464
*
413-
* @param otherParent the other parent
465+
* @param otherParents list of the other parents
414466
* @return the event
415467
*/
416468
@NonNull
417-
private UnsignedEvent assembleEventObject(@Nullable final PlatformEvent otherParent) {
469+
private UnsignedEvent assembleEventObject(final PlatformEvent... otherParents) {
418470
final List<TimestampedTransaction> transactions = transactionSupplier.getTransactionsForEvent();
419-
final List<EventDescriptorWrapper> allParents = Stream.of(lastSelfEvent, otherParent)
471+
final List<PlatformEvent> allParents = Stream.concat(Stream.of(lastSelfEvent), Stream.of(otherParents))
420472
.filter(Objects::nonNull)
421-
.map(PlatformEvent::getDescriptor)
422473
.toList();
474+
final List<EventDescriptorWrapper> allParentDescriptors =
475+
allParents.stream().map(PlatformEvent::getDescriptor).toList();
423476
final UnsignedEvent event = new UnsignedEvent(
424477
selfId,
425-
allParents,
478+
allParentDescriptors,
426479
eventWindow.newEventBirthRound(),
427-
calculateNewEventCreationTime(lastSelfEvent, otherParent, transactions),
480+
calculateNewEventCreationTime(lastSelfEvent, allParents, transactions),
428481
transactions.stream().map(TimestampedTransaction::transaction).toList(),
429482
random.nextLong(0, roster.rosterEntries().size() + 1));
430483
eventHasher.hashUnsignedEvent(event);
@@ -482,19 +535,17 @@ public String toString() {
482535
* parent to child.
483536
*
484537
* @param selfParent the self parent
485-
* @param otherParent the other parent
538+
* @param allParents list of all the parents, including self parent in the first position
486539
* @param transactions the transactions to be included in the new event
487540
* @return the creation time for the new event
488541
*/
489542
@NonNull
490543
private Instant calculateNewEventCreationTime(
491544
@Nullable final PlatformEvent selfParent,
492-
@Nullable final PlatformEvent otherParent,
545+
@NonNull final List<PlatformEvent> allParents,
493546
@NonNull final List<TimestampedTransaction> transactions) {
494547
final Instant maxReceivedTime = Stream.of(
495-
Stream.of(selfParent, otherParent)
496-
.filter(Objects::nonNull)
497-
.map(PlatformEvent::getTimeReceived),
548+
allParents.stream().map(PlatformEvent::getTimeReceived),
498549
transactions.stream().map(TimestampedTransaction::receivedTime),
499550
Stream.of(lastReceivedEventWindow))
500551
// flatten the stream of streams

platform-sdk/consensus-event-creator-impl/src/test/java/org/hiero/consensus/event/creator/impl/tipset/EventCreationTimeTests.java

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,17 @@ void setup() {
4040
final Roster roster =
4141
RandomRosterBuilder.create(random).withSize(networkSize).build();
4242
transactionPool = new ArrayList<>();
43-
eventCreator = buildEventCreator(random, time, roster, NodeId.of(0), () -> {
44-
final List<TimestampedTransaction> copy = List.copyOf(transactionPool);
45-
transactionPool.clear();
46-
return copy;
47-
});
43+
eventCreator = buildEventCreator(
44+
random,
45+
time,
46+
roster,
47+
NodeId.of(0),
48+
() -> {
49+
final List<TimestampedTransaction> copy = List.copyOf(transactionPool);
50+
transactionPool.clear();
51+
return copy;
52+
},
53+
1);
4854
}
4955

5056
/**

0 commit comments

Comments
 (0)