diff --git a/platform-sdk/platform-apps/tests/MigrationTestingTool/build.gradle.kts b/platform-sdk/platform-apps/tests/MigrationTestingTool/build.gradle.kts index e3fca20be0ff..809f513f0913 100644 --- a/platform-sdk/platform-apps/tests/MigrationTestingTool/build.gradle.kts +++ b/platform-sdk/platform-apps/tests/MigrationTestingTool/build.gradle.kts @@ -1,9 +1,26 @@ -// 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.migration.MigrationTestingToolMain" testModuleInfo { requires("org.junit.jupiter.api") + requires("org.assertj.core") requires("org.junit.jupiter.params") + requires("org.mockito") } diff --git a/platform-sdk/platform-apps/tests/MigrationTestingTool/src/main/java/com/swirlds/demo/migration/MigrationTestingToolMain.java b/platform-sdk/platform-apps/tests/MigrationTestingTool/src/main/java/com/swirlds/demo/migration/MigrationTestingToolMain.java index b095fd60545b..cea14826eea1 100644 --- a/platform-sdk/platform-apps/tests/MigrationTestingTool/src/main/java/com/swirlds/demo/migration/MigrationTestingToolMain.java +++ b/platform-sdk/platform-apps/tests/MigrationTestingTool/src/main/java/com/swirlds/demo/migration/MigrationTestingToolMain.java @@ -22,6 +22,8 @@ import static com.swirlds.platform.test.fixtures.state.FakeStateLifecycles.registerMerkleStateRootClassIds; import com.hedera.hapi.node.state.roster.RosterEntry; +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; @@ -207,4 +209,10 @@ public PlatformMerkleStateRoot newMerkleStateRoot() { public BasicSoftwareVersion getSoftwareVersion() { return softwareVersion; } + + @Override + @NonNull + public Bytes encodeSystemTransaction(final @NonNull StateSignatureTransaction transaction) { + return StateSignatureTransaction.PROTOBUF.toBytes(transaction); + } } diff --git a/platform-sdk/platform-apps/tests/MigrationTestingTool/src/main/java/com/swirlds/demo/migration/MigrationTestingToolState.java b/platform-sdk/platform-apps/tests/MigrationTestingTool/src/main/java/com/swirlds/demo/migration/MigrationTestingToolState.java index 6d0e2ae39866..db801e25980a 100644 --- a/platform-sdk/platform-apps/tests/MigrationTestingTool/src/main/java/com/swirlds/demo/migration/MigrationTestingToolState.java +++ b/platform-sdk/platform-apps/tests/MigrationTestingTool/src/main/java/com/swirlds/demo/migration/MigrationTestingToolState.java @@ -17,11 +17,13 @@ package com.swirlds.demo.migration; import static com.swirlds.demo.migration.MigrationTestingToolMain.PREVIOUS_SOFTWARE_VERSION; +import static com.swirlds.demo.migration.TransactionUtils.isSystemTransaction; import static com.swirlds.logging.legacy.LogMarker.STARTUP; import static com.swirlds.platform.test.fixtures.state.FakeStateLifecycles.FAKE_MERKLE_STATE_LIFECYCLES; import com.hedera.hapi.node.base.SemanticVersion; import com.hedera.hapi.platform.event.StateSignatureTransaction; +import com.hedera.pbj.runtime.ParseException; import com.swirlds.common.constructable.ConstructableIgnored; import com.swirlds.common.crypto.DigestType; import com.swirlds.common.merkle.MerkleNode; @@ -47,7 +49,9 @@ import com.swirlds.platform.system.SoftwareVersion; import com.swirlds.platform.system.address.AddressBook; import com.swirlds.platform.system.events.ConsensusEvent; +import com.swirlds.platform.system.events.Event; import com.swirlds.platform.system.transaction.ConsensusTransaction; +import com.swirlds.platform.system.transaction.Transaction; import com.swirlds.virtualmap.VirtualMap; import com.swirlds.virtualmap.datasource.VirtualDataSourceBuilder; import edu.umd.cs.findbugs.annotations.NonNull; @@ -278,6 +282,27 @@ public void init( } } + @Override + public void preHandle( + final @NonNull Event event, + final @NonNull Consumer> stateSignatureTransaction) { + event.forEachTransaction(transaction -> { + + // We don't want to consume deprecated EventTransaction.STATE_SIGNATURE_TRANSACTION system transactions in + // the callback, since it's intended to be used only + // for the new form of encoded system transactions in Bytes. + // We skip the current iteration, if it processes a deprecated system transaction with the + // EventTransaction.STATE_SIGNATURE_TRANSACTION type. + if (transaction.isSystem()) { + return; + } + + if (isSystemTransaction(transaction.getApplicationTransaction())) { + consumeSystemTransaction(transaction, event, stateSignatureTransaction); + } + }); + } + /** * {@inheritDoc} */ @@ -295,6 +320,11 @@ public void handleConsensusRound( if (trans.isSystem()) { continue; } + if (isSystemTransaction(trans.getApplicationTransaction())) { + consumeSystemTransaction(trans, event, stateSignatureTransaction); + continue; + } + final MigrationTestingToolTransaction mTrans = TransactionUtils.parseTransaction(trans.getApplicationTransaction()); mTrans.applyTo(this); @@ -335,4 +365,19 @@ public int getVersion() { public int getMinimumSupportedVersion() { return ClassVersion.VIRTUAL_MAP; } + + 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/MigrationTestingTool/src/main/java/com/swirlds/demo/migration/TransactionGenerator.java b/platform-sdk/platform-apps/tests/MigrationTestingTool/src/main/java/com/swirlds/demo/migration/TransactionGenerator.java index 9d61e9549126..f2194560614b 100644 --- a/platform-sdk/platform-apps/tests/MigrationTestingTool/src/main/java/com/swirlds/demo/migration/TransactionGenerator.java +++ b/platform-sdk/platform-apps/tests/MigrationTestingTool/src/main/java/com/swirlds/demo/migration/TransactionGenerator.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022-2024 Hedera Hashgraph, LLC + * Copyright (C) 2022-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. @@ -59,6 +59,8 @@ public byte[] generateTransaction() throws SignatureException { final ByteArrayOutputStream byteOut = new ByteArrayOutputStream(); final SerializableDataOutputStream out = new SerializableDataOutputStream(byteOut); try { + // Adding additional byte to differentiate application transactions from system ones + out.write(1); out.writeSerializable(transaction, false); } catch (final IOException e) { throw new UncheckedIOException(e); diff --git a/platform-sdk/platform-apps/tests/MigrationTestingTool/src/main/java/com/swirlds/demo/migration/TransactionUtils.java b/platform-sdk/platform-apps/tests/MigrationTestingTool/src/main/java/com/swirlds/demo/migration/TransactionUtils.java index 850351c9ba14..31185e89977c 100644 --- a/platform-sdk/platform-apps/tests/MigrationTestingTool/src/main/java/com/swirlds/demo/migration/TransactionUtils.java +++ b/platform-sdk/platform-apps/tests/MigrationTestingTool/src/main/java/com/swirlds/demo/migration/TransactionUtils.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2024 Hedera Hashgraph, LLC + * Copyright (C) 2024-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. @@ -26,11 +26,16 @@ * Utility methods for migration testing tool transactions */ public class TransactionUtils { + private static final byte APPLICATION_TRANSACTION_MARKER = 1; + /** * Parse a {@link MigrationTestingToolTransaction} from a {@link Bytes}. */ public static @NonNull MigrationTestingToolTransaction parseTransaction(@NonNull final Bytes bytes) { - final SerializableDataInputStream in = new SerializableDataInputStream(bytes.toInputStream()); + // Remove the first byte, which is marker added to distinguish application transactions from system ones in + // TransactionGenerator + final Bytes slicedBytes = bytes.slice(1, bytes.length() - 1); + final SerializableDataInputStream in = new SerializableDataInputStream(slicedBytes.toInputStream()); try { return in.readSerializable(false, MigrationTestingToolTransaction::new); @@ -38,4 +43,8 @@ public class TransactionUtils { throw new UncheckedIOException("Could not parse transaction kind:%s".formatted(bytes.toHex()), e); } } + + public static boolean isSystemTransaction(@NonNull final Bytes bytes) { + return bytes.getByte(0) != APPLICATION_TRANSACTION_MARKER; + } } diff --git a/platform-sdk/platform-apps/tests/MigrationTestingTool/src/test/java/com/swirlds/demo/migration/MigrationTestingToolStateTest.java b/platform-sdk/platform-apps/tests/MigrationTestingTool/src/test/java/com/swirlds/demo/migration/MigrationTestingToolStateTest.java new file mode 100644 index 000000000000..eff503460703 --- /dev/null +++ b/platform-sdk/platform-apps/tests/MigrationTestingTool/src/test/java/com/swirlds/demo/migration/MigrationTestingToolStateTest.java @@ -0,0 +1,229 @@ +/* + * 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.migration; + +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.hedera.hapi.node.base.Timestamp; +import com.hedera.hapi.platform.event.EventCore; +import com.hedera.hapi.platform.event.EventTransaction; +import com.hedera.hapi.platform.event.EventTransaction.TransactionOneOfType; +import com.hedera.hapi.platform.event.GossipEvent; +import com.hedera.hapi.platform.event.StateSignatureTransaction; +import com.hedera.pbj.runtime.OneOf; +import com.hedera.pbj.runtime.io.buffer.Bytes; +import com.swirlds.platform.components.transaction.system.ScopedSystemTransaction; +import com.swirlds.platform.event.PlatformEvent; +import com.swirlds.platform.state.PlatformStateModifier; +import com.swirlds.platform.state.StateLifecycles; +import com.swirlds.platform.system.Round; +import com.swirlds.platform.system.events.ConsensusEvent; +import com.swirlds.platform.system.transaction.ConsensusTransaction; +import com.swirlds.platform.system.transaction.Transaction; +import com.swirlds.platform.system.transaction.TransactionWrapper; +import java.security.SignatureException; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Random; +import java.util.function.Consumer; +import java.util.function.Function; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +class MigrationTestingToolStateTest { + private MigrationTestingToolState state; + private Random random; + private PlatformStateModifier platformStateModifier; + private Round round; + private ConsensusEvent event; + private List> consumedTransactions; + private Consumer> consumer; + private Transaction transaction; + private StateSignatureTransaction stateSignatureTransaction; + + @BeforeEach + void setUp() { + state = new MigrationTestingToolState(mock(StateLifecycles.class), mock(Function.class)); + random = new Random(); + round = mock(Round.class); + event = mock(ConsensusEvent.class); + + consumedTransactions = new ArrayList<>(); + consumer = systemTransaction -> consumedTransactions.add(systemTransaction); + transaction = 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() throws SignatureException { + givenRoundAndEvent(); + final TransactionGenerator generator = new TransactionGenerator(5); + final var bytes = Bytes.wrap(generator.generateTransaction()); + final var tr = TransactionUtils.parseTransaction(bytes); + when(transaction.getApplicationTransaction()).thenReturn(bytes); + + try (MockedStatic utilities = + Mockito.mockStatic(TransactionUtils.class, Mockito.CALLS_REAL_METHODS)) { + MigrationTestingToolTransaction migrationTestingToolTransaction = Mockito.spy(tr); + utilities.when(() -> TransactionUtils.parseTransaction(any())).thenReturn(migrationTestingToolTransaction); + Mockito.doNothing().when(migrationTestingToolTransaction).applyTo(state); + state.handleConsensusRound(round, platformStateModifier, consumer); + } + + assertThat(consumedTransactions).isEmpty(); + } + + @Test + void handleConsensusRoundWithSystemTransaction() { + givenRoundAndEvent(); + final var stateSignatureTransactionBytes = + StateSignatureTransaction.PROTOBUF.toBytes(stateSignatureTransaction); + when(transaction.getApplicationTransaction()).thenReturn(stateSignatureTransactionBytes); + + state.handleConsensusRound(round, platformStateModifier, consumer); + + assertThat(consumedTransactions).hasSize(1); + } + + @Test + void handleConsensusRoundWithMultipleSystemTransactions() { + final var secondConsensusTransaction = mock(TransactionWrapper.class); + final var thirdConsensusTransaction = mock(TransactionWrapper.class); + when(round.iterator()).thenReturn(Collections.singletonList(event).iterator()); + when(event.getConsensusTimestamp()).thenReturn(Instant.now()); + when(event.consensusTransactionIterator()) + .thenReturn(List.of( + (ConsensusTransaction) transaction, + secondConsensusTransaction, + thirdConsensusTransaction) + .iterator()); + + final var stateSignatureTransactionBytes = + StateSignatureTransaction.PROTOBUF.toBytes(stateSignatureTransaction); + when(transaction.getApplicationTransaction()).thenReturn(stateSignatureTransactionBytes); + when(secondConsensusTransaction.getApplicationTransaction()).thenReturn(stateSignatureTransactionBytes); + when(thirdConsensusTransaction.getApplicationTransaction()).thenReturn(stateSignatureTransactionBytes); + + state.handleConsensusRound(round, platformStateModifier, consumer); + + assertThat(consumedTransactions).hasSize(3); + } + + @Test + void handleConsensusRoundWithDeprecatedSystemTransaction() { + givenRoundAndEvent(); + when(transaction.getApplicationTransaction()).thenReturn(Bytes.EMPTY); + when(transaction.isSystem()).thenReturn(true); + + state.handleConsensusRound(round, platformStateModifier, consumer); + + assertThat(consumedTransactions).isEmpty(); + } + + @Test + void preHandleEventWithMultipleSystemTransactions() { + final var gossipEvent = mock(GossipEvent.class); + final var eventCore = mock(EventCore.class); + when(gossipEvent.eventCore()).thenReturn(eventCore); + when(eventCore.timeCreated()).thenReturn(Timestamp.DEFAULT); + final var eventTransaction = mock(EventTransaction.class); + final var secondEventTransaction = mock(EventTransaction.class); + final var thirdEventTransaction = mock(EventTransaction.class); + + final var stateSignatureTransactionBytes = + StateSignatureTransaction.PROTOBUF.toBytes(stateSignatureTransaction); + final var transactionProto = com.hedera.hapi.node.base.Transaction.newBuilder() + .bodyBytes(stateSignatureTransactionBytes) + .build(); + final var transactionBytes = com.hedera.hapi.node.base.Transaction.PROTOBUF.toBytes(transactionProto); + + final var systemTransactionWithType = + new OneOf<>(TransactionOneOfType.APPLICATION_TRANSACTION, transactionBytes); + + when(eventTransaction.transaction()).thenReturn(systemTransactionWithType); + when(secondEventTransaction.transaction()).thenReturn(systemTransactionWithType); + when(thirdEventTransaction.transaction()).thenReturn(systemTransactionWithType); + when(gossipEvent.eventTransaction()) + .thenReturn(List.of(eventTransaction, secondEventTransaction, thirdEventTransaction)); + event = new PlatformEvent(gossipEvent); + + state.preHandle(event, consumer); + + assertThat(consumedTransactions).hasSize(3); + } + + @Test + void preHandleEventWithSystemTransaction() { + final var gossipEvent = mock(GossipEvent.class); + final var eventCore = mock(EventCore.class); + when(eventCore.timeCreated()).thenReturn(Timestamp.DEFAULT); + final var eventTransaction = mock(EventTransaction.class); + when(gossipEvent.eventCore()).thenReturn(eventCore); + when(gossipEvent.eventTransaction()).thenReturn(List.of(eventTransaction)); + + final var stateSignatureTransactionBytes = + StateSignatureTransaction.PROTOBUF.toBytes(stateSignatureTransaction); + final var transactionProto = com.hedera.hapi.node.base.Transaction.newBuilder() + .bodyBytes(stateSignatureTransactionBytes) + .build(); + final var transactionBytes = com.hedera.hapi.node.base.Transaction.PROTOBUF.toBytes(transactionProto); + final var systemTransactionWithType = + new OneOf<>(TransactionOneOfType.APPLICATION_TRANSACTION, transactionBytes); + when(eventTransaction.transaction()).thenReturn(systemTransactionWithType); + event = new PlatformEvent(gossipEvent); + + state.preHandle(event, consumer); + + assertThat(consumedTransactions).hasSize(1); + } + + @Test + void preHandleEventWithDeprecatedSystemTransaction() { + event = mock(PlatformEvent.class); + + when(round.iterator()).thenReturn(Collections.singletonList(event).iterator()); + when(transaction.isSystem()).thenReturn(true); + + state.preHandle(event, consumer); + + assertThat(consumedTransactions).isEmpty(); + } + + private void givenRoundAndEvent() { + when(round.iterator()).thenReturn(Collections.singletonList(event).iterator()); + when(event.getConsensusTimestamp()).thenReturn(Instant.now()); + when(event.consensusTransactionIterator()) + .thenReturn(Collections.singletonList((ConsensusTransaction) transaction) + .iterator()); + } +}