From 7ae946c7a52f5506d2de1cb5b3b880a4e7e52e66 Mon Sep 17 00:00:00 2001 From: Austin Littley Date: Fri, 27 Oct 2023 12:03:53 -0400 Subject: [PATCH 1/3] Implement internal event validator Signed-off-by: Austin Littley --- .../validation/InternalEventValidator.java | 255 ++++++++++++++++++ 1 file changed, 255 insertions(+) create mode 100644 platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/event/validation/InternalEventValidator.java diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/event/validation/InternalEventValidator.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/event/validation/InternalEventValidator.java new file mode 100644 index 000000000000..f69d99f752cb --- /dev/null +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/event/validation/InternalEventValidator.java @@ -0,0 +1,255 @@ +/* + * Copyright (C) 2023 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.swirlds.platform.event.validation; + +import static com.swirlds.common.metrics.Metrics.PLATFORM_CATEGORY; +import static com.swirlds.logging.LogMarker.INVALID_EVENT_ERROR; +import static com.swirlds.platform.consensus.GraphGenerations.FIRST_GENERATION; + +import com.swirlds.base.time.Time; +import com.swirlds.common.config.TransactionConfig; +import com.swirlds.common.context.PlatformContext; +import com.swirlds.common.crypto.Hash; +import com.swirlds.common.metrics.LongAccumulator; +import com.swirlds.common.system.events.BaseEventHashedData; +import com.swirlds.common.system.transaction.ConsensusTransaction; +import com.swirlds.common.utility.throttle.RateLimitedLogger; +import com.swirlds.platform.event.GossipEvent; +import com.swirlds.platform.gossip.IntakeEventCounter; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.time.Duration; +import java.util.Objects; +import java.util.function.Consumer; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** + * Validates that events are internally complete and consistent. + */ +public class InternalEventValidator { + private static final Logger logger = LogManager.getLogger(InternalEventValidator.class); + + /** + * The minimum period between log messages for a specific mode of failure. + */ + private static final Duration MINIMUM_LOG_PERIOD = Duration.ofMinutes(1); + + /** + * Whether this node is in a single-node network. + */ + private final boolean singleNodeNetwork; + + /** + * Valid events are passed to this consumer. + */ + private final Consumer eventConsumer; + + /** + * Keeps track of the number of events in the intake pipeline from each peer + */ + private final IntakeEventCounter intakeEventCounter; + + private final TransactionConfig transactionConfig; + + private final RateLimitedLogger nullHashedDataLogger; + private final RateLimitedLogger nullUnhashedDataLogger; + private final RateLimitedLogger tooManyTransactionBytesLogger; + private final RateLimitedLogger inconsistentSelfParentLogger; + private final RateLimitedLogger inconsistentOtherParentLogger; + private final RateLimitedLogger identicalParentsLogger; + + private final LongAccumulator nullHashedDataAccumulator; + private final LongAccumulator nullUnhashedDataAccumulator; + private final LongAccumulator tooManyTransactionBytesAccumulator; + private final LongAccumulator inconsistentSelfParentAccumulator; + private final LongAccumulator inconsistentOtherParentAccumulator; + private final LongAccumulator identicalParentsAccumulator; + + /** + * Constructor + * + * @param platformContext the platform context + * @param time a time object, for rate limiting logging + * @param singleNodeNetwork true if this node is in a single-node network, otherwise false + * @param eventConsumer validated events are passed to this consumer + * @param intakeEventCounter keeps track of the number of events in the intake pipeline from each peer + */ + public InternalEventValidator( + @NonNull final PlatformContext platformContext, + @NonNull final Time time, + final boolean singleNodeNetwork, + @NonNull final Consumer eventConsumer, + @NonNull final IntakeEventCounter intakeEventCounter) { + + Objects.requireNonNull(time); + + this.singleNodeNetwork = singleNodeNetwork; + this.eventConsumer = Objects.requireNonNull(eventConsumer); + this.intakeEventCounter = Objects.requireNonNull(intakeEventCounter); + + this.transactionConfig = platformContext.getConfiguration().getConfigData(TransactionConfig.class); + + this.nullHashedDataLogger = new RateLimitedLogger(logger, time, MINIMUM_LOG_PERIOD); + this.nullUnhashedDataLogger = new RateLimitedLogger(logger, time, MINIMUM_LOG_PERIOD); + this.tooManyTransactionBytesLogger = new RateLimitedLogger(logger, time, MINIMUM_LOG_PERIOD); + this.inconsistentSelfParentLogger = new RateLimitedLogger(logger, time, MINIMUM_LOG_PERIOD); + this.inconsistentOtherParentLogger = new RateLimitedLogger(logger, time, MINIMUM_LOG_PERIOD); + this.identicalParentsLogger = new RateLimitedLogger(logger, time, MINIMUM_LOG_PERIOD); + + this.nullHashedDataAccumulator = platformContext + .getMetrics() + .getOrCreate(new LongAccumulator.Config(PLATFORM_CATEGORY, "eventsWithNullHashedData") + .withDescription("Events that had null hashed data") + .withUnit("events")); + this.nullUnhashedDataAccumulator = platformContext + .getMetrics() + .getOrCreate(new LongAccumulator.Config(PLATFORM_CATEGORY, "eventsWithNullUnhashedData") + .withDescription("Events that had null unhashed data") + .withUnit("events")); + this.tooManyTransactionBytesAccumulator = platformContext + .getMetrics() + .getOrCreate(new LongAccumulator.Config(PLATFORM_CATEGORY, "eventsWithTooManyTransactionBytes") + .withDescription("Events that had more transaction bytes than permitted") + .withUnit("events")); + this.inconsistentSelfParentAccumulator = platformContext + .getMetrics() + .getOrCreate(new LongAccumulator.Config(PLATFORM_CATEGORY, "eventsWithInconsistentSelfParent") + .withDescription("Events that had an internal self-parent inconsistency") + .withUnit("events")); + this.inconsistentOtherParentAccumulator = platformContext + .getMetrics() + .getOrCreate(new LongAccumulator.Config(PLATFORM_CATEGORY, "eventsWithInconsistentOtherParent") + .withDescription("Events that had an internal other-parent inconsistency") + .withUnit("events")); + this.identicalParentsAccumulator = platformContext + .getMetrics() + .getOrCreate(new LongAccumulator.Config(PLATFORM_CATEGORY, "eventsWithIdenticalParents") + .withDescription("Events with identical self-parent and other-parent hash") + .withUnit("events")); + } + + /** + * Checks whether the required fields of an event are non-null. + * + * @param event the event to check + * @return true if the required fields of the event are non-null, otherwise false + */ + private boolean areRequiredFieldsNonNull(@NonNull final GossipEvent event) { + if (event.getHashedData() == null) { + // do not log the event itself, since toString would throw a NullPointerException + nullHashedDataLogger.error(INVALID_EVENT_ERROR.getMarker(), "Event has null hashed data"); + nullHashedDataAccumulator.update(1); + return false; + } + + if (event.getUnhashedData() == null) { + // do not log the event itself, since toString would throw a NullPointerException + nullUnhashedDataLogger.error(INVALID_EVENT_ERROR.getMarker(), "Event has null unhashed data"); + nullUnhashedDataAccumulator.update(1); + return false; + } + + return true; + } + + /** + * Checks whether the total byte count of all transactions in an event is less than the maximum. + * + * @param event the event to check + * @return true if the total byte count of transactions in the event is less than the maximum, otherwise false + */ + private boolean isTransactionByteCountValid(@NonNull final GossipEvent event) { + int totalTransactionBytes = 0; + for (final ConsensusTransaction transaction : event.getHashedData().getTransactions()) { + totalTransactionBytes += transaction.getSerializedLength(); + } + + if (totalTransactionBytes > transactionConfig.transactionMaxBytes()) { + tooManyTransactionBytesLogger.error( + INVALID_EVENT_ERROR.getMarker(), + "Event %s has %s transaction bytes, which is more than permitted" + .formatted(event, totalTransactionBytes)); + tooManyTransactionBytesAccumulator.update(1); + return false; + } + + return true; + } + + /** + * Checks whether the parent hashes and generations of an event are internally consistent. + * + * @param event the event to check + * @return true if the parent hashes and generations of the event are internally consistent, otherwise false + */ + private boolean areParentsInternallyConsistent(@NonNull final GossipEvent event) { + final BaseEventHashedData hashedData = event.getHashedData(); + + // If a parent hash is missing, then the generation must also be invalid. + // If a parent hash is not missing, then the generation must be valid. + + final Hash selfParentHash = hashedData.getSelfParentHash(); + final long selfParentGeneration = hashedData.getSelfParentGen(); + if ((selfParentHash == null) != (selfParentGeneration < FIRST_GENERATION)) { + inconsistentSelfParentLogger.error( + INVALID_EVENT_ERROR.getMarker(), + "Event %s has inconsistent self-parent hash and generation. Self-parent hash: %s, self-parent generation: %s" + .formatted(event, selfParentHash, selfParentGeneration)); + inconsistentSelfParentAccumulator.update(1); + return false; + } + + final Hash otherParentHash = hashedData.getOtherParentHash(); + final long otherParentGeneration = hashedData.getOtherParentGen(); + if ((otherParentHash == null) != (otherParentGeneration < FIRST_GENERATION)) { + inconsistentOtherParentLogger.error( + INVALID_EVENT_ERROR.getMarker(), + "Event %s has inconsistent other-parent hash and generation. Other-parent hash: %s, other-parent generation: %s" + .formatted(event, otherParentHash, otherParentGeneration)); + inconsistentOtherParentAccumulator.update(1); + return false; + } + + // single node networks are allowed to have identical self-parent and other-parent hashes + if (!singleNodeNetwork && selfParentHash != null && selfParentHash.equals(otherParentHash)) { + identicalParentsLogger.error( + INVALID_EVENT_ERROR.getMarker(), + "Event %s has identical self-parent and other-parent hash: %s".formatted(event, selfParentHash)); + identicalParentsAccumulator.update(1); + return false; + } + + return true; + } + + /** + * Validate the internal data integrity of an event. + *

+ * If the event is determined to be valid, it is passed to the event consumer. + * + * @param event the event to validate + */ + public void handleEvent(@NonNull final GossipEvent event) { + if (areRequiredFieldsNonNull(event) + && isTransactionByteCountValid(event) + && areParentsInternallyConsistent(event)) { + eventConsumer.accept(event); + } else { + intakeEventCounter.eventExitedIntakePipeline(event.getSenderId()); + } + } +} From fe63955a91ca4cbb87ca415d8928a3d56ae99d26 Mon Sep 17 00:00:00 2001 From: Austin Littley Date: Fri, 27 Oct 2023 12:43:10 -0400 Subject: [PATCH 2/3] Test internal event validator Signed-off-by: Austin Littley --- .../InternalEventValidatorTests.java | 210 ++++++++++++++++++ 1 file changed, 210 insertions(+) create mode 100644 platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/event/validation/InternalEventValidatorTests.java diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/event/validation/InternalEventValidatorTests.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/event/validation/InternalEventValidatorTests.java new file mode 100644 index 000000000000..2a712b1ad638 --- /dev/null +++ b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/event/validation/InternalEventValidatorTests.java @@ -0,0 +1,210 @@ +/* + * Copyright (C) 2023 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.swirlds.platform.event.validation; + +import static com.swirlds.common.test.fixtures.RandomUtils.getRandomPrintSeed; +import static com.swirlds.common.test.fixtures.RandomUtils.randomHash; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.swirlds.base.test.fixtures.time.FakeTime; +import com.swirlds.base.time.Time; +import com.swirlds.common.context.PlatformContext; +import com.swirlds.common.crypto.Hash; +import com.swirlds.common.system.events.BaseEventHashedData; +import com.swirlds.common.system.events.BaseEventUnhashedData; +import com.swirlds.common.system.transaction.internal.ConsensusTransactionImpl; +import com.swirlds.platform.event.GossipEvent; +import com.swirlds.platform.gossip.IntakeEventCounter; +import com.swirlds.test.framework.context.TestPlatformContextBuilder; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.util.Random; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Consumer; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +/** + * Tests for {@link LinkedEventValidator} + */ +class InternalEventValidatorTests { + private AtomicInteger consumedEventCount; + private AtomicLong exitedIntakePipelineCount; + private Random random; + private InternalEventValidator multinodeValidator; + private InternalEventValidator singleNodeValidator; + + @BeforeEach + void setup() { + random = getRandomPrintSeed(); + + exitedIntakePipelineCount = new AtomicLong(0); + final IntakeEventCounter intakeEventCounter = mock(IntakeEventCounter.class); + doAnswer(invocation -> { + exitedIntakePipelineCount.incrementAndGet(); + return null; + }) + .when(intakeEventCounter) + .eventExitedIntakePipeline(any()); + + final PlatformContext platformContext = + TestPlatformContextBuilder.create().build(); + + consumedEventCount = new AtomicInteger(0); + final Consumer eventConsumer = event -> consumedEventCount.incrementAndGet(); + + final Time time = new FakeTime(); + + multinodeValidator = + new InternalEventValidator(platformContext, time, false, eventConsumer, intakeEventCounter); + singleNodeValidator = + new InternalEventValidator(platformContext, time, true, eventConsumer, intakeEventCounter); + } + + private GossipEvent generateEvent( + @Nullable final Hash selfParentHash, + @Nullable final Hash otherParentHash, + final long selfParentGeneration, + final long otherParentGeneration, + final int totalTransactionBytes) { + + final ConsensusTransactionImpl[] transactions = new ConsensusTransactionImpl[100]; + for (int index = 0; index < transactions.length; index++) { + transactions[index] = mock(ConsensusTransactionImpl.class); + when(transactions[index].getSerializedLength()).thenReturn(totalTransactionBytes / transactions.length); + } + + final BaseEventHashedData hashedData = mock(BaseEventHashedData.class); + when(hashedData.getSelfParentHash()).thenReturn(selfParentHash); + when(hashedData.getOtherParentHash()).thenReturn(otherParentHash); + when(hashedData.getSelfParentGen()).thenReturn(selfParentGeneration); + when(hashedData.getOtherParentGen()).thenReturn(otherParentGeneration); + when(hashedData.getTransactions()).thenReturn(transactions); + + final BaseEventUnhashedData unhashedData = mock(BaseEventUnhashedData.class); + + final GossipEvent event = mock(GossipEvent.class); + when(event.getHashedData()).thenReturn(hashedData); + when(event.getUnhashedData()).thenReturn(unhashedData); + + return event; + } + + @Test + @DisplayName("An event with null hashed data is invalid") + void nullHashedData() { + final GossipEvent event = generateEvent(randomHash(random), randomHash(random), 5, 6, 1111); + when(event.getHashedData()).thenReturn(null); + + multinodeValidator.handleEvent(event); + singleNodeValidator.handleEvent(event); + + assertEquals(0, consumedEventCount.get()); + assertEquals(2, exitedIntakePipelineCount.get()); + } + + @Test + @DisplayName("An event with null unhashed data is invalid") + void nullUnhashedData() { + final GossipEvent event = generateEvent(randomHash(random), randomHash(random), 5, 6, 1111); + when(event.getUnhashedData()).thenReturn(null); + + multinodeValidator.handleEvent(event); + singleNodeValidator.handleEvent(event); + + assertEquals(0, consumedEventCount.get()); + assertEquals(2, exitedIntakePipelineCount.get()); + } + + @Test + @DisplayName("An event with too many transaction bytes is invalid") + void tooManyTransactionBytes() { + // default max is 245_760 bytes + final GossipEvent event = generateEvent(randomHash(random), randomHash(random), 5, 6, 500_000); + + multinodeValidator.handleEvent(event); + singleNodeValidator.handleEvent(event); + + assertEquals(0, consumedEventCount.get()); + assertEquals(2, exitedIntakePipelineCount.get()); + } + + @Test + @DisplayName("An event with parent inconsistency is invalid") + void inconsistentParents() { + // has null self parent hash, but valid self parent generation + final GossipEvent nullSelfParentHash = generateEvent(null, randomHash(random), 5, 6, 1111); + // has valid self parent hash, but invalid self parent generation + final GossipEvent invalidSelfParentGeneration = + generateEvent(randomHash(random), randomHash(random), -1, 6, 1111); + // has null other parent hash, but valid other parent generation + final GossipEvent nullOtherParentHash = generateEvent(randomHash(random), null, 5, 6, 1111); + // has valid other parent hash, but invalid other parent generation + final GossipEvent invalidOtherParentGeneration = + generateEvent(randomHash(random), randomHash(random), 5, -1, 1111); + + multinodeValidator.handleEvent(nullSelfParentHash); + multinodeValidator.handleEvent(invalidSelfParentGeneration); + multinodeValidator.handleEvent(nullOtherParentHash); + multinodeValidator.handleEvent(invalidOtherParentGeneration); + + singleNodeValidator.handleEvent(nullSelfParentHash); + singleNodeValidator.handleEvent(invalidSelfParentGeneration); + singleNodeValidator.handleEvent(nullOtherParentHash); + singleNodeValidator.handleEvent(invalidOtherParentGeneration); + + assertEquals(0, consumedEventCount.get()); + assertEquals(8, exitedIntakePipelineCount.get()); + } + + @Test + @DisplayName("An event with identical parents is only valid in a single node network") + void identicalParents() { + final Hash sharedHash = randomHash(random); + final GossipEvent event = generateEvent(sharedHash, sharedHash, 5, 6, 1111); + + multinodeValidator.handleEvent(event); + singleNodeValidator.handleEvent(event); + + assertEquals(1, consumedEventCount.get()); + assertEquals(1, exitedIntakePipelineCount.get()); + } + + @Test + @DisplayName("Test that an event with no issues passes validation") + void successfulValidation() { + final GossipEvent normalEvent = generateEvent(randomHash(random), randomHash(random), 5, 6, 1111); + final GossipEvent missingSelfParent = generateEvent(null, randomHash(random), -1, 6, 1111); + final GossipEvent missingOtherParent = generateEvent(randomHash(random), null, 5, -1, 1111); + + multinodeValidator.handleEvent(normalEvent); + multinodeValidator.handleEvent(missingSelfParent); + multinodeValidator.handleEvent(missingOtherParent); + + singleNodeValidator.handleEvent(normalEvent); + singleNodeValidator.handleEvent(missingSelfParent); + singleNodeValidator.handleEvent(missingOtherParent); + + assertEquals(6, consumedEventCount.get()); + assertEquals(0, exitedIntakePipelineCount.get()); + } +} From 5f0f05fbac7066e75cc8a7356850faa251722629 Mon Sep 17 00:00:00 2001 From: Austin Littley Date: Fri, 27 Oct 2023 15:59:27 -0400 Subject: [PATCH 3/3] Add check for valid event generation Signed-off-by: Austin Littley --- .../validation/InternalEventValidator.java | 35 ++++++++++++++++- .../InternalEventValidatorTests.java | 39 +++++++++++++------ 2 files changed, 62 insertions(+), 12 deletions(-) diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/event/validation/InternalEventValidator.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/event/validation/InternalEventValidator.java index f69d99f752cb..262052b97a26 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/event/validation/InternalEventValidator.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/event/validation/InternalEventValidator.java @@ -71,6 +71,7 @@ public class InternalEventValidator { private final RateLimitedLogger inconsistentSelfParentLogger; private final RateLimitedLogger inconsistentOtherParentLogger; private final RateLimitedLogger identicalParentsLogger; + private final RateLimitedLogger invalidGenerationLogger; private final LongAccumulator nullHashedDataAccumulator; private final LongAccumulator nullUnhashedDataAccumulator; @@ -78,6 +79,7 @@ public class InternalEventValidator { private final LongAccumulator inconsistentSelfParentAccumulator; private final LongAccumulator inconsistentOtherParentAccumulator; private final LongAccumulator identicalParentsAccumulator; + private final LongAccumulator invalidGenerationAccumulator; /** * Constructor @@ -109,6 +111,7 @@ public InternalEventValidator( this.inconsistentSelfParentLogger = new RateLimitedLogger(logger, time, MINIMUM_LOG_PERIOD); this.inconsistentOtherParentLogger = new RateLimitedLogger(logger, time, MINIMUM_LOG_PERIOD); this.identicalParentsLogger = new RateLimitedLogger(logger, time, MINIMUM_LOG_PERIOD); + this.invalidGenerationLogger = new RateLimitedLogger(logger, time, MINIMUM_LOG_PERIOD); this.nullHashedDataAccumulator = platformContext .getMetrics() @@ -140,6 +143,11 @@ public InternalEventValidator( .getOrCreate(new LongAccumulator.Config(PLATFORM_CATEGORY, "eventsWithIdenticalParents") .withDescription("Events with identical self-parent and other-parent hash") .withUnit("events")); + this.invalidGenerationAccumulator = platformContext + .getMetrics() + .getOrCreate(new LongAccumulator.Config(PLATFORM_CATEGORY, "eventsWithInvalidGeneration") + .withDescription("Events with an invalid generation") + .withUnit("events")); } /** @@ -236,6 +244,30 @@ private boolean areParentsInternallyConsistent(@NonNull final GossipEvent event) return true; } + /** + * Checks whether the generation of an event is valid. A valid generation is one greater than the maximum generation + * of the event's parents. + * + * @param event the event to check + * @return true if the generation of the event is valid, otherwise false + */ + private boolean isEventGenerationValid(@NonNull final GossipEvent event) { + final long eventGeneration = event.getGeneration(); + final long selfParentGeneration = event.getHashedData().getSelfParentGen(); + final long otherParentGeneration = event.getHashedData().getOtherParentGen(); + + if (eventGeneration != Math.max(selfParentGeneration, otherParentGeneration) + 1) { + invalidGenerationLogger.error( + INVALID_EVENT_ERROR.getMarker(), + "Event %s has an invalid generation. Event generation: %s, self-parent generation: %s, other-parent generation: %s" + .formatted(event, eventGeneration, selfParentGeneration, otherParentGeneration)); + invalidGenerationAccumulator.update(1); + return false; + } + + return true; + } + /** * Validate the internal data integrity of an event. *

@@ -246,7 +278,8 @@ private boolean areParentsInternallyConsistent(@NonNull final GossipEvent event) public void handleEvent(@NonNull final GossipEvent event) { if (areRequiredFieldsNonNull(event) && isTransactionByteCountValid(event) - && areParentsInternallyConsistent(event)) { + && areParentsInternallyConsistent(event) + && isEventGenerationValid(event)) { eventConsumer.accept(event); } else { intakeEventCounter.eventExitedIntakePipeline(event.getSenderId()); diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/event/validation/InternalEventValidatorTests.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/event/validation/InternalEventValidatorTests.java index 2a712b1ad638..4715e3c44cd2 100644 --- a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/event/validation/InternalEventValidatorTests.java +++ b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/event/validation/InternalEventValidatorTests.java @@ -83,6 +83,7 @@ void setup() { private GossipEvent generateEvent( @Nullable final Hash selfParentHash, @Nullable final Hash otherParentHash, + final long eventGeneration, final long selfParentGeneration, final long otherParentGeneration, final int totalTransactionBytes) { @@ -105,6 +106,7 @@ private GossipEvent generateEvent( final GossipEvent event = mock(GossipEvent.class); when(event.getHashedData()).thenReturn(hashedData); when(event.getUnhashedData()).thenReturn(unhashedData); + when(event.getGeneration()).thenReturn(eventGeneration); return event; } @@ -112,7 +114,7 @@ private GossipEvent generateEvent( @Test @DisplayName("An event with null hashed data is invalid") void nullHashedData() { - final GossipEvent event = generateEvent(randomHash(random), randomHash(random), 5, 6, 1111); + final GossipEvent event = generateEvent(randomHash(random), randomHash(random), 7, 5, 6, 1111); when(event.getHashedData()).thenReturn(null); multinodeValidator.handleEvent(event); @@ -125,7 +127,7 @@ void nullHashedData() { @Test @DisplayName("An event with null unhashed data is invalid") void nullUnhashedData() { - final GossipEvent event = generateEvent(randomHash(random), randomHash(random), 5, 6, 1111); + final GossipEvent event = generateEvent(randomHash(random), randomHash(random), 7, 5, 6, 1111); when(event.getUnhashedData()).thenReturn(null); multinodeValidator.handleEvent(event); @@ -139,7 +141,7 @@ void nullUnhashedData() { @DisplayName("An event with too many transaction bytes is invalid") void tooManyTransactionBytes() { // default max is 245_760 bytes - final GossipEvent event = generateEvent(randomHash(random), randomHash(random), 5, 6, 500_000); + final GossipEvent event = generateEvent(randomHash(random), randomHash(random), 7, 5, 6, 500_000); multinodeValidator.handleEvent(event); singleNodeValidator.handleEvent(event); @@ -152,15 +154,15 @@ void tooManyTransactionBytes() { @DisplayName("An event with parent inconsistency is invalid") void inconsistentParents() { // has null self parent hash, but valid self parent generation - final GossipEvent nullSelfParentHash = generateEvent(null, randomHash(random), 5, 6, 1111); + final GossipEvent nullSelfParentHash = generateEvent(null, randomHash(random), 7, 5, 6, 1111); // has valid self parent hash, but invalid self parent generation final GossipEvent invalidSelfParentGeneration = - generateEvent(randomHash(random), randomHash(random), -1, 6, 1111); + generateEvent(randomHash(random), randomHash(random), -1, 7, 6, 1111); // has null other parent hash, but valid other parent generation - final GossipEvent nullOtherParentHash = generateEvent(randomHash(random), null, 5, 6, 1111); + final GossipEvent nullOtherParentHash = generateEvent(randomHash(random), null, 7, 5, 6, 1111); // has valid other parent hash, but invalid other parent generation final GossipEvent invalidOtherParentGeneration = - generateEvent(randomHash(random), randomHash(random), 5, -1, 1111); + generateEvent(randomHash(random), randomHash(random), 6, 5, -1, 1111); multinodeValidator.handleEvent(nullSelfParentHash); multinodeValidator.handleEvent(invalidSelfParentGeneration); @@ -180,7 +182,7 @@ void inconsistentParents() { @DisplayName("An event with identical parents is only valid in a single node network") void identicalParents() { final Hash sharedHash = randomHash(random); - final GossipEvent event = generateEvent(sharedHash, sharedHash, 5, 6, 1111); + final GossipEvent event = generateEvent(sharedHash, sharedHash, 7, 5, 6, 1111); multinodeValidator.handleEvent(event); singleNodeValidator.handleEvent(event); @@ -189,12 +191,27 @@ void identicalParents() { assertEquals(1, exitedIntakePipelineCount.get()); } + @Test + @DisplayName("An event must have a generation of the max parent generation + 1") + void invalidGeneration() { + final GossipEvent highGeneration = generateEvent(randomHash(random), randomHash(random), 8, 5, 6, 1111); + final GossipEvent lowGeneration = generateEvent(randomHash(random), randomHash(random), 4, 5, 6, 1111); + + multinodeValidator.handleEvent(highGeneration); + multinodeValidator.handleEvent(lowGeneration); + singleNodeValidator.handleEvent(highGeneration); + singleNodeValidator.handleEvent(lowGeneration); + + assertEquals(0, consumedEventCount.get()); + assertEquals(4, exitedIntakePipelineCount.get()); + } + @Test @DisplayName("Test that an event with no issues passes validation") void successfulValidation() { - final GossipEvent normalEvent = generateEvent(randomHash(random), randomHash(random), 5, 6, 1111); - final GossipEvent missingSelfParent = generateEvent(null, randomHash(random), -1, 6, 1111); - final GossipEvent missingOtherParent = generateEvent(randomHash(random), null, 5, -1, 1111); + final GossipEvent normalEvent = generateEvent(randomHash(random), randomHash(random), 7, 5, 6, 1111); + final GossipEvent missingSelfParent = generateEvent(null, randomHash(random), 7, -1, 6, 1111); + final GossipEvent missingOtherParent = generateEvent(randomHash(random), null, 6, 5, -1, 1111); multinodeValidator.handleEvent(normalEvent); multinodeValidator.handleEvent(missingSelfParent);