diff --git a/platform-sdk/platform-apps/tests/StatsSigningTestingTool/build.gradle.kts b/platform-sdk/platform-apps/tests/StatsSigningTestingTool/build.gradle.kts index 2a4f4cf630f..c960b7ec722 100644 --- a/platform-sdk/platform-apps/tests/StatsSigningTestingTool/build.gradle.kts +++ b/platform-sdk/platform-apps/tests/StatsSigningTestingTool/build.gradle.kts @@ -1,4 +1,19 @@ -// 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") } // Remove the following line to enable all 'javac' lint checks that we have turned on by default @@ -6,3 +21,10 @@ plugins { id("org.hiero.gradle.module.application") } tasks.withType().configureEach { options.compilerArgs.add("-Xlint:-cast") } application.mainClass = "com.swirlds.demo.stats.signing.StatsSigningTestingToolMain" + +testModuleInfo { + requires("org.assertj.core") + requires("org.junit.jupiter.api") + requires("org.mockito") + requires("org.junit.jupiter.params") +} diff --git a/platform-sdk/platform-apps/tests/StatsSigningTestingTool/src/main/java/com/swirlds/demo/stats/signing/StatsSigningTestingToolMain.java b/platform-sdk/platform-apps/tests/StatsSigningTestingTool/src/main/java/com/swirlds/demo/stats/signing/StatsSigningTestingToolMain.java index fb8870d32cd..de987153daa 100644 --- a/platform-sdk/platform-apps/tests/StatsSigningTestingTool/src/main/java/com/swirlds/demo/stats/signing/StatsSigningTestingToolMain.java +++ b/platform-sdk/platform-apps/tests/StatsSigningTestingTool/src/main/java/com/swirlds/demo/stats/signing/StatsSigningTestingToolMain.java @@ -34,6 +34,9 @@ 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.hedera.pbj.runtime.io.stream.WritableStreamingData; import com.swirlds.common.constructable.ClassConstructorPair; import com.swirlds.common.constructable.ConstructableRegistry; import com.swirlds.common.constructable.ConstructableRegistryException; @@ -52,6 +55,8 @@ import com.swirlds.platform.system.Platform; import com.swirlds.platform.system.SwirldMain; import edu.umd.cs.findbugs.annotations.NonNull; +import java.io.ByteArrayOutputStream; +import java.io.IOException; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -63,12 +68,13 @@ public class StatsSigningTestingToolMain implements SwirldMain { // the first four come from the parameters in the config.txt file + public static final byte SYSTEM_TRANSACTION_MARKER = 0; private static final Logger logger = LogManager.getLogger(StatsSigningTestingToolMain.class); static { try { logger.info(STARTUP.getMarker(), "Registering StatsSigningTestingToolState with ConstructableRegistry"); - ConstructableRegistry constructableRegistry = ConstructableRegistry.getInstance(); + final ConstructableRegistry constructableRegistry = ConstructableRegistry.getInstance(); constructableRegistry.registerConstructable( new ClassConstructorPair(StatsSigningTestingToolState.class, () -> { StatsSigningTestingToolState statsSigningTestingToolState = @@ -77,7 +83,7 @@ public class StatsSigningTestingToolMain implements SwirldMain -1) { // if not unlimited (-1 means unlimited) // ramp up the TPS to the expected value - long elapsedTime = now / MILLISECONDS_TO_NANOSECONDS - rampUpStartTimeMilliSeconds; + final long elapsedTime = now / MILLISECONDS_TO_NANOSECONDS - rampUpStartTimeMilliSeconds; double rampUpTPS = 0; if (elapsedTime < TPS_RAMP_UP_WINDOW_MILLISECONDS) { rampUpTPS = expectedTPS * elapsedTime / ((double) (TPS_RAMP_UP_WINDOW_MILLISECONDS)); @@ -314,4 +320,21 @@ public StateLifecycles newStateLifecycles() { public BasicSoftwareVersion getSoftwareVersion() { return softwareVersion; } + + @Override + @NonNull + public Bytes encodeSystemTransaction(@NonNull final StateSignatureTransaction transaction) { + final var bytes = new ByteArrayOutputStream(); + final var out = new WritableStreamingData(bytes); + + // Add a marker to indicate the start of a system transaction. This is used + // to later differentiate between application transactions and system transactions. + out.writeByte(SYSTEM_TRANSACTION_MARKER); + try { + StateSignatureTransaction.PROTOBUF.write(transaction, out); + return Bytes.wrap(bytes.toByteArray()); + } catch (final IOException e) { + throw new IllegalStateException("Failed to encode a system transaction.", e); + } + } } diff --git a/platform-sdk/platform-apps/tests/StatsSigningTestingTool/src/main/java/com/swirlds/demo/stats/signing/StatsSigningTestingToolStateLifecycles.java b/platform-sdk/platform-apps/tests/StatsSigningTestingTool/src/main/java/com/swirlds/demo/stats/signing/StatsSigningTestingToolStateLifecycles.java index 5e7d7e93b88..8bccbc435d4 100644 --- a/platform-sdk/platform-apps/tests/StatsSigningTestingTool/src/main/java/com/swirlds/demo/stats/signing/StatsSigningTestingToolStateLifecycles.java +++ b/platform-sdk/platform-apps/tests/StatsSigningTestingTool/src/main/java/com/swirlds/demo/stats/signing/StatsSigningTestingToolStateLifecycles.java @@ -17,10 +17,13 @@ package com.swirlds.demo.stats.signing; import static com.swirlds.common.utility.CommonUtils.hex; +import static com.swirlds.demo.stats.signing.StatsSigningTestingToolMain.SYSTEM_TRANSACTION_MARKER; import static com.swirlds.logging.legacy.LogMarker.EXCEPTION; import static com.swirlds.logging.legacy.LogMarker.TESTING_EXCEPTIONS_ACCEPTABLE_RECONNECT; import com.hedera.hapi.platform.event.StateSignatureTransaction; +import com.hedera.pbj.runtime.ParseException; +import com.hedera.pbj.runtime.io.buffer.Bytes; import com.swirlds.common.context.PlatformContext; import com.swirlds.common.crypto.CryptographyHolder; import com.swirlds.common.crypto.TransactionSignature; @@ -77,12 +80,27 @@ public void onPreHandle( @NonNull Event event, @NonNull StatsSigningTestingToolState state, @NonNull Consumer> stateSignatureTransactionCallback) { + final SttTransactionPool sttTransactionPool = transactionPoolSupplier.get(); if (sttTransactionPool != null) { 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); + return; + } + final TransactionSignature transactionSignature = sttTransactionPool.expandSignatures(transaction.getApplicationTransaction()); if (transactionSignature != null) { @@ -99,13 +117,29 @@ public void onHandleConsensusRound( @NonNull StatsSigningTestingToolState state, @NonNull Consumer> stateSignatureTransactionCallback) { state.throwIfImmutable(); - round.forEachTransaction(v -> handleTransaction(v, state)); + + round.forEachEventTransaction((event, 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, state); + } + }); } private void handleTransaction(final ConsensusTransaction trans, final StatsSigningTestingToolState state) { - if (trans.isSystem()) { - return; - } final TransactionSignature s = trans.getMetadata(); if (s != null && validateSignature(s, trans) && s.getSignatureStatus() != VerificationStatus.VALID) { @@ -133,6 +167,40 @@ private void handleTransaction(final ConsensusTransaction trans, final StatsSign maybeDelay(); } + /** + * Checks if the transaction bytes are system ones. + * + * @param transaction the transaction to check + * @return true if the transaction bytes are system ones, false otherwise + */ + private boolean areTransactionBytesSystemOnes(@NonNull final Transaction transaction) { + final var transactionBytes = transaction.getApplicationTransaction(); + + if (transactionBytes.length() == 0) { + return false; + } + + return transactionBytes.getByte(0) == SYSTEM_TRANSACTION_MARKER; + } + + private void consumeSystemTransaction( + @NonNull final Transaction transaction, + @NonNull final Event event, + @NonNull + final Consumer> + stateSignatureTransactionCallback) { + try { + final Bytes transactionBytes = transaction.getApplicationTransaction(); + final Bytes strippedSystemTransactionBytes = transactionBytes.slice(1, transactionBytes.length() - 1); + final StateSignatureTransaction stateSignatureTransaction = + StateSignatureTransaction.PROTOBUF.parse(strippedSystemTransactionBytes); + stateSignatureTransactionCallback.accept(new ScopedSystemTransaction<>( + event.getCreatorId(), event.getSoftwareVersion(), stateSignatureTransaction)); + } catch (final ParseException e) { + logger.error("Failed to parse StateSignatureTransaction", e); + } + } + private void maybeDelay() { if (SYNTHETIC_HANDLE_TIME) { final long start = System.nanoTime(); diff --git a/platform-sdk/platform-apps/tests/StatsSigningTestingTool/src/main/java/com/swirlds/demo/stats/signing/SttTransactionPool.java b/platform-sdk/platform-apps/tests/StatsSigningTestingTool/src/main/java/com/swirlds/demo/stats/signing/SttTransactionPool.java index 604a6672737..51dee62964d 100644 --- a/platform-sdk/platform-apps/tests/StatsSigningTestingTool/src/main/java/com/swirlds/demo/stats/signing/SttTransactionPool.java +++ b/platform-sdk/platform-apps/tests/StatsSigningTestingTool/src/main/java/com/swirlds/demo/stats/signing/SttTransactionPool.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. @@ -165,7 +165,7 @@ private void init() { final byte[] sig = exSig.getSignature(); transactions[i] = TransactionCodec.encode(alg, transactionId, sig, data); - } catch (SignatureException e) { + } catch (final SignatureException e) { // If we are unable to sign the transaction then log the failure and create an unsigned transaction logger.error( EXCEPTION.getMarker(), @@ -202,7 +202,7 @@ private void tryAcquirePrimitives() { if (algorithm.isAvailable()) { activeAlgorithms.put(algorithm.getId(), algorithm); } - } catch (Exception ex) { + } catch (final Exception ex) { logger.error( EXCEPTION.getMarker(), "Failed to Activate Signing Algorithm [ id = {}, class = {} ]", diff --git a/platform-sdk/platform-apps/tests/StatsSigningTestingTool/src/main/java/com/swirlds/demo/stats/signing/TransactionCodec.java b/platform-sdk/platform-apps/tests/StatsSigningTestingTool/src/main/java/com/swirlds/demo/stats/signing/TransactionCodec.java index f7d27a36147..abbc068a47c 100644 --- a/platform-sdk/platform-apps/tests/StatsSigningTestingTool/src/main/java/com/swirlds/demo/stats/signing/TransactionCodec.java +++ b/platform-sdk/platform-apps/tests/StatsSigningTestingTool/src/main/java/com/swirlds/demo/stats/signing/TransactionCodec.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. @@ -30,14 +30,15 @@ * The core transaction encoder and decoder implementation. See below for the binary transaction format specification. *

* Transaction Structure: - * ------------------------------------------------------------------------------------------------------------ - * | 8 bytes | 1 byte | 1 byte | 4 bytes | pklen bytes | 4 bytes | siglen bytes | 4 bytes | datalen bytes | - * |---------|--------|----------|-----------|-------------|---------|--------------|---------|---------------| - * | id | signed | sigAlgId | pklen | pk | siglen | sig | datalen | data | - * ------------------------------------------------------------------------------------------------------------ + * --------------------------------------------------------------------------------------------------------------------- + * | 1 byte | 8 bytes | 1 byte | 1 byte | 4 bytes | pklen bytes | 4 bytes | siglen bytes | 4 bytes | datalen bytes | + * |--------|---------|--------|----------|-----------|-------------|---------|--------------|---------|---------------| + * | marker | id | signed | sigAlgId | pklen | pk | siglen | sig | datalen | data | + * --------------------------------------------------------------------------------------------------------------------- */ final class TransactionCodec { + public static final byte APPLICATION_TRANSACTION_MARKER = 1; public static final byte NO_ALGORITHM_PRESENT = -1; private static final int PREAMBLE_SIZE = Long.BYTES + (Byte.BYTES * 2); @@ -78,11 +79,14 @@ public static int overheadSize(final SigningAlgorithm algorithm) { public static byte[] encode( final SigningAlgorithm algorithm, final long transactionId, final byte[] signature, final byte[] data) { - final ByteBuffer buffer = ByteBuffer.allocate(bufferSize(algorithm, (data != null) ? data.length : 0)); + final ByteBuffer buffer = ByteBuffer.allocate(1 + bufferSize(algorithm, (data != null) ? data.length : 0)); final boolean signed = algorithm != null && algorithm.isAvailable() && signature != null && signature.length > 0; - buffer.putLong(transactionId) + // Add a marker byte in the very beginning to indicate the start of an application transaction. This is used + // to later differentiate between application transactions and system transactions. + buffer.put(APPLICATION_TRANSACTION_MARKER) + .putLong(transactionId) .put((signed) ? (byte) 1 : 0) .put((signed) ? algorithm.getId() : NO_ALGORITHM_PRESENT) .putInt((signed) ? algorithm.getPublicKeyLength() : 0); diff --git a/platform-sdk/platform-apps/tests/StatsSigningTestingTool/src/test/java/com/swirlds/demo/stats/signing/StatsSigningTestingToolStateTest.java b/platform-sdk/platform-apps/tests/StatsSigningTestingTool/src/test/java/com/swirlds/demo/stats/signing/StatsSigningTestingToolStateTest.java new file mode 100644 index 00000000000..705d431da60 --- /dev/null +++ b/platform-sdk/platform-apps/tests/StatsSigningTestingTool/src/test/java/com/swirlds/demo/stats/signing/StatsSigningTestingToolStateTest.java @@ -0,0 +1,305 @@ +/* + * 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.stats.signing; + +import static com.hedera.hapi.platform.event.EventTransaction.TransactionOneOfType.APPLICATION_TRANSACTION; +import static com.hedera.hapi.platform.event.EventTransaction.TransactionOneOfType.STATE_SIGNATURE_TRANSACTION; +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.hedera.hapi.node.base.SemanticVersion; +import com.hedera.hapi.node.base.Timestamp; +import com.hedera.hapi.node.state.roster.Roster; +import com.hedera.hapi.platform.event.EventCore; +import com.hedera.hapi.platform.event.EventTransaction; +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.common.platform.NodeId; +import com.swirlds.demo.stats.signing.algorithms.X25519SigningAlgorithm; +import com.swirlds.platform.components.transaction.system.ScopedSystemTransaction; +import com.swirlds.platform.consensus.ConsensusSnapshot; +import com.swirlds.platform.consensus.EventWindow; +import com.swirlds.platform.event.AncientMode; +import com.swirlds.platform.event.PlatformEvent; +import com.swirlds.platform.gossip.shadowgraph.Generations; +import com.swirlds.platform.internal.ConsensusRound; +import com.swirlds.platform.system.Round; +import com.swirlds.platform.system.SoftwareVersion; +import com.swirlds.platform.system.transaction.ConsensusTransaction; +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 java.util.function.Supplier; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class StatsSigningTestingToolStateTest { + + private static final int transactionSize = 100; + private Random random; + private StatsSigningTestingToolState state; + private StatsSigningTestingToolStateLifecycles stateLifecycles; + private StatsSigningTestingToolMain main; + private Round round; + private PlatformEvent event; + private Consumer> consumer; + private List> consumedSystemTransactions; + private ConsensusTransaction consensusTransaction; + private StateSignatureTransaction stateSignatureTransaction; + + @BeforeEach + void setUp() { + final SttTransactionPool transactionPool = mock(SttTransactionPool.class); + final Supplier transactionPoolSupplier = mock(Supplier.class); + final Function versionFactory = mock(Function.class); + state = new StatsSigningTestingToolState(versionFactory); + stateLifecycles = new StatsSigningTestingToolStateLifecycles(transactionPoolSupplier); + main = new StatsSigningTestingToolMain(); + random = new Random(); + event = mock(PlatformEvent.class); + + final var eventWindow = new EventWindow(10, 5, 20, AncientMode.BIRTH_ROUND_THRESHOLD); + final var roster = new Roster(Collections.EMPTY_LIST); + when(event.transactionIterator()).thenReturn(Collections.emptyIterator()); + round = new ConsensusRound( + roster, + List.of(event), + event, + new Generations(), + eventWindow, + new ConsensusSnapshot(), + false, + Instant.now()); + + consumedSystemTransactions = new ArrayList<>(); + consumer = systemTransaction -> consumedSystemTransactions.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(); + + when(transactionPoolSupplier.get()).thenReturn(transactionPool); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void handleConsensusRoundWithApplicationTransaction(final boolean signedTransaction) throws SignatureException { + // Given + givenRoundAndEvent(); + + final var transactionBytes = + signedTransaction ? getSignedApplicationTransaction() : getUnsignedApplicationTransaction(); + + when(consensusTransaction.getApplicationTransaction()).thenReturn(transactionBytes); + + // When + stateLifecycles.onHandleConsensusRound(round, state, consumer); + + // Then + assertThat(consumedSystemTransactions.size()).isZero(); + } + + @Test + void handleConsensusRoundWithSystemTransaction() { + // Given + givenRoundAndEvent(); + + final var stateSignatureTransactionBytes = main.encodeSystemTransaction(stateSignatureTransaction); + when(consensusTransaction.getApplicationTransaction()).thenReturn(stateSignatureTransactionBytes); + + // When + stateLifecycles.onHandleConsensusRound(round, state, consumer); + + // Then + assertThat(consumedSystemTransactions.size()).isEqualTo(1); + } + + @Test + void handleConsensusRoundWithMultipleSystemTransaction() { + // Given + final var secondConsensusTransaction = mock(TransactionWrapper.class); + final var thirdConsensusTransaction = mock(TransactionWrapper.class); + when(event.getConsensusTimestamp()).thenReturn(Instant.now()); + when(event.consensusTransactionIterator()) + .thenReturn(List.of(consensusTransaction, secondConsensusTransaction, thirdConsensusTransaction) + .iterator()); + + final var stateSignatureTransactionBytes = main.encodeSystemTransaction(stateSignatureTransaction); + + when(consensusTransaction.getApplicationTransaction()).thenReturn(stateSignatureTransactionBytes); + when(secondConsensusTransaction.getApplicationTransaction()).thenReturn(stateSignatureTransactionBytes); + when(thirdConsensusTransaction.getApplicationTransaction()).thenReturn(stateSignatureTransactionBytes); + + // When + stateLifecycles.onHandleConsensusRound(round, state, consumer); + + // Then + assertThat(consumedSystemTransactions.size()).isEqualTo(3); + } + + @Test + void handleConsensusRoundWithDeprecatedSystemTransaction() { + // Given + givenRoundAndEvent(); + + when(consensusTransaction.getApplicationTransaction()).thenReturn(Bytes.EMPTY); + when(consensusTransaction.isSystem()).thenReturn(true); + + // When + stateLifecycles.onHandleConsensusRound(round, state, consumer); + + // Then + assertThat(consumedSystemTransactions.size()).isZero(); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void preHandleConsensusRoundWithApplicationTransaction(final boolean signedTransaction) throws SignatureException { + // Given + givenRoundAndEvent(); + + final var bytes = signedTransaction ? getSignedApplicationTransaction() : getUnsignedApplicationTransaction(); + + final var eventTransaction = new EventTransaction(new OneOf<>(APPLICATION_TRANSACTION, bytes)); + final var eventCore = mock(EventCore.class); + final var gossipEvent = new GossipEvent(eventCore, null, List.of(eventTransaction), Collections.emptyList()); + when(eventCore.timeCreated()).thenReturn(Timestamp.DEFAULT); + event = new PlatformEvent(gossipEvent); + + // When + stateLifecycles.onPreHandle(event, state, consumer); + + // Then + assertThat(consumedSystemTransactions.size()).isZero(); + } + + @Test + void preHandleConsensusRoundWithSystemTransaction() { + // Given + givenRoundAndEvent(); + + final var stateSignatureTransactionBytes = main.encodeSystemTransaction(stateSignatureTransaction); + final var eventTransaction = + new EventTransaction(new OneOf<>(APPLICATION_TRANSACTION, stateSignatureTransactionBytes)); + final var eventCore = mock(EventCore.class); + final var gossipEvent = new GossipEvent(eventCore, null, List.of(eventTransaction), Collections.emptyList()); + when(eventCore.timeCreated()).thenReturn(Timestamp.DEFAULT); + event = new PlatformEvent(gossipEvent); + + // When + stateLifecycles.onPreHandle(event, state, consumer); + + // Then + assertThat(consumedSystemTransactions.size()).isEqualTo(1); + } + + @Test + void preHandleConsensusRoundWithMultipleSystemTransaction() { + // Given + when(event.getConsensusTimestamp()).thenReturn(Instant.now()); + + final var stateSignatureTransactionBytes = main.encodeSystemTransaction(stateSignatureTransaction); + + final var eventTransaction = + new EventTransaction(new OneOf<>(APPLICATION_TRANSACTION, stateSignatureTransactionBytes)); + final var secondEventTransaction = + new EventTransaction(new OneOf<>(APPLICATION_TRANSACTION, stateSignatureTransactionBytes)); + final var thirdEventTransaction = + new EventTransaction(new OneOf<>(APPLICATION_TRANSACTION, stateSignatureTransactionBytes)); + final var eventCore = mock(EventCore.class); + final var gossipEvent = new GossipEvent( + eventCore, + null, + List.of(eventTransaction, secondEventTransaction, thirdEventTransaction), + Collections.emptyList()); + when(eventCore.timeCreated()).thenReturn(Timestamp.DEFAULT); + event = new PlatformEvent(gossipEvent); + + // When + stateLifecycles.onPreHandle(event, state, consumer); + + // Then + assertThat(consumedSystemTransactions.size()).isEqualTo(3); + } + + @Test + void preHandleConsensusRoundWithDeprecatedSystemTransaction() { + // Given + givenRoundAndEvent(); + + final var stateSignatureTransactionBytes = + StateSignatureTransaction.PROTOBUF.toBytes(stateSignatureTransaction); + final var eventTransaction = + new EventTransaction(new OneOf<>(STATE_SIGNATURE_TRANSACTION, stateSignatureTransactionBytes)); + final var eventCore = mock(EventCore.class); + final var gossipEvent = new GossipEvent(eventCore, null, List.of(eventTransaction), Collections.emptyList()); + when(eventCore.timeCreated()).thenReturn(Timestamp.DEFAULT); + event = new PlatformEvent(gossipEvent); + + // When + stateLifecycles.onPreHandle(event, state, consumer); + + // Then + assertThat(consumedSystemTransactions.size()).isZero(); + } + + private void givenRoundAndEvent() { + when(event.getCreatorId()).thenReturn(new NodeId()); + when(event.getSoftwareVersion()).thenReturn(new SemanticVersion(1, 1, 1, "", "")); + when(event.getConsensusTimestamp()).thenReturn(Instant.now()); + when(event.consensusTransactionIterator()) + .thenReturn(Collections.singletonList(consensusTransaction).iterator()); + } + + private Bytes getSignedApplicationTransaction() throws SignatureException { + final byte[] data = new byte[transactionSize]; + random.nextBytes(data); + + final var alg = new X25519SigningAlgorithm(); + alg.tryAcquirePrimitives(); + final var exSig = alg.signEx(data, 0, data.length); + final var sig = exSig.getSignature(); + final var transactionId = 80_000L; + return Bytes.wrap(TransactionCodec.encode(alg, transactionId, sig, data)); + } + + private Bytes getUnsignedApplicationTransaction() { + final byte[] data = new byte[transactionSize]; + random.nextBytes(data); + + final var transactionId = 80_000L; + return Bytes.wrap(TransactionCodec.encode(null, transactionId, null, data)); + } +}