diff --git a/platform-sdk/platform-apps/tests/ConsistencyTestingTool/build.gradle.kts b/platform-sdk/platform-apps/tests/ConsistencyTestingTool/build.gradle.kts index 963444f299ae..2780d6de7a90 100644 --- a/platform-sdk/platform-apps/tests/ConsistencyTestingTool/build.gradle.kts +++ b/platform-sdk/platform-apps/tests/ConsistencyTestingTool/build.gradle.kts @@ -1,4 +1,19 @@ -// SPDX-License-Identifier: Apache-2.0 +/* + * Copyright (C) 2025 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. + */ + plugins { id("org.hiero.gradle.module.application") } application.mainClass = "com.swirlds.demo.consistency.ConsistencyTestingToolMain" @@ -7,6 +22,7 @@ mainModuleInfo { annotationProcessor("com.swirlds.config.processor") } testModuleInfo { requires("com.swirlds.common.test.fixtures") + requires("org.assertj.core") requires("org.junit.jupiter.api") requires("org.mockito") } diff --git a/platform-sdk/platform-apps/tests/ConsistencyTestingTool/src/main/java/com/swirlds/demo/consistency/ConsistencyTestingToolMain.java b/platform-sdk/platform-apps/tests/ConsistencyTestingTool/src/main/java/com/swirlds/demo/consistency/ConsistencyTestingToolMain.java index 9ae84f755a38..54b70d5fe38c 100644 --- a/platform-sdk/platform-apps/tests/ConsistencyTestingTool/src/main/java/com/swirlds/demo/consistency/ConsistencyTestingToolMain.java +++ b/platform-sdk/platform-apps/tests/ConsistencyTestingTool/src/main/java/com/swirlds/demo/consistency/ConsistencyTestingToolMain.java @@ -20,6 +20,8 @@ import static com.swirlds.platform.test.fixtures.state.FakeStateLifecycles.FAKE_MERKLE_STATE_LIFECYCLES; import static com.swirlds.platform.test.fixtures.state.FakeStateLifecycles.registerMerkleStateRootClassIds; +import com.hedera.hapi.platform.event.StateSignatureTransaction; +import com.hedera.pbj.runtime.io.buffer.Bytes; import com.swirlds.common.constructable.ClassConstructorPair; import com.swirlds.common.constructable.ConstructableRegistry; import com.swirlds.common.constructable.ConstructableRegistryException; @@ -139,4 +141,9 @@ public SoftwareVersion getSoftwareVersion() { public List> getConfigDataTypes() { return List.of(ConsistencyTestingToolConfig.class); } + + @Override + public Bytes encodeSystemTransaction(final @NonNull StateSignatureTransaction transaction) { + return StateSignatureTransaction.PROTOBUF.toBytes(transaction); + } } diff --git a/platform-sdk/platform-apps/tests/ConsistencyTestingTool/src/main/java/com/swirlds/demo/consistency/ConsistencyTestingToolState.java b/platform-sdk/platform-apps/tests/ConsistencyTestingTool/src/main/java/com/swirlds/demo/consistency/ConsistencyTestingToolState.java index 455daa66c925..78117e97e578 100644 --- a/platform-sdk/platform-apps/tests/ConsistencyTestingTool/src/main/java/com/swirlds/demo/consistency/ConsistencyTestingToolState.java +++ b/platform-sdk/platform-apps/tests/ConsistencyTestingTool/src/main/java/com/swirlds/demo/consistency/ConsistencyTestingToolState.java @@ -23,6 +23,7 @@ import com.hedera.hapi.node.base.SemanticVersion; import com.hedera.hapi.platform.event.StateSignatureTransaction; +import com.hedera.pbj.runtime.ParseException; import com.swirlds.common.config.StateCommonConfig; import com.swirlds.common.constructable.ConstructableIgnored; import com.swirlds.common.utility.NonCryptographicHashing; @@ -36,6 +37,7 @@ import com.swirlds.platform.system.SoftwareVersion; import com.swirlds.platform.system.events.Event; import com.swirlds.platform.system.transaction.ConsensusTransaction; +import com.swirlds.platform.system.transaction.Transaction; import com.swirlds.state.merkle.singleton.StringLeaf; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; @@ -250,6 +252,11 @@ public void preHandle( if (transaction.isSystem()) { return; } + + if (isSystemTransaction(transaction)) { + consumeSystemTransaction(transaction, event, stateSignatureTransaction); + return; + } final long transactionContents = byteArrayToLong(transaction.getApplicationTransaction().toByteArray(), 0); @@ -284,7 +291,13 @@ public void handleConsensusRound( roundsHandled++; - round.forEachTransaction(this::applyTransactionToState); + round.forEachEventTransaction((ev, tx) -> { + if (isSystemTransaction(tx)) { + consumeSystemTransaction(tx, ev, stateSignatureTransaction); + } else { + applyTransactionToState(tx); + } + }); stateLong = NonCryptographicHashing.hash64(stateLong, round.getRoundNum()); transactionHandlingHistory.processRound(ConsistencyTestingToolRound.fromRound(round, stateLong)); @@ -292,4 +305,29 @@ public void handleConsensusRound( setChild(ROUND_HANDLED_INDEX, new StringLeaf(Long.toString(roundsHandled))); setChild(STATE_LONG_INDEX, new StringLeaf(Long.toString(stateLong))); } + + /** + * Determines if the given transaction is a system transaction for this app. + * + * @param transaction the transaction to check + * @return true if the transaction is a system transaction, false otherwise + */ + private boolean isSystemTransaction(final @NonNull Transaction transaction) { + return transaction.getApplicationTransaction().length() > 8; + } + + private void consumeSystemTransaction( + final @NonNull Transaction transaction, + final @NonNull Event event, + final @NonNull Consumer> + stateSignatureTransactionCallback) { + try { + final var stateSignatureTransaction = + StateSignatureTransaction.PROTOBUF.parse(transaction.getApplicationTransaction()); + stateSignatureTransactionCallback.accept(new ScopedSystemTransaction<>( + event.getCreatorId(), event.getSoftwareVersion(), stateSignatureTransaction)); + } catch (final ParseException e) { + logger.error("Failed to parse StateSignatureTransaction", e); + } + } } diff --git a/platform-sdk/platform-apps/tests/ConsistencyTestingTool/src/test/java/com/swirlds/demo/consistency/ConsistencyTestingToolStateTest.java b/platform-sdk/platform-apps/tests/ConsistencyTestingTool/src/test/java/com/swirlds/demo/consistency/ConsistencyTestingToolStateTest.java new file mode 100644 index 000000000000..23a06e6bb7ad --- /dev/null +++ b/platform-sdk/platform-apps/tests/ConsistencyTestingTool/src/test/java/com/swirlds/demo/consistency/ConsistencyTestingToolStateTest.java @@ -0,0 +1,279 @@ +/* + * Copyright (C) 2025 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.demo.consistency; + +import static com.swirlds.platform.test.fixtures.state.FakeStateLifecycles.FAKE_MERKLE_STATE_LIFECYCLES; +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; +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.hedera.hapi.platform.event.StateSignatureTransaction; +import com.hedera.pbj.runtime.io.buffer.Bytes; +import com.swirlds.common.config.StateCommonConfig; +import com.swirlds.common.context.PlatformContext; +import com.swirlds.common.platform.NodeId; +import com.swirlds.config.api.Configuration; +import com.swirlds.platform.components.transaction.system.ScopedSystemTransaction; +import com.swirlds.platform.state.PlatformStateModifier; +import com.swirlds.platform.system.BasicSoftwareVersion; +import com.swirlds.platform.system.InitTrigger; +import com.swirlds.platform.system.Platform; +import com.swirlds.platform.system.Round; +import com.swirlds.platform.system.SoftwareVersion; +import com.swirlds.platform.system.events.ConsensusEvent; +import com.swirlds.platform.system.transaction.Transaction; +import com.swirlds.platform.system.transaction.TransactionWrapper; +import java.nio.file.Path; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Function; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class ConsistencyTestingToolStateTest { + + private static ConsistencyTestingToolState state; + private Random random; + private PlatformStateModifier platformStateModifier; + private Platform platform; + private PlatformContext platformContext; + private Round round; + private ConsensusEvent event; + private List> consumedTransactions; + private Consumer> consumer; + private Transaction consensusTransaction; + private StateSignatureTransaction stateSignatureTransaction; + private InitTrigger initTrigger; + private SoftwareVersion softwareVersion; + private Configuration configuration; + private ConsistencyTestingToolConfig consistencyTestingToolConfig; + private StateCommonConfig stateCommonConfig; + + @BeforeAll + static void initState() { + state = new ConsistencyTestingToolState(FAKE_MERKLE_STATE_LIFECYCLES, mock(Function.class)); + FAKE_MERKLE_STATE_LIFECYCLES.initStates(state); + } + + @BeforeEach + void setUp() { + platform = mock(Platform.class); + initTrigger = InitTrigger.GENESIS; + softwareVersion = new BasicSoftwareVersion(1); + platformContext = mock(PlatformContext.class); + configuration = mock(Configuration.class); + consistencyTestingToolConfig = mock(ConsistencyTestingToolConfig.class); + stateCommonConfig = mock(StateCommonConfig.class); + + when(platform.getSelfId()).thenReturn(NodeId.of(1L)); + when(platform.getContext()).thenReturn(platformContext); + when(platformContext.getConfiguration()).thenReturn(configuration); + when(configuration.getConfigData(ConsistencyTestingToolConfig.class)).thenReturn(consistencyTestingToolConfig); + when(configuration.getConfigData(StateCommonConfig.class)).thenReturn(stateCommonConfig); + when(consistencyTestingToolConfig.freezeAfterGenesis()).thenReturn(Duration.ZERO); + when(stateCommonConfig.savedStateDirectory()).thenReturn(Path.of("consistency-test")); + when(consistencyTestingToolConfig.logfileDirectory()).thenReturn("consistency-test"); + + state.init(platform, initTrigger, softwareVersion); + + random = new Random(); + platformStateModifier = mock(PlatformStateModifier.class); + round = mock(Round.class); + event = mock(ConsensusEvent.class); + + consumedTransactions = new ArrayList<>(); + consumer = systemTransaction -> consumedTransactions.add(systemTransaction); + consensusTransaction = mock(TransactionWrapper.class); + + final byte[] signature = new byte[384]; + random.nextBytes(signature); + final byte[] hash = new byte[48]; + random.nextBytes(hash); + stateSignatureTransaction = StateSignatureTransaction.newBuilder() + .signature(Bytes.wrap(signature)) + .hash(Bytes.wrap(hash)) + .round(round.getRoundNum()) + .build(); + } + + @Test + void handleConsensusRoundWithApplicationTransaction() { + final var bytes = Bytes.wrap(new byte[] {1, 1, 1, 1, 1, 1, 1, 1}); + when(consensusTransaction.getApplicationTransaction()).thenReturn(bytes); + + doAnswer(invocation -> { + BiConsumer consumer = invocation.getArgument(0); + consumer.accept(event, consensusTransaction); + return null; + }) + .when(round) + .forEachEventTransaction(any()); + + state.handleConsensusRound(round, platformStateModifier, consumer); + + assertThat(consumedTransactions).isEmpty(); + } + + @Test + void handleConsensusRoundWithSystemTransaction() { + final var stateSignatureTransactionBytes = + StateSignatureTransaction.PROTOBUF.toBytes(stateSignatureTransaction); + when(consensusTransaction.getApplicationTransaction()).thenReturn(stateSignatureTransactionBytes); + + doAnswer(invocation -> { + BiConsumer consumer = invocation.getArgument(0); + consumer.accept(event, consensusTransaction); + return null; + }) + .when(round) + .forEachEventTransaction(any()); + + state.handleConsensusRound(round, platformStateModifier, consumer); + + assertThat(consumedTransactions).hasSize(1); + } + + @Test + void handleConsensusRoundWithMultipleSystemTransactions() { + // Given + final var secondConsensusTransaction = mock(TransactionWrapper.class); + final var thirdConsensusTransaction = mock(TransactionWrapper.class); + final var stateSignatureTransactionBytes = + StateSignatureTransaction.PROTOBUF.toBytes(stateSignatureTransaction); + when(consensusTransaction.getApplicationTransaction()).thenReturn(stateSignatureTransactionBytes); + when(secondConsensusTransaction.getApplicationTransaction()).thenReturn(stateSignatureTransactionBytes); + when(thirdConsensusTransaction.getApplicationTransaction()).thenReturn(stateSignatureTransactionBytes); + + doAnswer(invocation -> { + BiConsumer consumer = invocation.getArgument(0); + consumer.accept(event, consensusTransaction); + consumer.accept(event, secondConsensusTransaction); + consumer.accept(event, thirdConsensusTransaction); + return null; + }) + .when(round) + .forEachEventTransaction(any()); + + // When + state.handleConsensusRound(round, platformStateModifier, consumer); + + assertThat(consumedTransactions).hasSize(3); + } + + @Test + void handleConsensusRoundWithDeprecatedSystemTransaction() { + when(consensusTransaction.getApplicationTransaction()).thenReturn(Bytes.EMPTY); + when(consensusTransaction.isSystem()).thenReturn(true); + + doAnswer(invocation -> { + BiConsumer consumer = invocation.getArgument(0); + consumer.accept(event, consensusTransaction); + return null; + }) + .when(round) + .forEachEventTransaction(any()); + + state.handleConsensusRound(round, platformStateModifier, consumer); + + assertThat(consumedTransactions).isEmpty(); + } + + @Test + void preHandleEventWithMultipleSystemTransactions() { + final var secondConsensusTransaction = mock(TransactionWrapper.class); + final var thirdConsensusTransaction = mock(TransactionWrapper.class); + final var stateSignatureTransactionBytes = + StateSignatureTransaction.PROTOBUF.toBytes(stateSignatureTransaction); + when(consensusTransaction.getApplicationTransaction()).thenReturn(stateSignatureTransactionBytes); + when(secondConsensusTransaction.getApplicationTransaction()).thenReturn(stateSignatureTransactionBytes); + when(thirdConsensusTransaction.getApplicationTransaction()).thenReturn(stateSignatureTransactionBytes); + + doAnswer(invocation -> { + Consumer consumer = invocation.getArgument(0); + consumer.accept(consensusTransaction); + consumer.accept(secondConsensusTransaction); + consumer.accept(thirdConsensusTransaction); + return null; + }) + .when(event) + .forEachTransaction(any()); + + state.preHandle(event, consumer); + + assertThat(consumedTransactions).hasSize(3); + } + + @Test + void preHandleEventWithSystemTransaction() { + final var emptyStateSignatureBytes = StateSignatureTransaction.PROTOBUF.toBytes(stateSignatureTransaction); + when(consensusTransaction.getApplicationTransaction()).thenReturn(emptyStateSignatureBytes); + + doAnswer(invocation -> { + Consumer consumer = invocation.getArgument(0); + consumer.accept(consensusTransaction); + return null; + }) + .when(event) + .forEachTransaction(any()); + + state.preHandle(event, consumer); + + assertThat(consumedTransactions).hasSize(1); + } + + @Test + void preHandleEventWithApplicationTransaction() { + final var bytes = Bytes.wrap(new byte[] {1, 1, 1, 1, 1, 1, 1, 1}); + when(consensusTransaction.getApplicationTransaction()).thenReturn(bytes); + + doAnswer(invocation -> { + Consumer consumer = invocation.getArgument(0); + consumer.accept(consensusTransaction); + return null; + }) + .when(event) + .forEachTransaction(any()); + + state.preHandle(event, consumer); + + assertThat(consumedTransactions).isEmpty(); + } + + @Test + void preHandleEventWithDeprecatedSystemTransaction() { + when(consensusTransaction.isSystem()).thenReturn(true); + + doAnswer(invocation -> { + Consumer consumer = invocation.getArgument(0); + consumer.accept(consensusTransaction); + return null; + }) + .when(event) + .forEachTransaction(any()); + + state.preHandle(event, consumer); + + assertThat(consumedTransactions).isEmpty(); + } +} diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/SwirldState.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/SwirldState.java index c49f3047d4d1..d2d09585d282 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/SwirldState.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/SwirldState.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016-2024 Hedera Hashgraph, LLC + * Copyright (C) 2016-2025 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. @@ -71,7 +71,7 @@ default void init( * This method is always invoked on an immutable state. * * @param event the event to perform pre-handling on - * @param stateSignatureTransaction a consumer that accepts a list of {@link ScopedSystemTransaction}s that + * @param stateSignatureTransaction a consumer that accepts a {@link ScopedSystemTransaction} that * will be used for callbacks */ default void preHandle( @@ -83,7 +83,7 @@ default void preHandle( * * @param round the round to apply * @param platformState the platform state - * @param stateSignatureTransaction a consumer that accepts a list of {@link ScopedSystemTransaction}s that + * @param stateSignatureTransaction a consumer that accepts a {@link ScopedSystemTransaction} that * will be used for callbacks */ void handleConsensusRound(