diff --git a/hapi/src/main/java/module-info.java b/hapi/src/main/java/module-info.java index cf2294fa9964..4c84103d08e7 100644 --- a/hapi/src/main/java/module-info.java +++ b/hapi/src/main/java/module-info.java @@ -69,10 +69,11 @@ exports com.hedera.hapi.block.stream.protoc; exports com.hedera.hapi.block; exports com.hedera.hapi.services.auxiliary.tss.legacy; + exports com.hedera.hapi.platform.event.legacy; + requires transitive com.hedera.pbj.runtime; requires transitive com.google.common; requires transitive com.google.protobuf; - requires transitive com.hedera.pbj.runtime; requires transitive io.grpc.stub; requires transitive io.grpc; requires io.grpc.protobuf; diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/Hedera.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/Hedera.java index 5229f5e64740..23ab533dd860 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/Hedera.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/Hedera.java @@ -57,6 +57,7 @@ import com.hedera.hapi.node.state.blockstream.BlockStreamInfo; import com.hedera.hapi.node.transaction.ThrottleDefinitions; import com.hedera.hapi.node.transaction.TransactionBody; +import com.hedera.hapi.platform.event.StateSignatureTransaction; import com.hedera.hapi.platform.state.PlatformState; import com.hedera.hapi.util.HapiUtils; import com.hedera.hapi.util.UnknownHederaFunctionality; @@ -128,6 +129,7 @@ import com.swirlds.common.platform.NodeId; import com.swirlds.config.api.Configuration; import com.swirlds.metrics.api.Metrics; +import com.swirlds.platform.components.transaction.system.ScopedSystemTransaction; import com.swirlds.platform.config.AddressBookConfig; import com.swirlds.platform.listeners.PlatformStatusChangeListener; import com.swirlds.platform.listeners.PlatformStatusChangeNotification; @@ -166,6 +168,7 @@ import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.function.BiConsumer; +import java.util.function.Consumer; import java.util.function.Supplier; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -821,7 +824,10 @@ public void shutdown() { /** * Invoked by the platform to handle pre-consensus events. This only happens after {@link #run()} has been called. */ - public void onPreHandle(@NonNull final Event event, @NonNull final State state) { + public void onPreHandle( + @NonNull final Event event, + @NonNull final State state, + @NonNull final Consumer> stateSignatureTxnCallback) { final var readableStoreFactory = new ReadableStoreFactory(state); final var creator = daggerApp.networkInfo().nodeInfo(event.getCreatorId().id()); @@ -837,9 +843,20 @@ public void onPreHandle(@NonNull final Event event, @NonNull final State state) return; } + final Consumer simplifiedStateSignatureTxnCallback = txn -> { + final var scopedTxn = new ScopedSystemTransaction<>(event.getCreatorId(), event.getSoftwareVersion(), txn); + stateSignatureTxnCallback.accept(scopedTxn); + }; + final var transactions = new ArrayList(1000); event.forEachTransaction(transactions::add); - daggerApp.preHandleWorkflow().preHandle(readableStoreFactory, creator.accountId(), transactions.stream()); + daggerApp + .preHandleWorkflow() + .preHandle( + readableStoreFactory, + creator.accountId(), + transactions.stream(), + simplifiedStateSignatureTxnCallback); } public void onNewRecoveredState(@NonNull final State recoveredState) { @@ -862,9 +879,12 @@ public static boolean shouldDump(@NonNull final InitTrigger trigger, @NonNull fi * Invoked by the platform to handle a round of consensus events. This only happens after {@link #run()} has been * called. */ - public void onHandleConsensusRound(@NonNull final Round round, @NonNull final State state) { + public void onHandleConsensusRound( + @NonNull final Round round, + @NonNull final State state, + @NonNull final Consumer> stateSignatureTxnCallback) { daggerApp.workingStateAccessor().setState(state); - daggerApp.handleWorkflow().handleRound(state, round); + daggerApp.handleWorkflow().handleRound(state, round, stateSignatureTxnCallback); } /** diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/state/StateLifecyclesImpl.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/state/StateLifecyclesImpl.java index 6421a01f64f2..34f9baf11bf9 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/state/StateLifecyclesImpl.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/state/StateLifecyclesImpl.java @@ -18,8 +18,10 @@ import static java.util.Objects.requireNonNull; +import com.hedera.hapi.platform.event.StateSignatureTransaction; import com.hedera.node.app.Hedera; import com.swirlds.common.context.PlatformContext; +import com.swirlds.platform.components.transaction.system.ScopedSystemTransaction; import com.swirlds.platform.state.StateLifecycles; import com.swirlds.platform.system.InitTrigger; import com.swirlds.platform.system.Platform; @@ -30,6 +32,7 @@ import com.swirlds.state.State; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; +import java.util.function.Consumer; /** * Implements the major lifecycle events for Hedera Services by delegating to a Hedera instance. @@ -42,13 +45,19 @@ public StateLifecyclesImpl(@NonNull final Hedera hedera) { } @Override - public void onPreHandle(@NonNull final Event event, @NonNull final State state) { - hedera.onPreHandle(event, state); + public void onPreHandle( + @NonNull final Event event, + @NonNull final State state, + @NonNull Consumer> stateSignatureTransactionCallback) { + hedera.onPreHandle(event, state, stateSignatureTransactionCallback); } @Override - public void onHandleConsensusRound(@NonNull final Round round, @NonNull final State state) { - hedera.onHandleConsensusRound(round, state); + public void onHandleConsensusRound( + @NonNull final Round round, + @NonNull final State state, + @NonNull Consumer> stateSignatureTxnCallback) { + hedera.onHandleConsensusRound(round, state, stateSignatureTxnCallback); } @Override diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/HandleWorkflow.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/HandleWorkflow.java index 418e52bbc485..e8b9841eaaf2 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/HandleWorkflow.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/HandleWorkflow.java @@ -46,11 +46,13 @@ import com.hedera.hapi.block.stream.input.EventHeader; import com.hedera.hapi.block.stream.input.RoundHeader; import com.hedera.hapi.block.stream.output.StateChanges; +import com.hedera.hapi.node.base.HederaFunctionality; import com.hedera.hapi.node.base.ResponseCodeEnum; import com.hedera.hapi.node.base.SemanticVersion; import com.hedera.hapi.node.base.Transaction; import com.hedera.hapi.node.state.blockrecords.BlockInfo; import com.hedera.hapi.node.transaction.ExchangeRateSet; +import com.hedera.hapi.platform.event.StateSignatureTransaction; import com.hedera.hapi.util.HapiUtils; import com.hedera.node.app.blocks.BlockStreamManager; import com.hedera.node.app.blocks.impl.BlockStreamBuilder; @@ -90,12 +92,14 @@ import com.hedera.node.app.workflows.handle.steps.StakePeriodChanges; import com.hedera.node.app.workflows.handle.steps.UserTxn; import com.hedera.node.app.workflows.handle.steps.UserTxnFactory; +import com.hedera.node.app.workflows.prehandle.PreHandleResult; import com.hedera.node.config.ConfigProvider; import com.hedera.node.config.data.BlockStreamConfig; import com.hedera.node.config.data.ConsensusConfig; import com.hedera.node.config.data.SchedulingConfig; import com.hedera.node.config.types.StreamMode; import com.hedera.pbj.runtime.io.buffer.Bytes; +import com.swirlds.platform.components.transaction.system.ScopedSystemTransaction; import com.swirlds.platform.system.InitTrigger; import com.swirlds.platform.system.Round; import com.swirlds.platform.system.transaction.ConsensusTransaction; @@ -109,6 +113,7 @@ import java.util.ArrayList; import java.util.LinkedList; import java.util.List; +import java.util.function.Consumer; import javax.inject.Inject; import javax.inject.Singleton; import org.apache.logging.log4j.LogManager; @@ -215,8 +220,12 @@ public HandleWorkflow( * * @param state the writable {@link State} that this round will work on * @param round the next {@link Round} that needs to be processed + * @param stateSignatureTxnCallback A callback to be called when encountering a {@link StateSignatureTransaction} */ - public void handleRound(@NonNull final State state, @NonNull final Round round) { + public void handleRound( + @NonNull final State state, + @NonNull final Round round, + @NonNull final Consumer> stateSignatureTxnCallback) { logStartRound(round); cacheWarmer.warm(state, round); if (streamMode != RECORDS) { @@ -234,7 +243,7 @@ public void handleRound(@NonNull final State state, @NonNull final Round round) } recordCache.resetRoundReceipts(); try { - handleEvents(state, round); + handleEvents(state, round, stateSignatureTxnCallback); } finally { // Even if there is an exception somewhere, we need to commit the receipts of any handled transactions // to the state so these transactions cannot be replayed in future rounds @@ -248,8 +257,12 @@ public void handleRound(@NonNull final State state, @NonNull final Round round) * * @param state the state to apply the effects to * @param round the round to apply the effects of + * @param stateSignatureTxnCallback A callback to be called when encountering a {@link StateSignatureTransaction} */ - private void handleEvents(@NonNull final State state, @NonNull final Round round) { + private void handleEvents( + @NonNull final State state, + @NonNull final Round round, + @NonNull final Consumer> stateSignatureTxnCallback) { boolean userTransactionsHandled = false; for (final var event : round) { if (streamMode != RECORDS) { @@ -275,6 +288,13 @@ private void handleEvents(@NonNull final State state, @NonNull final Round round } continue; } + + final Consumer simplifiedStateSignatureTxnCallback = txn -> { + final var scopedTxn = + new ScopedSystemTransaction<>(event.getCreatorId(), event.getSoftwareVersion(), txn); + stateSignatureTxnCallback.accept(scopedTxn); + }; + // log start of event to transaction state log logStartEvent(event, creator); // handle each transaction of the event @@ -283,8 +303,12 @@ private void handleEvents(@NonNull final State state, @NonNull final Round round try { // skip system transactions if (!platformTxn.isSystem()) { - userTransactionsHandled = true; - handlePlatformTransaction(state, creator, platformTxn, event.getSoftwareVersion()); + userTransactionsHandled |= handlePlatformTransaction( + state, + creator, + platformTxn, + event.getSoftwareVersion(), + simplifiedStateSignatureTxnCallback); } } catch (final Exception e) { logger.fatal( @@ -316,14 +340,21 @@ private void handleEvents(@NonNull final State state, @NonNull final Round round * @param creator the {@link NodeInfo} of the creator of the transaction * @param txn the {@link ConsensusTransaction} to be handled * @param txnVersion the software version for the event containing the transaction + * @return {@code true} if the transaction was a user transaction, {@code false} if a system transaction */ - private void handlePlatformTransaction( + private boolean handlePlatformTransaction( @NonNull final State state, @NonNull final NodeInfo creator, @NonNull final ConsensusTransaction txn, - @NonNull final SemanticVersion txnVersion) { + @NonNull final SemanticVersion txnVersion, + @NonNull final Consumer stateSignatureTxnCallback) { final var handleStart = System.nanoTime(); + // Temporary check until we can deprecate StateSignatureTransaction + if (stateSignatureTransactionEncountered(txn, stateSignatureTxnCallback)) { + return false; + } + // Always use platform-assigned time for user transaction, c.f. https://hips.hedera.com/hip/hip-993 final var consensusNow = txn.getConsensusTimestamp(); var type = ORDINARY_TRANSACTION; @@ -380,6 +411,19 @@ private void handlePlatformTransaction( blockStreamManager.setLastIntervalProcessTime(userTxn.consensusNow()); } } + return true; + } + + private boolean stateSignatureTransactionEncountered( + @NonNull final ConsensusTransaction txn, + @NonNull final Consumer stateSignatureTxnCallback) { + if (txn.getMetadata() instanceof PreHandleResult preHandleResult + && preHandleResult.txInfo() != null + && preHandleResult.txInfo().functionality() == HederaFunctionality.STATE_SIGNATURE_TRANSACTION) { + stateSignatureTxnCallback.accept(preHandleResult.txInfo().txBody().stateSignatureTransactionOrThrow()); + return true; + } + return false; } /** diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/prehandle/PreHandleResult.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/prehandle/PreHandleResult.java index 90f61c7131ee..d459c5ef3132 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/prehandle/PreHandleResult.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/prehandle/PreHandleResult.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. @@ -236,4 +236,13 @@ public static PreHandleResult preHandleFailure( null, UNKNOWN_VERSION); } + + /** + * Creates a new {@link PreHandleResult} when encountering a {@link com.hedera.hapi.platform.event.StateSignatureTransaction}. + */ + @NonNull + public static PreHandleResult stateSignatureTransactionEncountered(@NonNull final TransactionInfo txInfo) { + return new PreHandleResult( + null, null, Status.SO_FAR_SO_GOOD, UNKNOWN, txInfo, null, null, null, null, null, UNKNOWN_VERSION); + } } diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/prehandle/PreHandleWorkflow.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/prehandle/PreHandleWorkflow.java index a416328dd97e..c4736a045184 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/prehandle/PreHandleWorkflow.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/prehandle/PreHandleWorkflow.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. @@ -17,6 +17,7 @@ package com.hedera.node.app.workflows.prehandle; import com.hedera.hapi.node.base.AccountID; +import com.hedera.hapi.platform.event.StateSignatureTransaction; import com.hedera.node.app.service.token.ReadableAccountStore; import com.hedera.node.app.store.ReadableStoreFactory; import com.swirlds.platform.system.events.Event; @@ -25,6 +26,7 @@ import com.swirlds.state.lifecycle.info.NodeInfo; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; +import java.util.function.Consumer; import java.util.stream.Stream; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -39,12 +41,14 @@ public interface PreHandleWorkflow { * @param readableStoreFactory the {@link ReadableStoreFactory} that is used for looking up stores * @param creator The {@link AccountID} of the node that created these transactions * @param transactions An {@link Stream} over all transactions to pre-handle + * @param stateSignatureTxnCallback A callback to be called when encountering a {@link StateSignatureTransaction} * @throws NullPointerException if one of the arguments is {@code null} */ void preHandle( @NonNull final ReadableStoreFactory readableStoreFactory, @NonNull final AccountID creator, - @NonNull final Stream transactions); + @NonNull final Stream transactions, + @NonNull final Consumer stateSignatureTxnCallback); /** * A convenience method to start the pre-handle transaction workflow for a single @@ -54,14 +58,16 @@ void preHandle( * @param storeFactory The {@link ReadableStoreFactory} based on the current state * @param accountStore The {@link ReadableAccountStore} based on the current state * @param platformTx The {@link Transaction} to pre-handle + * @param stateSignatureTxnCallback A callback to be called when encountering a {@link StateSignatureTransaction} * @return The {@link PreHandleResult} of running pre-handle */ default @NonNull PreHandleResult preHandleTransaction( @NonNull AccountID creator, @NonNull ReadableStoreFactory storeFactory, @NonNull ReadableAccountStore accountStore, - @NonNull Transaction platformTx) { - return preHandleTransaction(creator, storeFactory, accountStore, platformTx, null); + @NonNull Transaction platformTx, + @NonNull Consumer stateSignatureTxnCallback) { + return preHandleTransaction(creator, storeFactory, accountStore, platformTx, null, stateSignatureTxnCallback); } /** @@ -74,6 +80,7 @@ void preHandle( * @param accountStore The {@link ReadableAccountStore} based on the current state * @param platformTx The {@link Transaction} to pre-handle * @param maybeReusableResult The result of a previous call to the same method that may, + * @param stateSignatureTxnCallback A callback to be called when encountering a {@link StateSignatureTransaction} * depending on changes in state, be reusable for this call * @return The {@link PreHandleResult} of running pre-handle */ @@ -83,7 +90,8 @@ PreHandleResult preHandleTransaction( @NonNull ReadableStoreFactory storeFactory, @NonNull ReadableAccountStore accountStore, @NonNull Transaction platformTx, - @Nullable PreHandleResult maybeReusableResult); + @Nullable PreHandleResult maybeReusableResult, + @NonNull Consumer stateSignatureTxnCallback); /** * This method gets all the verification data for the current transaction. If pre-handle was previously ran @@ -100,7 +108,7 @@ PreHandleResult preHandleTransaction( default PreHandleResult getCurrentPreHandleResult( @NonNull final NodeInfo creator, @NonNull final ConsensusTransaction platformTxn, - final ReadableStoreFactory storeFactory) { + @NonNull final ReadableStoreFactory storeFactory) { final var metadata = platformTxn.getMetadata(); final PreHandleResult previousResult; if (metadata instanceof PreHandleResult result) { @@ -122,6 +130,7 @@ default PreHandleResult getCurrentPreHandleResult( storeFactory, storeFactory.getStore(ReadableAccountStore.class), platformTxn, - previousResult); + previousResult, + txns -> {}); } } diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/prehandle/PreHandleWorkflowImpl.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/prehandle/PreHandleWorkflowImpl.java index 91661fc6af5a..4ea6daa459d3 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/prehandle/PreHandleWorkflowImpl.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/prehandle/PreHandleWorkflowImpl.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. @@ -28,10 +28,12 @@ import static java.util.Objects.requireNonNull; import com.hedera.hapi.node.base.AccountID; +import com.hedera.hapi.node.base.HederaFunctionality; import com.hedera.hapi.node.base.Key; import com.hedera.hapi.node.base.SignaturePair; import com.hedera.hapi.node.state.token.Account; import com.hedera.hapi.node.transaction.TransactionBody; +import com.hedera.hapi.platform.event.StateSignatureTransaction; import com.hedera.node.app.service.token.ReadableAccountStore; import com.hedera.node.app.signature.ExpandedSignaturePair; import com.hedera.node.app.signature.SignatureExpander; @@ -54,6 +56,7 @@ import java.util.LinkedHashSet; import java.util.Map; import java.util.Set; +import java.util.function.Consumer; import java.util.stream.Stream; import javax.inject.Inject; import javax.inject.Singleton; @@ -128,11 +131,13 @@ public PreHandleWorkflowImpl( public void preHandle( @NonNull final ReadableStoreFactory readableStoreFactory, @NonNull final AccountID creator, - @NonNull final Stream transactions) { + @NonNull final Stream transactions, + @NonNull final Consumer stateSignatureTxnCallback) { requireNonNull(readableStoreFactory); requireNonNull(creator); requireNonNull(transactions); + requireNonNull(stateSignatureTxnCallback); // Used for looking up payer account information. final var accountStore = readableStoreFactory.getStore(ReadableAccountStore.class); @@ -141,13 +146,13 @@ public void preHandle( transactions.parallel().forEach(tx -> { if (tx.isSystem()) return; try { - tx.setMetadata(preHandleTransaction(creator, readableStoreFactory, accountStore, tx)); + tx.setMetadata(preHandleTransaction( + creator, readableStoreFactory, accountStore, tx, stateSignatureTxnCallback)); } catch (final Exception unexpectedException) { // If some random exception happened, then we should not charge the node for it. Instead, // we will just record the exception and try again during handle. Then if we fail again // at handle, then we will throw away the transaction (hopefully, deterministically!) - logger.error( - "Possibly CATASTROPHIC failure while running the pre-handle workflow", unexpectedException); + logger.error("Unexpected Exception while running the pre-handle workflow", unexpectedException); tx.setMetadata(unknownFailure()); } }); @@ -163,7 +168,8 @@ public PreHandleResult preHandleTransaction( @NonNull final ReadableStoreFactory storeFactory, @NonNull final ReadableAccountStore accountStore, @NonNull final Transaction platformTx, - @Nullable PreHandleResult previousResult) { + @Nullable PreHandleResult previousResult, + @NonNull final Consumer stateSignatureTransactionCallback) { // 0. Ignore the previous result if it was computed using different node configuration if (!wasComputedWithCurrentNodeConfiguration(previousResult)) { previousResult = null; @@ -181,6 +187,12 @@ public PreHandleResult preHandleTransaction( // In particular, a null transaction info means we already know the transaction's final failure status return previousResult; } + + if (txInfo.functionality() == HederaFunctionality.STATE_SIGNATURE_TRANSACTION) { + stateSignatureTransactionCallback.accept(txInfo.txBody().stateSignatureTransaction()); + return PreHandleResult.stateSignatureTransactionEncountered(txInfo); + } + // But we still re-check for node diligence failures transactionChecker.checkParsed(txInfo); // The transaction account ID MUST have matched the creator! diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/state/StateLifecyclesImplTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/state/StateLifecyclesImplTest.java index 2c458199f422..27890d59df1d 100644 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/state/StateLifecyclesImplTest.java +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/state/StateLifecyclesImplTest.java @@ -20,8 +20,10 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; +import com.hedera.hapi.platform.event.StateSignatureTransaction; import com.hedera.node.app.Hedera; import com.swirlds.common.context.PlatformContext; +import com.swirlds.platform.components.transaction.system.ScopedSystemTransaction; import com.swirlds.platform.system.InitTrigger; import com.swirlds.platform.system.Platform; import com.swirlds.platform.system.Round; @@ -29,6 +31,7 @@ import com.swirlds.platform.system.events.Event; import com.swirlds.platform.test.fixtures.state.MerkleTestBase; import com.swirlds.state.merkle.MerkleStateRoot; +import java.util.function.Consumer; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -64,16 +67,18 @@ void setUp() { @Test void delegatesOnPreHandle() { - subject.onPreHandle(event, merkleStateRoot); + final Consumer> callback = txns -> {}; + subject.onPreHandle(event, merkleStateRoot, callback); - verify(hedera).onPreHandle(event, merkleStateRoot); + verify(hedera).onPreHandle(event, merkleStateRoot, callback); } @Test void delegatesOnHandleConsensusRound() { - subject.onHandleConsensusRound(round, merkleStateRoot); + final Consumer> callback = txns -> {}; + subject.onHandleConsensusRound(round, merkleStateRoot, callback); - verify(hedera).onHandleConsensusRound(round, merkleStateRoot); + verify(hedera).onHandleConsensusRound(round, merkleStateRoot, callback); } @Test diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/HandleWorkflowTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/HandleWorkflowTest.java index 94153992bb85..e003b8c686f4 100644 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/HandleWorkflowTest.java +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/HandleWorkflowTest.java @@ -167,7 +167,7 @@ void onlySkipsEventWithMissingCreator() { givenSubjectWith(RECORDS, emptyList()); - subject.handleRound(state, round); + subject.handleRound(state, round, txns -> {}); verify(eventFromPresentCreator).consensusTransactionIterator(); verify(recordCache).resetRoundReceipts(); @@ -184,7 +184,7 @@ void writesEachMigrationStateChangeWithBlockTimestamp() { givenSubjectWith(BOTH, builders); given(blockStreamManager.blockTimestamp()).willReturn(BLOCK_TIME); - subject.handleRound(state, round); + subject.handleRound(state, round, txns -> {}); builders.forEach(builder -> verify(blockStreamManager) .writeItem(BlockItem.newBuilder() diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/prehandle/PreHandleWorkflowImplTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/prehandle/PreHandleWorkflowImplTest.java index d526155dbb17..cf3fc48d8131 100644 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/prehandle/PreHandleWorkflowImplTest.java +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/prehandle/PreHandleWorkflowImplTest.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. @@ -68,7 +68,6 @@ import java.util.Collections; import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.Set; import java.util.concurrent.Future; import java.util.stream.Stream; @@ -196,29 +195,14 @@ void nullPreHandleArgsTest() { final List list = List.of(createAppPayloadWrapper(new byte[10])); final var transactions = list.stream(); final var creator = NODE_1.nodeAccountID(); - assertThatThrownBy(() -> workflow.preHandle(null, creator, transactions)) + assertThatThrownBy(() -> workflow.preHandle(null, creator, transactions, txns -> {})) .isInstanceOf(NullPointerException.class); - assertThatThrownBy(() -> workflow.preHandle(storeFactory, null, transactions)) + assertThatThrownBy(() -> workflow.preHandle(storeFactory, null, transactions, txns -> {})) .isInstanceOf(NullPointerException.class); - assertThatThrownBy(() -> workflow.preHandle(storeFactory, creator, null)) + assertThatThrownBy(() -> workflow.preHandle(storeFactory, creator, null, txns -> {})) .isInstanceOf(NullPointerException.class); } - /** - * We do not currently handle any platform transactions that are marked as system transactions. This test ensures - * that if we send any system transactions, they are ignored. - */ - @Test - @DisplayName("Pre-handle skips system transactions") - void preHandleSkipsSystemTransactionsTest() { - final var platformTx = createAppPayloadWrapper(new byte[10]); - final List list = List.of(platformTx); - final var transactions = list.stream(); - final var creator = NODE_1.nodeAccountID(); - workflow.preHandle(storeFactory, creator, transactions); - assertThat(Optional.ofNullable(platformTx.getMetadata())).isEmpty(); - } - /** * This suite of tests verifies that should we encounter unexpected failures in our code, we will still behave in a * safe and consistent way. @@ -256,7 +240,7 @@ void timeoutExceptionDueToRandomErrorLeadsToUnknownFailureResponseTest() throws .when(dispatcher) .dispatchPreHandle(any()); - workflow.preHandle(storeFactory, creator, transactions); + workflow.preHandle(storeFactory, creator, transactions, txns -> {}); final PreHandleResult result = platformTx.getMetadata(); assertThat(result.responseCode()).isEqualTo(UNKNOWN); assertThat(result.status()).isEqualTo(UNKNOWN_FAILURE); @@ -295,7 +279,7 @@ void preHandleBadBytes() throws PreCheckException { .thenThrow(new PreCheckException(INVALID_TRANSACTION)); // When we try to pre-handle the transaction - workflow.preHandle(storeFactory, NODE_1.nodeAccountID(), Stream.of(platformTx)); + workflow.preHandle(storeFactory, NODE_1.nodeAccountID(), Stream.of(platformTx), txns -> {}); // Then we get a failure with INVALID_TRANSACTION final PreHandleResult result = platformTx.getMetadata(); @@ -316,7 +300,7 @@ void preHandleFailedSyntacticCheckWithUnknownException() throws PreCheckExceptio when(transactionChecker.parseAndCheck(any(Bytes.class))).thenThrow(new RuntimeException("Random")); // When we pre-handle the transaction - workflow.preHandle(storeFactory, NODE_1.nodeAccountID(), Stream.of(platformTx)); + workflow.preHandle(storeFactory, NODE_1.nodeAccountID(), Stream.of(platformTx), txns -> {}); // The throwable is caught, and we get an UNKNOWN status code final PreHandleResult result = platformTx.getMetadata(); @@ -342,7 +326,7 @@ void preHandlePayerAccountNotFound() throws PreCheckException { when(transactionChecker.parseAndCheck(any(Bytes.class))).thenReturn(txInfo); // When we pre-handle the transaction - workflow.preHandle(storeFactory, NODE_1.nodeAccountID(), Stream.of(platformTx)); + workflow.preHandle(storeFactory, NODE_1.nodeAccountID(), Stream.of(platformTx), txns -> {}); // Then the transaction fails and the node is the payer final PreHandleResult result1 = platformTx.getMetadata(); @@ -369,7 +353,7 @@ void preHandlePayerAccountDeleted() throws PreCheckException { when(transactionChecker.parseAndCheck(any(Bytes.class))).thenReturn(txInfo); // When we pre-handle the transaction - workflow.preHandle(storeFactory, NODE_1.nodeAccountID(), Stream.of(platformTx)); + workflow.preHandle(storeFactory, NODE_1.nodeAccountID(), Stream.of(platformTx), txns -> {}); // Then the transaction fails and the node is the payer final PreHandleResult result1 = platformTx.getMetadata(); @@ -399,7 +383,7 @@ void payerSignatureInvalid(@Mock SignatureVerificationFuture sigFuture) throws E when(sigFuture.get(anyLong(), any())).thenReturn(new SignatureVerificationImpl(key, null, false)); // When we pre-handle the transaction - workflow.preHandle(storeFactory, NODE_1.nodeAccountID(), Stream.of(platformTx)); + workflow.preHandle(storeFactory, NODE_1.nodeAccountID(), Stream.of(platformTx), txns -> {}); // Then the transaction still succeeds (since the payer signature check is async) final PreHandleResult result1 = platformTx.getMetadata(); @@ -432,7 +416,7 @@ void preHandleCreatorAccountNotTxNodeAccount() throws PreCheckException { when(transactionChecker.parseAndCheck(any(Bytes.class))).thenReturn(txInfo); // When we pre-handle the transaction - workflow.preHandle(storeFactory, NODE_1.nodeAccountID(), Stream.of(platformTx)); + workflow.preHandle(storeFactory, NODE_1.nodeAccountID(), Stream.of(platformTx), txns -> {}); // Then the transaction fails and the node is the payer final PreHandleResult result1 = platformTx.getMetadata(); @@ -459,7 +443,8 @@ void reusesUnparseableTransactionResult() { storeFactory, storeFactory.getStore(ReadableAccountStore.class), createAppPayloadWrapper(new byte[2]), - previousResult); + previousResult, + txns -> {}); // Then the entire result is re-used assertThat(result).isSameAs(previousResult); @@ -493,7 +478,7 @@ void preHandleSemanticChecksFail(@Mock SignatureVerificationFuture sigFuture) th .dispatchPreHandle(any()); // When we pre-handle the transaction - workflow.preHandle(storeFactory, NODE_1.nodeAccountID(), Stream.of(platformTx)); + workflow.preHandle(storeFactory, NODE_1.nodeAccountID(), Stream.of(platformTx), txns -> {}); // Then the transaction failure is INVALID_ACCOUNT_AMOUNTS and the payer is the payer final PreHandleResult result = platformTx.getMetadata(); @@ -518,7 +503,7 @@ void preHandleWarmingFails() throws PreCheckException { doThrow(new RuntimeException()).when(dispatcher).dispatchPreHandle(any()); // When we pre-handle the transaction - workflow.preHandle(storeFactory, NODE_1.nodeAccountID(), Stream.of(platformTx)); + workflow.preHandle(storeFactory, NODE_1.nodeAccountID(), Stream.of(platformTx), txns -> {}); // Then the transaction failure is UNKNOWN and the payer is null. There can be no payer in this case. final PreHandleResult result = platformTx.getMetadata(); @@ -560,7 +545,7 @@ void nonPayerSignatureInvalid( .dispatchPreHandle(any()); // When we pre-handle the transaction - workflow.preHandle(storeFactory, NODE_1.nodeAccountID(), Stream.of(platformTx)); + workflow.preHandle(storeFactory, NODE_1.nodeAccountID(), Stream.of(platformTx), txns -> {}); // Then the transaction succeeds final PreHandleResult result = platformTx.getMetadata(); @@ -601,7 +586,7 @@ void happyPath(@Mock SignatureVerificationFuture sigFuture) throws Exception { when(signatureVerifier.verify(any(), any())).thenReturn(Map.of(payerKey, sigFuture)); // When we pre-handle the transaction - workflow.preHandle(storeFactory, NODE_1.nodeAccountID(), Stream.of(platformTx)); + workflow.preHandle(storeFactory, NODE_1.nodeAccountID(), Stream.of(platformTx), txns -> {}); // Then the transaction pre-handle succeeds! final PreHandleResult result = platformTx.getMetadata(); @@ -651,7 +636,8 @@ void happyPathWithoutReuse(@Mock SignatureVerificationFuture sigFuture) throws E storeFactory, storeFactory.getStore(ReadableAccountStore.class), platformTx, - previousResult); + previousResult, + txns -> {}); // Then the transaction pre-handle succeeds! assertThat(result.status()).isEqualTo(SO_FAR_SO_GOOD); @@ -697,7 +683,8 @@ void happyPathWithFullReuseOfPreviousResult(@Mock SignatureVerificationFuture si storeFactory, storeFactory.getStore(ReadableAccountStore.class), platformTx, - previousResult); + previousResult, + txns -> {}); // Then the transaction pre-handle succeeds! assertThat(result.status()).isEqualTo(SO_FAR_SO_GOOD); @@ -731,7 +718,7 @@ void happyPathHollowAccountAsPayer(@Mock SignatureVerificationFuture sigFuture) .thenReturn(new SignatureVerificationImpl(finalizedKey, hollowAccountAlias, true)); // When we pre-handle the transaction - workflow.preHandle(storeFactory, NODE_1.nodeAccountID(), Stream.of(platformTx)); + workflow.preHandle(storeFactory, NODE_1.nodeAccountID(), Stream.of(platformTx), txns -> {}); // Then the transaction pre-handle succeeds! final PreHandleResult result = platformTx.getMetadata(); @@ -781,7 +768,7 @@ void happyPathHollowAccountsNonPayer( .dispatchPreHandle(any()); // When we pre-handle the transaction - workflow.preHandle(storeFactory, NODE_1.nodeAccountID(), Stream.of(platformTx)); + workflow.preHandle(storeFactory, NODE_1.nodeAccountID(), Stream.of(platformTx), txns -> {}); // Then the transaction pre-handle succeeds! final PreHandleResult result = platformTx.getMetadata(); diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/RepeatableReason.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/RepeatableReason.java index 462376b2943c..eded906503a6 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/RepeatableReason.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/RepeatableReason.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. @@ -46,4 +46,8 @@ public enum RepeatableReason { * other tests if they expect the default throttles. */ THROTTLE_OVERRIDES, + /** + * The test uses the state signature transaction callback. + */ + USES_STATE_SIGNATURE_TRANSACTION_CALLBACK, } diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/embedded/ConcurrentEmbeddedHedera.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/embedded/ConcurrentEmbeddedHedera.java index 5e9458a502ff..ccbf6a5e8b59 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/embedded/ConcurrentEmbeddedHedera.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/embedded/ConcurrentEmbeddedHedera.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. @@ -21,6 +21,7 @@ import static java.util.concurrent.TimeUnit.MILLISECONDS; import com.hedera.hapi.node.base.SemanticVersion; +import com.hedera.hapi.platform.event.StateSignatureTransaction; import com.hedera.pbj.runtime.io.buffer.BufferedData; import com.hedera.pbj.runtime.io.buffer.Bytes; import com.hedera.services.bdd.junit.hedera.embedded.fakes.AbstractFakePlatform; @@ -30,6 +31,7 @@ import com.hederahashgraph.api.proto.java.AccountID; import com.hederahashgraph.api.proto.java.Transaction; import com.hederahashgraph.api.proto.java.TransactionResponse; +import com.swirlds.platform.components.transaction.system.ScopedSystemTransaction; import com.swirlds.platform.system.Platform; import com.swirlds.platform.system.events.ConsensusEvent; import edu.umd.cs.findbugs.annotations.NonNull; @@ -40,6 +42,7 @@ import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ScheduledExecutorService; +import java.util.function.Consumer; import java.util.stream.IntStream; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -74,6 +77,15 @@ public void tick(@NonNull final Duration duration) { } } + @Override + public TransactionResponse submit( + @NonNull Transaction transaction, + @NonNull AccountID nodeAccountId, + @NonNull Consumer> preHandleCallback, + @NonNull Consumer> handleCallback) { + throw new UnsupportedOperationException("ConcurrentEmbeddedHedera does not support state signature callbacks"); + } + @Override public TransactionResponse submit( @NonNull final Transaction transaction, @@ -160,7 +172,7 @@ private void handleTransactions() { }) .toList(); final var round = new FakeRound(roundNo.getAndIncrement(), requireNonNull(roster), consensusEvents); - hedera.handleWorkflow().handleRound(state, round); + hedera.handleWorkflow().handleRound(state, round, txns -> {}); hedera.onSealConsensusRound(round, state); notifyStateHashed(round.getRoundNum()); prehandledEvents.clear(); @@ -168,7 +180,7 @@ private void handleTransactions() { // Now drain all events that will go in the next round and pre-handle them final List newEvents = new ArrayList<>(); queue.drainTo(newEvents); - newEvents.forEach(event -> hedera.onPreHandle(event, state)); + newEvents.forEach(event -> hedera.onPreHandle(event, state, txns -> {})); prehandledEvents.addAll(newEvents); } catch (Throwable t) { log.error("Error handling transactions", t); diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/embedded/EmbeddedHedera.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/embedded/EmbeddedHedera.java index 2ceef5b46fe5..4e5aba15fdd6 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/embedded/EmbeddedHedera.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/embedded/EmbeddedHedera.java @@ -17,6 +17,7 @@ package com.hedera.services.bdd.junit.hedera.embedded; import com.hedera.hapi.node.state.roster.Roster; +import com.hedera.hapi.platform.event.StateSignatureTransaction; import com.hedera.node.app.Hedera; import com.hedera.node.app.fixtures.state.FakeState; import com.hederahashgraph.api.proto.java.AccountID; @@ -26,10 +27,12 @@ import com.hederahashgraph.api.proto.java.Transaction; import com.hederahashgraph.api.proto.java.TransactionID; import com.hederahashgraph.api.proto.java.TransactionResponse; +import com.swirlds.platform.components.transaction.system.ScopedSystemTransaction; import com.swirlds.platform.system.SoftwareVersion; import edu.umd.cs.findbugs.annotations.NonNull; import java.time.Duration; import java.time.Instant; +import java.util.function.Consumer; public interface EmbeddedHedera { /** @@ -118,6 +121,21 @@ default TransactionResponse submit(@NonNull Transaction transaction, @NonNull Ac TransactionResponse submit( @NonNull Transaction transaction, @NonNull AccountID nodeAccountId, @NonNull SyntheticVersion version); + /** + * Submits a transaction to the embedded node. + * + * @param transaction the transaction to submit + * @param nodeAccountId the account ID of the node to submit the transaction to + * @param preHandleCallback the callback to call during preHandle when a {@link StateSignatureTransaction} is encountered + * @param handleCallback the callback to call during preHandle when a {@link StateSignatureTransaction} is encountered + * @return the response to the transaction + */ + TransactionResponse submit( + @NonNull Transaction transaction, + @NonNull AccountID nodeAccountId, + @NonNull Consumer> preHandleCallback, + @NonNull Consumer> handleCallback); + /** * Sends a query to the embedded node. * diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/embedded/RepeatableEmbeddedHedera.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/embedded/RepeatableEmbeddedHedera.java index 3e1ef35aa4bd..25f684921990 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/embedded/RepeatableEmbeddedHedera.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/embedded/RepeatableEmbeddedHedera.java @@ -21,6 +21,7 @@ import static java.util.Objects.requireNonNull; import com.hedera.hapi.node.base.SemanticVersion; +import com.hedera.hapi.platform.event.StateSignatureTransaction; import com.hedera.pbj.runtime.io.buffer.BufferedData; import com.hedera.pbj.runtime.io.buffer.Bytes; import com.hedera.services.bdd.junit.hedera.embedded.fakes.AbstractFakePlatform; @@ -33,6 +34,7 @@ import com.hederahashgraph.api.proto.java.TransactionResponse; import com.swirlds.base.test.fixtures.time.FakeTime; import com.swirlds.common.platform.NodeId; +import com.swirlds.platform.components.transaction.system.ScopedSystemTransaction; import com.swirlds.platform.system.Platform; import com.swirlds.platform.system.Round; import com.swirlds.platform.system.events.ConsensusEvent; @@ -43,6 +45,7 @@ import java.util.List; import java.util.Queue; import java.util.concurrent.ScheduledExecutorService; +import java.util.function.Consumer; /** * An embedded Hedera node that handles transactions synchronously on ingest and thus @@ -59,6 +62,11 @@ public class RepeatableEmbeddedHedera extends AbstractEmbeddedHedera implements private final SynchronousFakePlatform platform; private final Queue pendingNodeSubmissions = new ArrayDeque<>(); + private static final Consumer> NO_OP_CALLBACK = ignore -> {}; + private Consumer> preHandleStateSignatureCallback = + NO_OP_CALLBACK; + private Consumer> handleStateSignatureCallback = NO_OP_CALLBACK; + // The amount of consensus time that will be simulated to elapse before the next transaction---note // that in repeatable mode, every transaction gets its own event, and each event gets its own round private Duration roundDuration = DEFAULT_ROUND_DURATION; @@ -83,6 +91,20 @@ public void tick(@NonNull Duration duration) { time.tick(duration); } + @Override + public TransactionResponse submit( + @NonNull Transaction transaction, + @NonNull AccountID nodeAccountId, + @NonNull Consumer> preHandleCallback, + @NonNull Consumer> handleCallback) { + this.preHandleStateSignatureCallback = preHandleCallback; + this.handleStateSignatureCallback = handleCallback; + final var response = submit(transaction, nodeAccountId); + this.preHandleStateSignatureCallback = NO_OP_CALLBACK; + this.handleStateSignatureCallback = NO_OP_CALLBACK; + return response; + } + @Override public TransactionResponse submit( @NonNull final Transaction transaction, @@ -152,10 +174,10 @@ public void setRoundDuration(@NonNull final Duration roundDuration) { * Executes the transaction in the last-created event within its own round. */ private void handleNextRound() { - hedera.onPreHandle(platform.lastCreatedEvent, state); + hedera.onPreHandle(platform.lastCreatedEvent, state, preHandleStateSignatureCallback); final var round = platform.nextConsensusRound(); // Handle each transaction in own round - hedera.handleWorkflow().handleRound(state, round); + hedera.handleWorkflow().handleRound(state, round, handleStateSignatureCallback); hedera.onSealConsensusRound(round, state); notifyStateHashed(round.getRoundNum()); } diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/HapiTxnOp.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/HapiTxnOp.java index 9a2823ad58ca..388c7e9989a5 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/HapiTxnOp.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/HapiTxnOp.java @@ -1,4 +1,19 @@ -// SPDX-License-Identifier: Apache-2.0 +/* + * Copyright (C) 2025 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.hedera.services.bdd.spec.transactions; import static com.hedera.services.bdd.spec.TargetNetworkType.EMBEDDED_NETWORK; @@ -39,6 +54,7 @@ import com.hedera.services.bdd.spec.keys.ControlForKey; import com.hedera.services.bdd.spec.keys.SigMapGenerator; import com.hedera.services.bdd.spec.utilops.mod.BodyMutation; +import com.hedera.services.bdd.spec.verification.Condition; import com.hederahashgraph.api.proto.java.AccountID; import com.hederahashgraph.api.proto.java.HederaFunctionality; import com.hederahashgraph.api.proto.java.Key; @@ -60,6 +76,7 @@ import java.util.List; import java.util.Optional; import java.util.OptionalDouble; +import java.util.function.BooleanSupplier; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; @@ -108,6 +125,21 @@ public abstract class HapiTxnOp> extends HapiSpecOperatio /** if response code in the set then allow to resubmit transaction */ protected Optional> retryPrechecks = Optional.empty(); + protected List conditions = new ArrayList<>(); + + public T satisfies(@NonNull final Condition condition) { + conditions.add(condition); + return self(); + } + + public T satisfies(@NonNull final BooleanSupplier condition, @NonNull final Supplier errorMessage) { + return satisfies(new Condition(condition, errorMessage)); + } + + public T satisfies(@NonNull final BooleanSupplier condition, @NonNull final String errorMessage) { + return satisfies(new Condition(condition, () -> errorMessage)); + } + /** * A strategy for submitting a transaction of the given function and type to a network node with the given id. */ @@ -287,6 +319,12 @@ && isWithInRetryLimit(retryCount)) { spec.offerFinisher(new DelegatingOpFinisher(this)); } + for (final var condition : conditions) { + if (!condition.condition().getAsBoolean()) { + throw new HapiTxnCheckStateException("Condition failed: " + condition.errorMessage()); + } + } + return !deferStatusResolution; } diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/TxnFactory.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/TxnFactory.java index a936867ee309..a7900ead9a39 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/TxnFactory.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/TxnFactory.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020-2024 Hedera Hashgraph, LLC + * Copyright (C) 2020-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. @@ -22,6 +22,7 @@ import com.google.protobuf.ByteString; import com.google.protobuf.Message; +import com.hedera.hapi.platform.event.legacy.StateSignatureTransaction; import com.hedera.services.bdd.spec.HapiSpec; import com.hedera.services.bdd.spec.HapiSpecSetup; import com.hedera.services.bdd.spec.utilops.mod.BodyMutation; @@ -356,6 +357,10 @@ public Consumer defaultDefFileUpdateTransacti return builder -> {}; } + public Consumer defaultDefStateSignatureTransaction() { + return builder -> {}; + } + /** * Returns a {@link Timestamp} that is the default expiry time for an entity being created * in the given spec context. diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/TxnVerbs.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/TxnVerbs.java index b1dd836bf42e..f7035f217e84 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/TxnVerbs.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/TxnVerbs.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020-2024 Hedera Hashgraph, LLC + * Copyright (C) 2020-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. @@ -75,6 +75,7 @@ import com.hedera.services.bdd.spec.transactions.schedule.HapiScheduleDelete; import com.hedera.services.bdd.spec.transactions.schedule.HapiScheduleSign; import com.hedera.services.bdd.spec.transactions.system.HapiFreeze; +import com.hedera.services.bdd.spec.transactions.system.HapiStateSignature; import com.hedera.services.bdd.spec.transactions.system.HapiSysDelete; import com.hedera.services.bdd.spec.transactions.system.HapiSysUndelete; import com.hedera.services.bdd.spec.transactions.token.HapiTokenAirdrop; @@ -757,6 +758,10 @@ public static HapiFreeze hapiFreeze(final Instant freezeStartTime) { return new HapiFreeze().startingAt(freezeStartTime); } + public static HapiStateSignature hapiStateSignature() { + return new HapiStateSignature(); + } + /* UTIL */ public static HapiUtilPrng hapiPrng() { return new HapiUtilPrng(); diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/system/HapiStateSignature.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/system/HapiStateSignature.java new file mode 100644 index 000000000000..d355a3762f02 --- /dev/null +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/system/HapiStateSignature.java @@ -0,0 +1,48 @@ +/* + * 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.hedera.services.bdd.spec.transactions.system; + +import com.hedera.hapi.platform.event.legacy.StateSignatureTransaction; +import com.hedera.services.bdd.spec.HapiSpec; +import com.hedera.services.bdd.spec.transactions.HapiTxnOp; +import com.hederahashgraph.api.proto.java.HederaFunctionality; +import com.hederahashgraph.api.proto.java.Transaction; +import com.hederahashgraph.api.proto.java.TransactionBody; +import java.util.function.Consumer; + +public class HapiStateSignature extends HapiTxnOp { + @Override + protected HapiStateSignature self() { + return this; + } + + @Override + public HederaFunctionality type() { + return HederaFunctionality.StateSignatureTransaction; + } + + @Override + protected Consumer opBodyDef(HapiSpec spec) throws Throwable { + final var opBody = spec.txns().body(StateSignatureTransaction.class, b -> {}); + return b -> b.setStateSignatureTransaction(opBody); + } + + @Override + protected long feeFor(HapiSpec spec, Transaction txn, int numPayerKeys) throws Throwable { + return 0; + } +} diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/UtilVerbs.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/UtilVerbs.java index e82fdb690cc8..046c0b67e6e0 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/UtilVerbs.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/UtilVerbs.java @@ -1,4 +1,19 @@ -// SPDX-License-Identifier: Apache-2.0 +/* + * Copyright (C) 2025 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.hedera.services.bdd.spec.utilops; import static com.hedera.node.app.hapi.utils.CommonPbjConverters.fromByteString; @@ -82,6 +97,7 @@ import com.hedera.hapi.node.state.addressbook.Node; import com.hedera.hapi.node.state.roster.Roster; import com.hedera.hapi.node.state.token.Account; +import com.hedera.hapi.platform.event.StateSignatureTransaction; import com.hedera.services.bdd.junit.hedera.MarkerFile; import com.hedera.services.bdd.junit.hedera.NodeSelector; import com.hedera.services.bdd.junit.hedera.embedded.EmbeddedNetwork; @@ -179,6 +195,7 @@ import com.hederahashgraph.api.proto.java.TransactionBody; import com.hederahashgraph.api.proto.java.TransactionRecord; import com.swirlds.common.utility.CommonUtils; +import com.swirlds.platform.components.transaction.system.ScopedSystemTransaction; import edu.umd.cs.findbugs.annotations.NonNull; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -515,6 +532,27 @@ public static HapiTxnOp.SubmissionStrategy usingVersion(@NonNull final Synthetic }; } + /** + * Returns a submission strategy that requires an embedded network and given one submits a transaction with + * the given {@link StateSignatureTransaction}-callback. + * + * @param preHandleCallback the callback that is called during preHandle when a {@link StateSignatureTransaction} is encountered + * @param handleCallback the callback that is called when a {@link StateSignatureTransaction} is encountered + * @return the submission strategy + */ + public static HapiTxnOp.SubmissionStrategy usingStateSignatureTransactionCallback( + @NonNull final Consumer> preHandleCallback, + @NonNull final Consumer> handleCallback) { + return (network, transaction, functionality, target, nodeAccountId) -> { + if (!(network instanceof EmbeddedNetwork embeddedNetwork)) { + throw new IllegalArgumentException("Expected an EmbeddedNetwork"); + } + return embeddedNetwork + .embeddedHederaOrThrow() + .submit(transaction, nodeAccountId, preHandleCallback, handleCallback); + }; + } + public static WaitForStatusOp waitForFrozenNetwork(@NonNull final Duration timeout) { return new WaitForStatusOp(NodeSelector.allNodes(), FREEZE_COMPLETE, timeout); } diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/verification/Condition.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/verification/Condition.java new file mode 100644 index 000000000000..633c729c60eb --- /dev/null +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/verification/Condition.java @@ -0,0 +1,23 @@ +/* + * 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.hedera.services.bdd.spec.verification; + +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.function.BooleanSupplier; +import java.util.function.Supplier; + +public record Condition(@NonNull BooleanSupplier condition, @NonNull Supplier errorMessage) {} diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/misc/StateSignatureCallbackSuite.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/misc/StateSignatureCallbackSuite.java new file mode 100644 index 000000000000..ba097c00eba3 --- /dev/null +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/misc/StateSignatureCallbackSuite.java @@ -0,0 +1,85 @@ +/* + * 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.hedera.services.bdd.suites.misc; + +import static com.hedera.services.bdd.junit.RepeatableReason.USES_STATE_SIGNATURE_TRANSACTION_CALLBACK; +import static com.hedera.services.bdd.spec.HapiSpec.hapiTest; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoCreate; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.hapiStateSignature; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.usingStateSignatureTransactionCallback; + +import com.hedera.hapi.platform.event.StateSignatureTransaction; +import com.hedera.services.bdd.junit.RepeatableHapiTest; +import com.swirlds.platform.components.transaction.system.ScopedSystemTransaction; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; +import java.util.stream.Stream; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.DynamicTest; + +public class StateSignatureCallbackSuite { + + @RepeatableHapiTest(USES_STATE_SIGNATURE_TRANSACTION_CALLBACK) + @DisplayName("regular transaction does not call StateSignatureTransaction callbacks") + @Disabled + final Stream doesNotCallStateSignatureCallback() { + final var preHandleCallback = new Callback(); + final var handleCallback = new Callback(); + return hapiTest(cryptoCreate("somebody") + .balance(0L) + .withSubmissionStrategy(usingStateSignatureTransactionCallback(preHandleCallback, handleCallback)) + .satisfies( + () -> preHandleCallback.counter.get() == 0, + "Pre-handle StateSignatureTxnCallback was called but should not") + .satisfies( + () -> handleCallback.counter.get() == 0, + "Handle StateSignatureTxnCallback was called but should not")); + } + + @RepeatableHapiTest(USES_STATE_SIGNATURE_TRANSACTION_CALLBACK) + @DisplayName("StateSignatureTransaction calls StateSignatureTransaction callbacks") + final Stream callsStateSignatureCallback() { + final var preHandleCallback = new Callback(); + final var handleCallback = new Callback(); + return hapiTest(hapiStateSignature() + .withSubmissionStrategy(usingStateSignatureTransactionCallback(preHandleCallback, handleCallback)) + .setNode("0.0.4") + .fireAndForget() + .satisfies( + () -> preHandleCallback.counter.get() == 1, + () -> + "Pre-handle StateSignatureTxnCallback should have been called once, but was called was called " + + preHandleCallback.counter.get() + " times") + .satisfies( + () -> handleCallback.counter.get() == 1, + () -> + "Handle StateSignatureTxnCallback should have been called once, but was called was called " + + handleCallback.counter.get() + " times")); + } + + private static class Callback implements Consumer> { + + private final AtomicInteger counter = new AtomicInteger(); + + @Override + public void accept( + ScopedSystemTransaction stateSignatureTransactionScopedSystemTransaction) { + counter.incrementAndGet(); + } + } +} diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/PlatformMerkleStateRoot.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/PlatformMerkleStateRoot.java index 5aa82f05089f..64d3c18b3980 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/PlatformMerkleStateRoot.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/PlatformMerkleStateRoot.java @@ -152,7 +152,7 @@ public void handleConsensusRound( @NonNull final PlatformStateModifier platformState, @NonNull final Consumer> stateSignatureTransaction) { throwIfImmutable(); - lifecycles.onHandleConsensusRound(round, this); + lifecycles.onHandleConsensusRound(round, this, stateSignatureTransaction); } /** @@ -172,7 +172,7 @@ public void sealConsensusRound(@NonNull final Round round) { public void preHandle( @NonNull final Event event, @NonNull final Consumer> stateSignatureTransaction) { - lifecycles.onPreHandle(event, this); + lifecycles.onPreHandle(event, this, stateSignatureTransaction); } /** diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/StateLifecycles.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/StateLifecycles.java index c672d0f9f1f0..cd3e404ef24d 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/StateLifecycles.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/StateLifecycles.java @@ -16,7 +16,9 @@ package com.swirlds.platform.state; +import com.hedera.hapi.platform.event.StateSignatureTransaction; import com.swirlds.common.context.PlatformContext; +import com.swirlds.platform.components.transaction.system.ScopedSystemTransaction; import com.swirlds.platform.system.InitTrigger; import com.swirlds.platform.system.Platform; import com.swirlds.platform.system.Round; @@ -26,6 +28,7 @@ import com.swirlds.state.State; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; +import java.util.function.Consumer; /** * Implements the major lifecycle events for the state. @@ -40,17 +43,24 @@ public interface StateLifecycles { * * @param event the event that was added * @param state the latest immutable state at the time of the event + * @param stateSignatureTransactionCallback a consumer that will be used for callbacks */ - void onPreHandle(@NonNull Event event, @NonNull State state); + void onPreHandle( + @NonNull Event event, + @NonNull State state, + @NonNull Consumer> stateSignatureTransactionCallback); /** - * Called when a round of events have reached consensus, and are ready to be handled - * by the network. + * Called when a round of events have reached consensus, and are ready to be handled by the network. * * @param round the round that has just reached consensus * @param state the working state of the network + * @param stateSignatureTransactionCallback a consumer that will be used for callbacks */ - void onHandleConsensusRound(@NonNull Round round, @NonNull State state); + void onHandleConsensusRound( + @NonNull Round round, + @NonNull State state, + @NonNull Consumer> stateSignatureTransactionCallback); /** * Called by the platform after it has made all its changes to this state for the given round. diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/PlatformMerkleStateRootTest.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/PlatformMerkleStateRootTest.java index 1407d3476673..116e6c9236b2 100644 --- a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/PlatformMerkleStateRootTest.java +++ b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/PlatformMerkleStateRootTest.java @@ -37,6 +37,7 @@ import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; +import com.hedera.hapi.platform.event.StateSignatureTransaction; import com.hedera.hapi.platform.state.PlatformState; import com.swirlds.base.state.MutabilityException; import com.swirlds.common.context.PlatformContext; @@ -49,6 +50,7 @@ import com.swirlds.common.metrics.noop.NoOpMetrics; import com.swirlds.config.api.ConfigurationBuilder; import com.swirlds.merkle.map.MerkleMap; +import com.swirlds.platform.components.transaction.system.ScopedSystemTransaction; import com.swirlds.platform.state.service.PlatformStateService; import com.swirlds.platform.state.service.schemas.V0540PlatformStateSchema; import com.swirlds.platform.system.InitTrigger; @@ -82,6 +84,7 @@ import java.util.List; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; @@ -108,7 +111,10 @@ public void onSealConsensusRound(@NonNull Round round, @NonNull State state) { } @Override - public void onPreHandle(@NonNull Event event, @NonNull State state) { + public void onPreHandle( + @NonNull Event event, + @NonNull State state, + @NonNull Consumer> stateSignatureTransactions) { onPreHandleCalled.set(true); } @@ -118,7 +124,10 @@ public void onNewRecoveredState(@NonNull State recoveredState) { } @Override - public void onHandleConsensusRound(@NonNull Round round, @NonNull State state) { + public void onHandleConsensusRound( + @NonNull Round round, + @NonNull State state, + @NonNull Consumer> stateSignatureTransactions) { onHandleCalled.set(true); } diff --git a/platform-sdk/swirlds-platform-core/src/testFixtures/java/com/swirlds/platform/test/fixtures/state/FakeStateLifecycles.java b/platform-sdk/swirlds-platform-core/src/testFixtures/java/com/swirlds/platform/test/fixtures/state/FakeStateLifecycles.java index 5d76bec75c0a..3a179535bd9e 100644 --- a/platform-sdk/swirlds-platform-core/src/testFixtures/java/com/swirlds/platform/test/fixtures/state/FakeStateLifecycles.java +++ b/platform-sdk/swirlds-platform-core/src/testFixtures/java/com/swirlds/platform/test/fixtures/state/FakeStateLifecycles.java @@ -21,6 +21,7 @@ import static org.mockito.Mockito.mock; import com.hedera.hapi.block.stream.output.StateChanges; +import com.hedera.hapi.platform.event.StateSignatureTransaction; import com.swirlds.common.RosterStateId; import com.swirlds.common.config.StateCommonConfig; import com.swirlds.common.constructable.ClassConstructorPair; @@ -35,6 +36,7 @@ import com.swirlds.merkledb.MerkleDbDataSourceBuilder; import com.swirlds.merkledb.MerkleDbTableConfig; import com.swirlds.merkledb.config.MerkleDbConfig; +import com.swirlds.platform.components.transaction.system.ScopedSystemTransaction; import com.swirlds.platform.config.AddressBookConfig; import com.swirlds.platform.config.BasicConfig; import com.swirlds.platform.state.PlatformMerkleStateRoot; @@ -70,6 +72,7 @@ import java.util.Collections; import java.util.Comparator; import java.util.List; +import java.util.function.Consumer; public enum FakeStateLifecycles implements StateLifecycles { FAKE_MERKLE_STATE_LIFECYCLES; @@ -205,12 +208,18 @@ public List initRosterState(@NonNull final State state) { } @Override - public void onPreHandle(@NonNull Event event, @NonNull State state) { + public void onPreHandle( + @NonNull Event event, + @NonNull State state, + @NonNull Consumer> stateSignatureTransactionCallback) { // no-op } @Override - public void onHandleConsensusRound(@NonNull Round round, @NonNull State state) { + public void onHandleConsensusRound( + @NonNull Round round, + @NonNull State state, + @NonNull Consumer> stateSignatureTransactionCallback) { // no-op }