From 7627d79c1a0555788081e8f1ac8e2867488b7a5b Mon Sep 17 00:00:00 2001 From: Matt Hess Date: Tue, 28 May 2024 16:14:57 -0500 Subject: [PATCH] fix: Skip transactions older than the software version (#13527) Signed-off-by: Matt Hess Signed-off-by: Bilyana Gospodinova --- .../main/java/com/hedera/node/app/Hedera.java | 1 + .../node/app/HederaInjectionComponent.java | 4 + .../app/workflows/handle/HandleWorkflow.java | 57 +++- .../app/components/IngestComponentTest.java | 1 + .../workflows/handle/HandleWorkflowTest.java | 272 ++++++++++++++++-- 5 files changed, 303 insertions(+), 32 deletions(-) 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 f8935323c425..4f459b87d924 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 @@ -1107,6 +1107,7 @@ private void initializeDagger( // Fully qualified so as to not confuse javadoc daggerApp = com.hedera.node.app.DaggerHederaInjectionComponent.builder() .initTrigger(trigger) + .softwareVersion(version) .configProvider(configProvider) .configProviderImpl(configProvider) .self(SelfNodeInfoImpl.of(nodeAddress, version)) diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/HederaInjectionComponent.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/HederaInjectionComponent.java index 86b869bd1ec2..00da7c4704cd 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/HederaInjectionComponent.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/HederaInjectionComponent.java @@ -59,6 +59,7 @@ import com.swirlds.platform.listeners.StateWriteToDiskCompleteListener; import com.swirlds.platform.system.InitTrigger; import com.swirlds.platform.system.Platform; +import com.swirlds.platform.system.SoftwareVersion; import dagger.BindsInstance; import dagger.Component; import java.nio.charset.Charset; @@ -175,6 +176,9 @@ interface Builder { @BindsInstance Builder genesisRecordsConsensusHook(GenesisRecordsConsensusHook genesisRecordsBuilder); + @BindsInstance + Builder softwareVersion(SoftwareVersion softwareVersion); + HederaInjectionComponent build(); } } 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 23684fe3d640..160ba739e2ae 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,6 +46,7 @@ import static com.hedera.node.app.workflows.prehandle.PreHandleResult.Status.PAYER_UNWILLING_OR_UNABLE_TO_PAY_SERVICE_FEE; import static com.hedera.node.app.workflows.prehandle.PreHandleResult.Status.PRE_HANDLE_FAILURE; import static com.hedera.node.app.workflows.prehandle.PreHandleResult.Status.SO_FAR_SO_GOOD; +import static com.swirlds.platform.system.InitTrigger.EVENT_STREAM_RECOVERY; import static java.util.Collections.emptyMap; import static java.util.Collections.emptySet; import static java.util.Objects.requireNonNull; @@ -121,7 +122,9 @@ import com.hedera.pbj.runtime.io.buffer.Bytes; import com.swirlds.config.api.Configuration; import com.swirlds.platform.state.PlatformState; +import com.swirlds.platform.system.InitTrigger; 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.state.HederaState; @@ -173,6 +176,9 @@ public class HandleWorkflow { private final HandleWorkflowMetrics handleWorkflowMetrics; private final ThrottleServiceManager throttleServiceManager; private final StoreMetricsService storeMetricsService; + private final TransactionChecker transactionChecker; + private final InitTrigger initTrigger; + private final SoftwareVersion softwareVersion; @Inject public HandleWorkflow( @@ -200,7 +206,10 @@ public HandleWorkflow( @NonNull final CacheWarmer cacheWarmer, @NonNull final HandleWorkflowMetrics handleWorkflowMetrics, @NonNull final ThrottleServiceManager throttleServiceManager, - @NonNull final StoreMetricsService storeMetricsService) { + @NonNull final StoreMetricsService storeMetricsService, + @NonNull final TransactionChecker transactionChecker, + @NonNull final InitTrigger initTrigger, + @NonNull final SoftwareVersion softwareVersion) { this.networkInfo = requireNonNull(networkInfo, "networkInfo must not be null"); this.preHandleWorkflow = requireNonNull(preHandleWorkflow, "preHandleWorkflow must not be null"); this.dispatcher = requireNonNull(dispatcher, "dispatcher must not be null"); @@ -230,6 +239,9 @@ public HandleWorkflow( this.handleWorkflowMetrics = requireNonNull(handleWorkflowMetrics, "handleWorkflowMetrics must not be null"); this.throttleServiceManager = requireNonNull(throttleServiceManager, "throttleServiceManager must not be null"); this.storeMetricsService = requireNonNull(storeMetricsService, "storeMetricsService must not be null"); + this.transactionChecker = requireNonNull(transactionChecker, "transactionChecker must not be null"); + this.initTrigger = requireNonNull(initTrigger, "initTrigger must not be null"); + this.softwareVersion = requireNonNull(softwareVersion, "softwareVersion must not be null"); } /** @@ -317,15 +329,50 @@ private void handleUserTransaction( @NonNull final ConsensusEvent platformEvent, @NonNull final NodeInfo creator, @NonNull final ConsensusTransaction platformTxn) { - // (FUTURE) We actually want consider exporting synthetic transactions on every + final var recordListBuilder = new RecordListBuilder(consensusNow); + final var recordBuilder = recordListBuilder.userTransactionRecordBuilder(); + + // First, check if the transaction is submitted with a version prior to the deployed version. If so, set the + // status on the receipt to BUSY and return + if (this.initTrigger != EVENT_STREAM_RECOVERY + && softwareVersion.compareTo(platformEvent.getSoftwareVersion()) > 0) { + // Reparse the transaction (so we don't need to get the prehandle result) + final TransactionInfo transactionInfo; + try { + transactionInfo = transactionChecker.parseAndCheck(platformTxn.getApplicationPayload()); + } catch (PreCheckException e) { + logger.error( + "Bad old transaction (version {}) from creator {}", + platformEvent.getSoftwareVersion(), + creator, + e); + // We don't care since we're checking a transaction with an older software version. We were going to + // skip the transaction handling anyway + return; + } + + // Initialize record builder list + recordBuilder + .transaction(transactionInfo.transaction()) + .transactionBytes(transactionInfo.signedBytes()) + .transactionID(transactionInfo.transactionID()) + .exchangeRate(exchangeRateManager.exchangeRates()) + .memo(transactionInfo.txBody().memo()); + + // Place a BUSY record in the cache + final var record = recordBuilder.status(ResponseCodeEnum.BUSY).build(); + recordCache.add(creator.nodeId(), transactionInfo.payerID(), List.of(record)); + + return; + } + + // (FUTURE) We actually want to consider exporting synthetic transactions on every // first post-upgrade transaction, not just the first transaction after genesis. final var consTimeOfLastHandledTxn = blockRecordManager.consTimeOfLastHandledTxn(); final var isFirstTransaction = !consTimeOfLastHandledTxn.isAfter(Instant.EPOCH); - // Setup record builder list + // Start the user transaction final boolean switchedBlocks = blockRecordManager.startUserTransaction(consensusNow, state, platformState); - final var recordListBuilder = new RecordListBuilder(consensusNow); - final var recordBuilder = recordListBuilder.userTransactionRecordBuilder(); // Setup helpers final var configuration = configProvider.getConfiguration(); diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/components/IngestComponentTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/components/IngestComponentTest.java index 05ff514fe1e6..8d163b919dfa 100644 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/components/IngestComponentTest.java +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/components/IngestComponentTest.java @@ -107,6 +107,7 @@ void setUp() { .servicesRegistry(Set::of) .instantSource(InstantSource.system()) .genesisRecordsConsensusHook(mock(GenesisRecordsConsensusHook.class)) + .softwareVersion(mock(HederaSoftwareVersion.class)) .build(); final var state = new FakeHederaState(); 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 ccb35c504488..25b27951afd7 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 @@ -17,6 +17,7 @@ package com.hedera.node.app.workflows.handle; import static com.hedera.hapi.node.base.ResponseCodeEnum.AUTHORIZATION_FAILED; +import static com.hedera.hapi.node.base.ResponseCodeEnum.BUSY; import static com.hedera.hapi.node.base.ResponseCodeEnum.ENTITY_NOT_ALLOWED_TO_DELETE; import static com.hedera.hapi.node.base.ResponseCodeEnum.SUCCESS; import static com.hedera.hapi.node.base.ResponseCodeEnum.UNAUTHORIZED; @@ -24,6 +25,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.intThat; import static org.mockito.ArgumentMatchers.notNull; @@ -69,6 +71,7 @@ import com.hedera.node.app.throttle.NetworkUtilizationManager; import com.hedera.node.app.throttle.SynchronizedThrottleAccumulator; import com.hedera.node.app.throttle.ThrottleServiceManager; +import com.hedera.node.app.version.HederaSoftwareVersion; import com.hedera.node.app.workflows.SolvencyPreCheck; import com.hedera.node.app.workflows.TransactionChecker; import com.hedera.node.app.workflows.TransactionScenarioBuilder; @@ -84,7 +87,9 @@ import com.hedera.node.config.testfixtures.HederaTestConfigBuilder; import com.hedera.pbj.runtime.io.buffer.Bytes; import com.swirlds.platform.state.PlatformState; +import com.swirlds.platform.system.InitTrigger; 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.SwirldTransaction; @@ -233,12 +238,24 @@ private static PreHandleResult createPreHandleResult(@NonNull Status status, @No @Mock private StoreMetricsService storeMetricsService; + @Mock + private TransactionChecker transactionChecker; + + @Mock + private InitTrigger initTrigger; + + @Mock + private SoftwareVersion softwareVersion; + private HandleWorkflow workflow; @BeforeEach void setup() throws PreCheckException { setupStandardStates(); + final var config = new VersionedConfigImpl(HederaTestConfigBuilder.createConfig(), CONFIG_VERSION); + when(configProvider.getConfiguration()).thenReturn(config); + accountsState.put( ALICE.accountID(), ALICE.account() @@ -257,15 +274,17 @@ void setup() throws PreCheckException { when(event.consensusTransactionIterator()) .thenReturn(List.of(platformTxn).iterator()); when(event.getCreatorId()).thenReturn(nodeSelfId); + + final var hederaVersion = + new HederaSoftwareVersion(selfNodeInfo.hapiVersion(), selfNodeInfo.appVersion(), (int) CONFIG_VERSION); + when(event.getSoftwareVersion()).thenReturn(hederaVersion); + when(platformTxn.getConsensusTimestamp()).thenReturn(CONSENSUS_NOW); when(platformTxn.getMetadata()).thenReturn(OK_RESULT); lenient().when(blockRecordManager.consTimeOfLastHandledTxn()).thenReturn(CONSENSUS_NOW.minusSeconds(1)); when(serviceLookup.getServiceName(any())).thenReturn(TokenService.NAME); - final var config = new VersionedConfigImpl(HederaTestConfigBuilder.createConfig(), CONFIG_VERSION); - when(configProvider.getConfiguration()).thenReturn(config); - when(solvencyPreCheck.getPayerAccount(any(), eq(ALICE.accountID()))).thenReturn(ALICE.account()); doAnswer(invocation -> { @@ -312,7 +331,10 @@ void setup() throws PreCheckException { cacheWarmer, handleWorkflowMetrics, throttleServiceManager, - storeMetricsService); + storeMetricsService, + transactionChecker, + initTrigger, + hederaVersion); } @SuppressWarnings("ConstantConditions") @@ -343,7 +365,10 @@ void testContructorWithInvalidArguments() { cacheWarmer, handleWorkflowMetrics, throttleServiceManager, - storeMetricsService)) + storeMetricsService, + transactionChecker, + initTrigger, + softwareVersion)) .isInstanceOf(NullPointerException.class); assertThatThrownBy(() -> new HandleWorkflow( networkInfo, @@ -370,7 +395,10 @@ void testContructorWithInvalidArguments() { cacheWarmer, handleWorkflowMetrics, throttleServiceManager, - storeMetricsService)) + storeMetricsService, + transactionChecker, + initTrigger, + softwareVersion)) .isInstanceOf(NullPointerException.class); assertThatThrownBy(() -> new HandleWorkflow( networkInfo, @@ -397,7 +425,10 @@ void testContructorWithInvalidArguments() { cacheWarmer, handleWorkflowMetrics, throttleServiceManager, - storeMetricsService)) + storeMetricsService, + transactionChecker, + initTrigger, + softwareVersion)) .isInstanceOf(NullPointerException.class); assertThatThrownBy(() -> new HandleWorkflow( networkInfo, @@ -424,7 +455,10 @@ void testContructorWithInvalidArguments() { cacheWarmer, handleWorkflowMetrics, throttleServiceManager, - storeMetricsService)) + storeMetricsService, + transactionChecker, + initTrigger, + softwareVersion)) .isInstanceOf(NullPointerException.class); assertThatThrownBy(() -> new HandleWorkflow( networkInfo, @@ -451,7 +485,10 @@ void testContructorWithInvalidArguments() { cacheWarmer, handleWorkflowMetrics, throttleServiceManager, - storeMetricsService)) + storeMetricsService, + transactionChecker, + initTrigger, + softwareVersion)) .isInstanceOf(NullPointerException.class); assertThatThrownBy(() -> new HandleWorkflow( networkInfo, @@ -478,7 +515,10 @@ void testContructorWithInvalidArguments() { cacheWarmer, handleWorkflowMetrics, throttleServiceManager, - storeMetricsService)) + storeMetricsService, + transactionChecker, + initTrigger, + softwareVersion)) .isInstanceOf(NullPointerException.class); assertThatThrownBy(() -> new HandleWorkflow( networkInfo, @@ -505,7 +545,10 @@ void testContructorWithInvalidArguments() { cacheWarmer, handleWorkflowMetrics, throttleServiceManager, - storeMetricsService)) + storeMetricsService, + transactionChecker, + initTrigger, + softwareVersion)) .isInstanceOf(NullPointerException.class); assertThatThrownBy(() -> new HandleWorkflow( networkInfo, @@ -532,7 +575,10 @@ void testContructorWithInvalidArguments() { cacheWarmer, handleWorkflowMetrics, throttleServiceManager, - storeMetricsService)) + storeMetricsService, + transactionChecker, + initTrigger, + softwareVersion)) .isInstanceOf(NullPointerException.class); assertThatThrownBy(() -> new HandleWorkflow( networkInfo, @@ -559,7 +605,10 @@ void testContructorWithInvalidArguments() { cacheWarmer, handleWorkflowMetrics, throttleServiceManager, - storeMetricsService)) + storeMetricsService, + transactionChecker, + initTrigger, + softwareVersion)) .isInstanceOf(NullPointerException.class); assertThatThrownBy(() -> new HandleWorkflow( networkInfo, @@ -586,7 +635,10 @@ void testContructorWithInvalidArguments() { cacheWarmer, handleWorkflowMetrics, throttleServiceManager, - storeMetricsService)) + storeMetricsService, + transactionChecker, + initTrigger, + softwareVersion)) .isInstanceOf(NullPointerException.class); assertThatThrownBy(() -> new HandleWorkflow( networkInfo, @@ -613,7 +665,10 @@ void testContructorWithInvalidArguments() { cacheWarmer, handleWorkflowMetrics, throttleServiceManager, - storeMetricsService)) + storeMetricsService, + transactionChecker, + initTrigger, + softwareVersion)) .isInstanceOf(NullPointerException.class); assertThatThrownBy(() -> new HandleWorkflow( networkInfo, @@ -640,7 +695,10 @@ void testContructorWithInvalidArguments() { cacheWarmer, handleWorkflowMetrics, throttleServiceManager, - storeMetricsService)) + storeMetricsService, + transactionChecker, + initTrigger, + softwareVersion)) .isInstanceOf(NullPointerException.class); assertThatThrownBy(() -> new HandleWorkflow( networkInfo, @@ -667,7 +725,10 @@ void testContructorWithInvalidArguments() { cacheWarmer, handleWorkflowMetrics, throttleServiceManager, - storeMetricsService)) + storeMetricsService, + transactionChecker, + initTrigger, + softwareVersion)) .isInstanceOf(NullPointerException.class); assertThatThrownBy(() -> new HandleWorkflow( networkInfo, @@ -694,7 +755,10 @@ void testContructorWithInvalidArguments() { cacheWarmer, handleWorkflowMetrics, throttleServiceManager, - storeMetricsService)) + storeMetricsService, + transactionChecker, + initTrigger, + softwareVersion)) .isInstanceOf(NullPointerException.class); assertThatThrownBy(() -> new HandleWorkflow( networkInfo, @@ -721,7 +785,10 @@ void testContructorWithInvalidArguments() { cacheWarmer, handleWorkflowMetrics, throttleServiceManager, - storeMetricsService)) + storeMetricsService, + transactionChecker, + initTrigger, + softwareVersion)) .isInstanceOf(NullPointerException.class); assertThatThrownBy(() -> new HandleWorkflow( networkInfo, @@ -748,7 +815,10 @@ void testContructorWithInvalidArguments() { cacheWarmer, handleWorkflowMetrics, throttleServiceManager, - storeMetricsService)) + storeMetricsService, + transactionChecker, + initTrigger, + softwareVersion)) .isInstanceOf(NullPointerException.class); assertThatThrownBy(() -> new HandleWorkflow( networkInfo, @@ -775,7 +845,10 @@ void testContructorWithInvalidArguments() { cacheWarmer, handleWorkflowMetrics, throttleServiceManager, - storeMetricsService)) + storeMetricsService, + transactionChecker, + initTrigger, + softwareVersion)) .isInstanceOf(NullPointerException.class); assertThatThrownBy(() -> new HandleWorkflow( networkInfo, @@ -802,7 +875,10 @@ void testContructorWithInvalidArguments() { cacheWarmer, handleWorkflowMetrics, throttleServiceManager, - storeMetricsService)) + storeMetricsService, + transactionChecker, + initTrigger, + softwareVersion)) .isInstanceOf(NullPointerException.class); assertThatThrownBy(() -> new HandleWorkflow( networkInfo, @@ -829,7 +905,10 @@ void testContructorWithInvalidArguments() { cacheWarmer, handleWorkflowMetrics, throttleServiceManager, - storeMetricsService)) + storeMetricsService, + transactionChecker, + initTrigger, + softwareVersion)) .isInstanceOf(NullPointerException.class); assertThatThrownBy(() -> new HandleWorkflow( networkInfo, @@ -856,7 +935,10 @@ void testContructorWithInvalidArguments() { cacheWarmer, handleWorkflowMetrics, throttleServiceManager, - storeMetricsService)) + storeMetricsService, + transactionChecker, + initTrigger, + softwareVersion)) .isInstanceOf(NullPointerException.class); assertThatThrownBy(() -> new HandleWorkflow( networkInfo, @@ -883,7 +965,70 @@ void testContructorWithInvalidArguments() { null, handleWorkflowMetrics, throttleServiceManager, - storeMetricsService)) + storeMetricsService, + transactionChecker, + initTrigger, + softwareVersion)) + .isInstanceOf(NullPointerException.class); + assertThatThrownBy(() -> new HandleWorkflow( + networkInfo, + preHandleWorkflow, + dispatcher, + blockRecordManager, + checker, + serviceLookup, + configProvider, + recordCache, + genesisRecordsTimeHook, + stakingPeriodTimeHook, + feeManager, + exchangeRateManager, + childRecordFinalizer, + finalizer, + systemFileUpdateFacility, + platformStateUpdateFacility, + solvencyPreCheck, + authorizer, + networkUtilizationManager, + synchronizedThrottleAccumulator, + scheduleExpirationHook, + cacheWarmer, + null, + throttleServiceManager, + storeMetricsService, + transactionChecker, + initTrigger, + softwareVersion)) + .isInstanceOf(NullPointerException.class); + assertThatThrownBy(() -> new HandleWorkflow( + networkInfo, + preHandleWorkflow, + dispatcher, + blockRecordManager, + checker, + serviceLookup, + configProvider, + recordCache, + genesisRecordsTimeHook, + stakingPeriodTimeHook, + feeManager, + exchangeRateManager, + childRecordFinalizer, + finalizer, + systemFileUpdateFacility, + platformStateUpdateFacility, + solvencyPreCheck, + authorizer, + networkUtilizationManager, + synchronizedThrottleAccumulator, + scheduleExpirationHook, + cacheWarmer, + handleWorkflowMetrics, + null, + storeMetricsService, + transactionChecker, + initTrigger, + softwareVersion)) .isInstanceOf(NullPointerException.class); assertThatThrownBy(() -> new HandleWorkflow( networkInfo, @@ -908,9 +1053,42 @@ void testContructorWithInvalidArguments() { synchronizedThrottleAccumulator, scheduleExpirationHook, cacheWarmer, + handleWorkflowMetrics, + throttleServiceManager, null, + transactionChecker, + initTrigger, + softwareVersion)) + .isInstanceOf(NullPointerException.class); + assertThatThrownBy(() -> new HandleWorkflow( + networkInfo, + preHandleWorkflow, + dispatcher, + blockRecordManager, + checker, + serviceLookup, + configProvider, + recordCache, + genesisRecordsTimeHook, + stakingPeriodTimeHook, + feeManager, + exchangeRateManager, + childRecordFinalizer, + finalizer, + systemFileUpdateFacility, + platformStateUpdateFacility, + solvencyPreCheck, + authorizer, + networkUtilizationManager, + synchronizedThrottleAccumulator, + scheduleExpirationHook, + cacheWarmer, + handleWorkflowMetrics, throttleServiceManager, - storeMetricsService)) + storeMetricsService, + null, + initTrigger, + softwareVersion)) .isInstanceOf(NullPointerException.class); assertThatThrownBy(() -> new HandleWorkflow( networkInfo, @@ -936,8 +1114,11 @@ void testContructorWithInvalidArguments() { scheduleExpirationHook, cacheWarmer, handleWorkflowMetrics, + throttleServiceManager, + storeMetricsService, + transactionChecker, null, - storeMetricsService)) + softwareVersion)) .isInstanceOf(NullPointerException.class); assertThatThrownBy(() -> new HandleWorkflow( networkInfo, @@ -964,6 +1145,9 @@ void testContructorWithInvalidArguments() { cacheWarmer, handleWorkflowMetrics, throttleServiceManager, + storeMetricsService, + transactionChecker, + initTrigger, null)) .isInstanceOf(NullPointerException.class); } @@ -1500,6 +1684,40 @@ void testInsolventPayerAccountFails(final ResponseCodeEnum responseCode) throws assertThat(accountsState.get(nodeSelfAccountId).tinybarBalance()).isLessThan(DEFAULT_FEES.totalFee()); } + @Test + @DisplayName("Reject transaction as BUSY, if the txn version is older than the software") + void testOldVersionTxnFails() throws PreCheckException { + // given + // Construct a version intentionally lower than the version given to the workflow + final var hederaVersion = new HederaSoftwareVersion( + selfNodeInfo.hapiVersion(), + selfNodeInfo + .appVersion() + .copyBuilder() + .minor(selfNodeInfo.appVersion().minor() - 1) + .build(), + (int) CONFIG_VERSION); + when(event.getSoftwareVersion()).thenReturn(hederaVersion); + given(transactionChecker.parseAndCheck(any())).willReturn(OK_RESULT.txInfo()); + + // when + workflow.handleRound(state, platformState, round); + + // then + // Verify a BUSY record was added to the cache + final var recordsCaptor = ArgumentCaptor.forClass(List.class); + verify(recordCache).add(anyLong(), notNull(), recordsCaptor.capture()); + final var capturedRecords = recordsCaptor.getValue(); + assertThat(capturedRecords).hasSize(1); + final var record = (SingleTransactionRecord) capturedRecords.getFirst(); + assertThat(record.transactionRecord().receipt().status()).isEqualTo(BUSY); + + // Verify handling of the old transaction didn't proceed + verify(blockRecordManager, never()).advanceConsensusClock(any(), any()); + verify(blockRecordManager, never()).startUserTransaction(any(), any(), any()); + verify(blockRecordManager, never()).endUserTransaction(any(), any()); + } + @Test @DisplayName("Reject transaction, if the payer is not authorized") void testNonAuthorizedAccountFails() {