diff --git a/platform-sdk/platform-apps/tests/AddressBookTestingTool/build.gradle.kts b/platform-sdk/platform-apps/tests/AddressBookTestingTool/build.gradle.kts index 39ec1c99fb74..22d19745b186 100644 --- a/platform-sdk/platform-apps/tests/AddressBookTestingTool/build.gradle.kts +++ b/platform-sdk/platform-apps/tests/AddressBookTestingTool/build.gradle.kts @@ -1,4 +1,25 @@ -// 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.addressbook.AddressBookTestingToolMain" + +testModuleInfo { + requires("org.assertj.core") + requires("org.junit.jupiter.api") + requires("org.mockito") +} diff --git a/platform-sdk/platform-apps/tests/AddressBookTestingTool/src/main/java/com/swirlds/demo/addressbook/AddressBookTestingToolMain.java b/platform-sdk/platform-apps/tests/AddressBookTestingTool/src/main/java/com/swirlds/demo/addressbook/AddressBookTestingToolMain.java index 8c0357a8f040..90ab90a8e10b 100644 --- a/platform-sdk/platform-apps/tests/AddressBookTestingTool/src/main/java/com/swirlds/demo/addressbook/AddressBookTestingToolMain.java +++ b/platform-sdk/platform-apps/tests/AddressBookTestingTool/src/main/java/com/swirlds/demo/addressbook/AddressBookTestingToolMain.java @@ -21,6 +21,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; @@ -163,4 +165,9 @@ public BasicSoftwareVersion getSoftwareVersion() { logger.info(STARTUP.getMarker(), "returning software version {}", softwareVersion); return softwareVersion; } + + @Override + public Bytes encodeSystemTransaction(@NonNull final StateSignatureTransaction transaction) { + return StateSignatureTransaction.PROTOBUF.toBytes(transaction); + } } diff --git a/platform-sdk/platform-apps/tests/AddressBookTestingTool/src/main/java/com/swirlds/demo/addressbook/AddressBookTestingToolState.java b/platform-sdk/platform-apps/tests/AddressBookTestingTool/src/main/java/com/swirlds/demo/addressbook/AddressBookTestingToolState.java index 3ff85fe37e7e..71d302cf9a87 100644 --- a/platform-sdk/platform-apps/tests/AddressBookTestingTool/src/main/java/com/swirlds/demo/addressbook/AddressBookTestingToolState.java +++ b/platform-sdk/platform-apps/tests/AddressBookTestingTool/src/main/java/com/swirlds/demo/addressbook/AddressBookTestingToolState.java @@ -59,8 +59,9 @@ import com.swirlds.platform.system.address.Address; import com.swirlds.platform.system.address.AddressBook; import com.swirlds.platform.system.address.AddressBookUtils; -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.state.merkle.singleton.StringLeaf; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; @@ -70,7 +71,6 @@ import java.nio.file.Path; import java.text.ParseException; import java.time.Duration; -import java.util.Iterator; import java.util.Objects; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; @@ -214,6 +214,29 @@ public void init( logger.info(STARTUP.getMarker(), "Registered PlatformService and RosterService states."); } + @Override + public void preHandle( + @NonNull final Event event, + @NonNull final Consumer> stateSignatureTransaction) { + event.transactionIterator().forEachRemaining(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, stateSignatureTransaction); + } + }); + } + /** * {@inheritDoc} */ @@ -238,11 +261,26 @@ public void handleConsensusRound( roundsHandled++; setChild(ROUND_HANDLED_INDEX, new StringLeaf(Long.toString(roundsHandled))); - final Iterator eventIterator = round.iterator(); + for (final var event : round) { + 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; + } - while (eventIterator.hasNext()) { - final ConsensusEvent event = eventIterator.next(); - event.consensusTransactionIterator().forEachRemaining(this::handleTransaction); + // We should consume in the callback the new form of system transactions in Bytes + if (areTransactionBytesSystemOnes(transaction)) { + consumeSystemTransaction(transaction, event, stateSignatureTransaction); + } else { + handleTransaction(transaction); + } + }); } if (!validationPerformed.getAndSet(true)) { @@ -255,15 +293,44 @@ public void handleConsensusRound( } } + /** + * 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; + } + + /** + * Converts a transaction to a {@link StateSignatureTransaction} and then consumes it into a callback. + * + * @param transaction the transaction to consume + * @param event the event that contains the transaction + * @param stateSignatureTransactionCallback the callback to call with the system transaction + */ + 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 com.hedera.pbj.runtime.ParseException e) { + logger.error("Failed to parse StateSignatureTransaction", e); + } + } + /** * Apply a transaction to the state. * * @param transaction the transaction to apply */ private void handleTransaction(@NonNull final ConsensusTransaction transaction) { - if (transaction.isSystem()) { - return; - } final int delta = ByteUtils.byteArrayToInt(transaction.getApplicationTransaction().toByteArray(), 0); runningSum += delta; diff --git a/platform-sdk/platform-apps/tests/AddressBookTestingTool/src/test/java/com/swirlds/demo/addressbook/AddressBookTestingToolStateTest.java b/platform-sdk/platform-apps/tests/AddressBookTestingTool/src/test/java/com/swirlds/demo/addressbook/AddressBookTestingToolStateTest.java new file mode 100644 index 000000000000..8c1475fd9a5f --- /dev/null +++ b/platform-sdk/platform-apps/tests/AddressBookTestingTool/src/test/java/com/swirlds/demo/addressbook/AddressBookTestingToolStateTest.java @@ -0,0 +1,328 @@ +/* + * 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.addressbook; + +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.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +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.context.PlatformContext; +import com.swirlds.config.api.Configuration; +import com.swirlds.platform.components.transaction.system.ScopedSystemTransaction; +import com.swirlds.platform.config.AddressBookConfig; +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.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.Duration; +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.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class AddressBookTestingToolStateTest { + + private static final int RUNNING_SUM_INDEX = 3; + private static AddressBookTestingToolState state; + private AddressBookTestingToolMain main; + 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 AddressBookConfig addressBookConfig; + private AddressBookTestingToolConfig addressBookTestingToolConfig; + + @BeforeAll + static void initState() { + state = new AddressBookTestingToolState(FAKE_MERKLE_STATE_LIFECYCLES, mock(Function.class)); + FAKE_MERKLE_STATE_LIFECYCLES.initStates(state); + } + + @BeforeEach + void setUp() { + state.setChild(RUNNING_SUM_INDEX, new StringLeaf("0")); + platform = mock(Platform.class); + initTrigger = InitTrigger.GENESIS; + softwareVersion = new BasicSoftwareVersion(1); + platformContext = mock(PlatformContext.class); + configuration = mock(Configuration.class); + addressBookConfig = mock(AddressBookConfig.class); + addressBookTestingToolConfig = mock(AddressBookTestingToolConfig.class); + + when(platform.getContext()).thenReturn(platformContext); + when(platformContext.getConfiguration()).thenReturn(configuration); + when(configuration.getConfigData(AddressBookConfig.class)).thenReturn(addressBookConfig); + when(configuration.getConfigData(AddressBookTestingToolConfig.class)).thenReturn(addressBookTestingToolConfig); + when(addressBookTestingToolConfig.freezeAfterGenesis()).thenReturn(Duration.ZERO); + when(addressBookTestingToolConfig.testScenario()) + .thenReturn(String.valueOf(AddressBookTestScenario.GENESIS_NORMAL)); + + state.init(platform, initTrigger, softwareVersion); + + main = mock(AddressBookTestingToolMain.class); + 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(); + } + + @AfterEach + void tearDown() { + state.setChild(RUNNING_SUM_INDEX, null); + } + + @Test + void handleConsensusRoundWithApplicationTransaction() { + // Given + givenRoundAndEvent(); + + final var bytes = Bytes.wrap(new byte[] {1, 1, 1, 1}); + when(consensusTransaction.getApplicationTransaction()).thenReturn(bytes); + + // When + state.handleConsensusRound(round, platformStateModifier, consumer); + + // Then + verify(round, times(1)).iterator(); + 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(consensusTransaction.getApplicationTransaction()).thenReturn(stateSignatureTransactionBytes); + + // When + state.handleConsensusRound(round, platformStateModifier, consumer); + + // Then + verify(round, times(1)).iterator(); + verify(event, times(1)).consensusTransactionIterator(); + + assertThat(Long.parseLong(((StringLeaf) state.getChild(RUNNING_SUM_INDEX)).getLabel())) + .isZero(); + 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) consensusTransaction, + secondConsensusTransaction, + thirdConsensusTransaction) + .iterator()); + + final var stateSignatureTransactionBytes = + StateSignatureTransaction.PROTOBUF.toBytes(stateSignatureTransaction); + when(main.encodeSystemTransaction(stateSignatureTransaction)).thenReturn(stateSignatureTransactionBytes); + when(consensusTransaction.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(1)).consensusTransactionIterator(); + + assertThat(Long.parseLong(((StringLeaf) state.getChild(RUNNING_SUM_INDEX)).getLabel())) + .isZero(); + assertThat(consumedTransactions).hasSize(3); + } + + @Test + void handleConsensusRoundWithDeprecatedSystemTransaction() { + // Given + givenRoundAndEvent(); + + when(consensusTransaction.getApplicationTransaction()).thenReturn(Bytes.EMPTY); + when(consensusTransaction.isSystem()).thenReturn(true); + + // When + state.handleConsensusRound(round, platformStateModifier, consumer); + + // Then + verify(round, times(1)).iterator(); + verify(event, times(1)).consensusTransactionIterator(); + + assertThat(Long.parseLong(((StringLeaf) state.getChild(RUNNING_SUM_INDEX)).getLabel())) + .isZero(); + 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(consensusTransaction.isSystem()).thenReturn(false); + when(consensusTransaction.getApplicationTransaction()).thenReturn(emptyStateSignatureTransactionBytes); + + // When + state.handleConsensusRound(round, platformStateModifier, consumer); + + // Then + verify(round, times(1)).iterator(); + verify(event, times(1)).consensusTransactionIterator(); + + System.out.println(consumedTransactions); + assertThat(Long.parseLong(((StringLeaf) state.getChild(RUNNING_SUM_INDEX)).getLabel())) + .isZero(); + assertThat(consumedTransactions).isEmpty(); + } + + @Test + void preHandleEventWithMultipleSystemTransaction() { + // Given + when(round.iterator()).thenReturn(Collections.singletonList(event).iterator()); + final var secondConsensusTransaction = mock(TransactionWrapper.class); + final var thirdConsensusTransaction = mock(TransactionWrapper.class); + when(event.transactionIterator()) + .thenReturn(List.of(consensusTransaction, secondConsensusTransaction, thirdConsensusTransaction) + .iterator()); + final var stateSignatureTransactionBytes = + StateSignatureTransaction.PROTOBUF.toBytes(stateSignatureTransaction); + when(consensusTransaction.getApplicationTransaction()).thenReturn(stateSignatureTransactionBytes); + when(secondConsensusTransaction.getApplicationTransaction()).thenReturn(stateSignatureTransactionBytes); + when(thirdConsensusTransaction.getApplicationTransaction()).thenReturn(stateSignatureTransactionBytes); + + // When + state.preHandle(event, consumer); + + // Then + assertThat(consumedTransactions).hasSize(3); + } + + @Test + void preHandleEventWithSystemTransaction() { + // Given + when(round.iterator()).thenReturn(Collections.singletonList(event).iterator()); + when(event.transactionIterator()) + .thenReturn(Collections.singletonList(consensusTransaction).iterator()); + final var emptyStateSignatureBytes = StateSignatureTransaction.PROTOBUF.toBytes(stateSignatureTransaction); + when(consensusTransaction.getApplicationTransaction()).thenReturn(emptyStateSignatureBytes); + + // When + state.preHandle(event, consumer); + + // Then + assertThat(consumedTransactions).hasSize(1); + } + + @Test + void preHandleEventWithDeprecatedSystemTransaction() { + // Given + when(round.iterator()).thenReturn(Collections.singletonList(event).iterator()); + when(event.transactionIterator()) + .thenReturn(Collections.singletonList(consensusTransaction).iterator()); + when(consensusTransaction.isSystem()).thenReturn(true); + + // When + state.preHandle(event, consumer); + + // Then + assertThat(consumedTransactions).isEmpty(); + } + + @Test + void preHandleEventWithEmptyTransaction() { + // Given + when(round.iterator()).thenReturn(Collections.singletonList(event).iterator()); + when(event.transactionIterator()) + .thenReturn(Collections.singletonList(consensusTransaction).iterator()); + final var emptyStateSignatureBytes = + StateSignatureTransaction.PROTOBUF.toBytes(StateSignatureTransaction.DEFAULT); + when(consensusTransaction.getApplicationTransaction()).thenReturn(emptyStateSignatureBytes); + + // 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) consensusTransaction) + .iterator()); + } +}