diff --git a/platform-sdk/platform-apps/tests/ISSTestingTool/build.gradle.kts b/platform-sdk/platform-apps/tests/ISSTestingTool/build.gradle.kts index 25eac23b95bf..0abcf6dbe42b 100644 --- a/platform-sdk/platform-apps/tests/ISSTestingTool/build.gradle.kts +++ b/platform-sdk/platform-apps/tests/ISSTestingTool/build.gradle.kts @@ -1,6 +1,27 @@ -// SPDX-License-Identifier: Apache-2.0 +/* + * 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. + * 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.iss.ISSTestingToolMain" mainModuleInfo { annotationProcessor("com.swirlds.config.processor") } + +testModuleInfo { + requires("org.assertj.core") + requires("org.junit.jupiter.api") + requires("org.mockito") +} diff --git a/platform-sdk/platform-apps/tests/ISSTestingTool/src/main/java/com/swirlds/demo/iss/ISSTestingToolMain.java b/platform-sdk/platform-apps/tests/ISSTestingTool/src/main/java/com/swirlds/demo/iss/ISSTestingToolMain.java index 05e442031f57..8caa91b9e9d4 100644 --- a/platform-sdk/platform-apps/tests/ISSTestingTool/src/main/java/com/swirlds/demo/iss/ISSTestingToolMain.java +++ b/platform-sdk/platform-apps/tests/ISSTestingTool/src/main/java/com/swirlds/demo/iss/ISSTestingToolMain.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; @@ -134,4 +136,9 @@ public BasicSoftwareVersion getSoftwareVersion() { public List> getConfigDataTypes() { return List.of(ISSTestingToolConfig.class); } + + @Override + public Bytes encodeSystemTransaction(@NonNull final StateSignatureTransaction transaction) { + return StateSignatureTransaction.PROTOBUF.toBytes(transaction); + } } diff --git a/platform-sdk/platform-apps/tests/ISSTestingTool/src/main/java/com/swirlds/demo/iss/ISSTestingToolState.java b/platform-sdk/platform-apps/tests/ISSTestingTool/src/main/java/com/swirlds/demo/iss/ISSTestingToolState.java index 28dbd6897e1d..9ecda4703be1 100644 --- a/platform-sdk/platform-apps/tests/ISSTestingTool/src/main/java/com/swirlds/demo/iss/ISSTestingToolState.java +++ b/platform-sdk/platform-apps/tests/ISSTestingTool/src/main/java/com/swirlds/demo/iss/ISSTestingToolState.java @@ -36,6 +36,7 @@ import com.hedera.hapi.node.state.roster.Roster; import com.hedera.hapi.node.state.roster.RosterEntry; import com.hedera.hapi.platform.event.StateSignatureTransaction; +import com.hedera.pbj.runtime.ParseException; import com.swirlds.common.constructable.ConstructableIgnored; import com.swirlds.common.io.SelfSerializable; import com.swirlds.common.io.streams.SerializableDataInputStream; @@ -53,7 +54,9 @@ import com.swirlds.platform.system.Round; import com.swirlds.platform.system.SoftwareVersion; 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.platform.test.fixtures.state.FakeStateLifecycles; import com.swirlds.state.merkle.singleton.StringLeaf; import edu.umd.cs.findbugs.annotations.NonNull; @@ -193,11 +196,11 @@ public void init( writeObjectByChildIndex(PLANNED_ISS_LIST_INDEX, plannedIssList); writeObjectByChildIndex(PLANNED_LOG_ERROR_LIST_INDEX, plannedLogErrorList); } else { - StringLeaf runningSumLeaf = getChild(RUNNING_SUM_INDEX); + final StringLeaf runningSumLeaf = getChild(RUNNING_SUM_INDEX); if (runningSumLeaf != null) { runningSum = Long.parseLong(runningSumLeaf.getLabel()); } - StringLeaf genesisTimestampLeaf = getChild(GENESIS_TIMESTAMP_INDEX); + final StringLeaf genesisTimestampLeaf = getChild(GENESIS_TIMESTAMP_INDEX); if (genesisTimestampLeaf != null) { genesisTimestamp = Instant.parse(genesisTimestampLeaf.getLabel()); } @@ -210,14 +213,14 @@ public void init( Scratchpad.create(platform.getContext(), selfId, IssTestingToolScratchpad.class, "ISSTestingTool"); } - List readObjectByChildIndex(int index, Supplier factory) { - StringLeaf stringValue = getChild(index); + List readObjectByChildIndex(final int index, final Supplier factory) { + final StringLeaf stringValue = getChild(index); if (stringValue != null) { try { - SerializableDataInputStream in = new SerializableDataInputStream( + final SerializableDataInputStream in = new SerializableDataInputStream( new ByteArrayInputStream(stringValue.getLabel().getBytes(StandardCharsets.UTF_8))); return in.readSerializableList(1024, false, factory); - } catch (IOException e) { + } catch (final IOException e) { throw new RuntimeException(e); } } else { @@ -225,17 +228,42 @@ List readObjectByChildIndex(int index, Supplier< } } - void writeObjectByChildIndex(int index, List list) { + void writeObjectByChildIndex(final int index, final List list) { try { - ByteArrayOutputStream byteOut = new ByteArrayOutputStream(); - SerializableDataOutputStream out = new SerializableDataOutputStream(byteOut); + final ByteArrayOutputStream byteOut = new ByteArrayOutputStream(); + final SerializableDataOutputStream out = new SerializableDataOutputStream(byteOut); out.writeSerializableList(list, false, true); setChild(index, new StringLeaf(byteOut.toString(StandardCharsets.UTF_8))); - } catch (IOException e) { + } catch (final IOException e) { throw new RuntimeException(e); } } + @Override + public void preHandle( + @NonNull final Event event, + @NonNull + final Consumer> + stateSignatureTransactionCallback) { + event.forEachTransaction(transaction -> { + // We are not interested in pre-handling any system transactions, as they are + // specific for the platform only.We also 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.Thus, we can directly skip the current + // iteration, if it processes a deprecated system transaction with the + // EventTransaction.STATE_SIGNATURE_TRANSACTION type. + if (transaction.isSystem()) { + return; + } + + // We should consume in the callback the new form of system transactions in Bytes + if (areTransactionBytesSystemOnes(transaction)) { + consumeSystemTransaction(transaction, event, stateSignatureTransactionCallback); + } + }); + } + /** * {@inheritDoc} */ @@ -243,14 +271,34 @@ void writeObjectByChildIndex(int index, List lis public void handleConsensusRound( @NonNull final Round round, @NonNull final PlatformStateModifier platformState, - @NonNull final Consumer> stateSignatureTransaction) { + @NonNull + final Consumer> + stateSignatureTransactionCallback) { throwIfImmutable(); final Iterator eventIterator = round.iterator(); while (eventIterator.hasNext()) { - final ConsensusEvent event = eventIterator.next(); + final var event = eventIterator.next(); captureTimestamp(event); - event.consensusTransactionIterator().forEachRemaining(this::handleTransaction); + event.consensusTransactionIterator().forEachRemaining(transaction -> { + // We are not interested in handling any system transactions, as they are specific + // for the platform only.We also 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.Thus, we can directly skip the current + // iteration, if it processes a deprecated system transaction with the + // EventTransaction.STATE_SIGNATURE_TRANSACTION type. + if (transaction.isSystem()) { + return; + } + + // We should consume in the callback the new form of system transactions in Bytes + if (areTransactionBytesSystemOnes(transaction)) { + consumeSystemTransaction(transaction, event, stateSignatureTransactionCallback); + } else { + handleTransaction(transaction); + } + }); if (!eventIterator.hasNext()) { final Instant currentTimestamp = event.getConsensusTimestamp(); final Duration elapsedSinceGenesis = Duration.between(genesisTimestamp, currentTimestamp); @@ -291,15 +339,37 @@ private void captureTimestamp(final ConsensusEvent event) { * @param transaction the transaction to apply */ private void handleTransaction(final ConsensusTransaction transaction) { - if (transaction.isSystem()) { - return; - } final int delta = ByteUtils.byteArrayToInt(transaction.getApplicationTransaction().toByteArray(), 0); runningSum += delta; setChild(RUNNING_SUM_INDEX, new StringLeaf(Long.toString(runningSum))); } + /** + * Checks if the transaction bytes are system ones. The test creates application transactions + * with max length of 4. System transactions will be always bigger than that. + * + * @param transaction the consensus transaction to check + * @return true if the transaction bytes are system ones, false otherwise + */ + private boolean areTransactionBytesSystemOnes(final Transaction transaction) { + return transaction.getApplicationTransaction().length() > 4; + } + + private void consumeSystemTransaction( + final Transaction transaction, + final Event event, + final 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); + } + } + /** * Iterate over a list of planned incidents, and return the first one that should be triggered. If no incident from * the list should be triggered, return null diff --git a/platform-sdk/platform-apps/tests/ISSTestingTool/src/test/java/com/swirlds/demo/iss/ISSTestingToolStateTest.java b/platform-sdk/platform-apps/tests/ISSTestingTool/src/test/java/com/swirlds/demo/iss/ISSTestingToolStateTest.java new file mode 100644 index 000000000000..0bdecce5accd --- /dev/null +++ b/platform-sdk/platform-apps/tests/ISSTestingTool/src/test/java/com/swirlds/demo/iss/ISSTestingToolStateTest.java @@ -0,0 +1,366 @@ +/* + * 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. + * 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.iss; + +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +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 com.swirlds.state.merkle.singleton.StringLeaf; +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; + +class ISSTestingToolStateTest { + + private static final int RUNNING_SUM_INDEX = 3; + private ISSTestingToolMain main; + private ISSTestingToolState state; + 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 ISSTestingToolState(mock(StateLifecycles.class), mock(Function.class)); + main = mock(ISSTestingToolMain.class); + final var random = new Random(); + platformStateModifier = mock(PlatformStateModifier.class); + 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() { + // Given + givenRoundAndEvent(); + + final var bytes = Bytes.wrap(new byte[] {1, 1, 1, 1}); + when(transaction.getApplicationTransaction()).thenReturn(bytes); + + // When + state.handleConsensusRound(round, platformStateModifier, consumer); + + // Then + verify(round, times(1)).iterator(); + verify(event, times(2)).getConsensusTimestamp(); + verify(event, times(1)).consensusTransactionIterator(); + + assertThat(Long.parseLong(((StringLeaf) state.getChild(RUNNING_SUM_INDEX)).getLabel())) + .isPositive(); + assertThat(consumedTransactions).isEmpty(); + } + + @Test + void handleConsensusRoundWithSystemTransaction() { + // Given + givenRoundAndEvent(); + + final var stateSignatureTransactionBytes = + StateSignatureTransaction.PROTOBUF.toBytes(stateSignatureTransaction); + when(main.encodeSystemTransaction(stateSignatureTransaction)).thenReturn(stateSignatureTransactionBytes); + when(transaction.getApplicationTransaction()).thenReturn(stateSignatureTransactionBytes); + + // When + state.handleConsensusRound(round, platformStateModifier, consumer); + + // Then + verify(round, times(1)).iterator(); + verify(event, times(2)).getConsensusTimestamp(); + verify(event, times(1)).consensusTransactionIterator(); + + assertThat((StringLeaf) state.getChild(RUNNING_SUM_INDEX)).isNull(); + assertThat(consumedTransactions).hasSize(1); + } + + @Test + void handleConsensusRoundWithMultipleSystemTransaction() { + // Given + 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(main.encodeSystemTransaction(stateSignatureTransaction)).thenReturn(stateSignatureTransactionBytes); + when(transaction.getApplicationTransaction()).thenReturn(stateSignatureTransactionBytes); + when(secondConsensusTransaction.getApplicationTransaction()).thenReturn(stateSignatureTransactionBytes); + when(thirdConsensusTransaction.getApplicationTransaction()).thenReturn(stateSignatureTransactionBytes); + + // When + state.handleConsensusRound(round, platformStateModifier, consumer); + + // Then + verify(round, times(1)).iterator(); + verify(event, times(2)).getConsensusTimestamp(); + verify(event, times(1)).consensusTransactionIterator(); + + assertThat((StringLeaf) state.getChild(RUNNING_SUM_INDEX)).isNull(); + assertThat(consumedTransactions).hasSize(3); + } + + @Test + void handleConsensusRoundWithDeprecatedSystemTransaction() { + // Given + givenRoundAndEvent(); + + when(transaction.getApplicationTransaction()).thenReturn(Bytes.EMPTY); + when(transaction.isSystem()).thenReturn(true); + + // When + state.handleConsensusRound(round, platformStateModifier, consumer); + + // Then + verify(round, times(1)).iterator(); + verify(event, times(2)).getConsensusTimestamp(); + verify(event, times(1)).consensusTransactionIterator(); + + assertThat((StringLeaf) state.getChild(RUNNING_SUM_INDEX)).isNull(); + assertThat(consumedTransactions).isEmpty(); + } + + @Test + void handleConsensusRoundWithEmptyTransaction() { + // Given + givenRoundAndEvent(); + + final var emptyStateSignatureTransaction = StateSignatureTransaction.DEFAULT; + final var emptyStateSignatureTransactionBytes = + StateSignatureTransaction.PROTOBUF.toBytes(emptyStateSignatureTransaction); + when(main.encodeSystemTransaction(emptyStateSignatureTransaction)) + .thenReturn(emptyStateSignatureTransactionBytes); + when(transaction.getApplicationTransaction()).thenReturn(emptyStateSignatureTransactionBytes); + + // When + state.handleConsensusRound(round, platformStateModifier, consumer); + + // Then + verify(round, times(1)).iterator(); + verify(event, times(2)).getConsensusTimestamp(); + verify(event, times(1)).consensusTransactionIterator(); + + assertThat(Long.parseLong(((StringLeaf) state.getChild(RUNNING_SUM_INDEX)).getLabel())) + .isZero(); + assertThat(consumedTransactions).isEmpty(); + } + + @Test + void handleConsensusRoundWithNullTransaction() { + // Given + givenRoundAndEvent(); + + final var emptyStateSignatureTransaction = StateSignatureTransaction.DEFAULT; + final var emptyStateSignatureTransactionBytes = + StateSignatureTransaction.PROTOBUF.toBytes(emptyStateSignatureTransaction); + when(main.encodeSystemTransaction(null)) + .thenReturn(StateSignatureTransaction.PROTOBUF.toBytes(emptyStateSignatureTransaction)); + when(transaction.getApplicationTransaction()).thenReturn(emptyStateSignatureTransactionBytes); + + // When + state.handleConsensusRound(round, platformStateModifier, consumer); + + // Then + verify(round, times(1)).iterator(); + verify(event, times(2)).getConsensusTimestamp(); + verify(event, times(1)).consensusTransactionIterator(); + + assertThat(Long.parseLong(((StringLeaf) state.getChild(RUNNING_SUM_INDEX)).getLabel())) + .isZero(); + assertThat(consumedTransactions).isEmpty(); + } + + @Test + void preHandleEventWithMultipleSystemTransaction() { + // Given + final var gossipEvent = mock(GossipEvent.class); + final var eventCore = mock(EventCore.class); + when(gossipEvent.eventCore()).thenReturn(eventCore); + when(eventCore.timeCreated()).thenReturn(Timestamp.DEFAULT); + when(eventCore.creatorNodeId()).thenReturn(1L); + when(eventCore.parents()).thenReturn(Collections.emptyList()); + 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); + when(round.iterator()).thenReturn(Collections.singletonList(event).iterator()); + when(transaction.getApplicationTransaction()).thenReturn(transactionBytes); + when(secondEventTransaction.applicationTransaction()).thenReturn(transactionBytes); + when(thirdEventTransaction.applicationTransaction()).thenReturn(transactionBytes); + + // When + state.preHandle(event, consumer); + + // Then + assertThat(consumedTransactions).hasSize(3); + } + + @Test + void preHandleEventWithSystemTransaction() { + // Given + final var gossipEvent = mock(GossipEvent.class); + final var eventCore = mock(EventCore.class); + when(eventCore.timeCreated()).thenReturn(Timestamp.DEFAULT); + when(eventCore.creatorNodeId()).thenReturn(1L); + when(eventCore.parents()).thenReturn(Collections.emptyList()); + 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); + + when(round.iterator()).thenReturn(Collections.singletonList(event).iterator()); + when(transaction.getApplicationTransaction()).thenReturn(transactionBytes); + + // When + state.preHandle(event, consumer); + + // Then + assertThat(consumedTransactions).hasSize(1); + } + + @Test + void preHandleEventWithDeprecatedSystemTransaction() { + // Given + event = mock(PlatformEvent.class); + + when(round.iterator()).thenReturn(Collections.singletonList(event).iterator()); + when(transaction.isSystem()).thenReturn(true); + + // When + state.preHandle(event, consumer); + + // Then + assertThat(consumedTransactions).isEmpty(); + } + + @Test + void preHandleEventWithEmptyTransaction() { + // Given + final var gossipEvent = mock(GossipEvent.class); + final var eventCore = mock(EventCore.class); + when(eventCore.timeCreated()).thenReturn(Timestamp.DEFAULT); + when(eventCore.creatorNodeId()).thenReturn(1L); + when(eventCore.parents()).thenReturn(Collections.emptyList()); + 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.DEFAULT); + 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); + when(round.iterator()).thenReturn(Collections.singletonList(event).iterator()); + when(transaction.getApplicationTransaction()).thenReturn(transactionBytes); + + // When + state.preHandle(event, consumer); + + // Then + 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()); + } +} diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/SwirldMain.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/SwirldMain.java index 35a902c9821f..6bd799f9348f 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/SwirldMain.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/SwirldMain.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. @@ -16,6 +16,8 @@ package com.swirlds.platform.system; +import com.hedera.hapi.platform.event.StateSignatureTransaction; +import com.hedera.pbj.runtime.io.buffer.Bytes; import com.swirlds.common.platform.NodeId; import com.swirlds.platform.state.PlatformMerkleStateRoot; import edu.umd.cs.findbugs.annotations.NonNull; @@ -91,4 +93,14 @@ default List> getConfigDataTypes() { */ @NonNull SoftwareVersion getSoftwareVersion(); + + /** + * Encodes a system transaction to {@link Bytes} representation of a {@link com.hedera.hapi.node.base.Transaction}. + * + * @param transaction the {@link StateSignatureTransaction} to encode + * @return {@link Bytes} representation of the transaction + */ + default Bytes encodeSystemTransaction(@NonNull final StateSignatureTransaction transaction) { + throw new IllegalStateException("Invoke the method on the appropriate SwirldMain implementation!"); + } } diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/transaction/Transaction.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/transaction/Transaction.java index 18a912213f7c..a45acc3b15ff 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/transaction/Transaction.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/transaction/Transaction.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016-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. @@ -36,6 +36,7 @@ public sealed interface Transaction permits ConsensusTransaction { * @return the transaction */ @NonNull + @Deprecated EventTransaction getTransaction(); /** @@ -61,6 +62,7 @@ public sealed interface Transaction permits ConsensusTransaction { * @return {@code true} if this is a system transaction; otherwise {@code false} if this is an application * transaction */ + @Deprecated default boolean isSystem() { return TransactionUtils.isSystemTransaction(getTransaction()); } diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/transaction/TransactionWrapper.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/transaction/TransactionWrapper.java index 7d39092a96b8..549888a0169b 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/transaction/TransactionWrapper.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/transaction/TransactionWrapper.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016-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. @@ -30,6 +30,7 @@ * A transaction that may or may not reach consensus. */ public non-sealed class TransactionWrapper implements ConsensusTransaction { + /** * The consensus timestamp of this transaction, or null if consensus has not yet been reached. * NOT serialized and not part of object equality or hash code @@ -65,6 +66,19 @@ public TransactionWrapper(@NonNull final EventTransaction transaction) { this.payload = Objects.requireNonNull(transaction, "transaction should not be null"); } + /** + * Constructs a new transaction wrapper + * + * @param payloadBytes the serialized bytes of the transaction + * + * @throws NullPointerException if payloadBytes is null + */ + public TransactionWrapper(@NonNull final Bytes payloadBytes) { + this.payload = EventTransaction.newBuilder() + .applicationTransaction(payloadBytes) + .build(); + } + /** * {@inheritDoc} */