From f2d47d2db97e9810d4b5d4a4ac2aa2fa17ed3549 Mon Sep 17 00:00:00 2001 From: Neeharika Sompalli <52669918+Neeharika-Sompalli@users.noreply.github.com> Date: Wed, 12 Jun 2024 09:33:49 -0500 Subject: [PATCH] chore: Add logic for `ChildDispatchScope`, `UserDispatchScope` and `UserTxnScope` (#13780) Signed-off-by: Neeharika-Sompalli Co-authored-by: Michael Tinker --- .../child/logic/ChildDispatchFactory.java | 251 ++++++++++++++++++ .../logic/ChildRecordBuilderFactory.java | 103 +++++++ .../child/logic/ChildTxnInfoFactory.java | 83 ++++++ .../user/logic/PreHandleResultManager.java | 86 ++++++ .../user/logic/UserRecordInitializer.java | 67 +++++ .../txn/logic/HollowAccountCompleter.java | 197 ++++++++++++++ .../handle/flow/txn/logic/SchedulePurger.java | 77 ++++++ .../child/logic/ChildDispatchFactoryTest.java | 213 +++++++++++++++ .../logic/ChildRecordBuilderFactoryTest.java | 215 +++++++++++++++ .../child/logic/ChildTxnInfoFactoryTest.java | 95 +++++++ .../txn/logic/HollowAccountCompleterTest.java | 235 ++++++++++++++++ .../txn/logic/SchedulePurgerTest.java | 127 +++++++++ .../logic/PreHandleResultManagerTest.java | 105 ++++++++ .../user/logic/UserRecordInitializerTest.java | 156 +++++++++++ 14 files changed, 2010 insertions(+) create mode 100644 hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/flow/dispatch/child/logic/ChildDispatchFactory.java create mode 100644 hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/flow/dispatch/child/logic/ChildRecordBuilderFactory.java create mode 100644 hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/flow/dispatch/child/logic/ChildTxnInfoFactory.java create mode 100644 hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/flow/dispatch/user/logic/PreHandleResultManager.java create mode 100644 hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/flow/dispatch/user/logic/UserRecordInitializer.java create mode 100644 hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/flow/txn/logic/HollowAccountCompleter.java create mode 100644 hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/flow/txn/logic/SchedulePurger.java create mode 100644 hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/flow/dispatch/child/logic/ChildDispatchFactoryTest.java create mode 100644 hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/flow/dispatch/child/logic/ChildRecordBuilderFactoryTest.java create mode 100644 hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/flow/dispatch/child/logic/ChildTxnInfoFactoryTest.java create mode 100644 hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/flow/dispatch/txn/logic/HollowAccountCompleterTest.java create mode 100644 hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/flow/dispatch/txn/logic/SchedulePurgerTest.java create mode 100644 hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/flow/dispatch/user/logic/PreHandleResultManagerTest.java create mode 100644 hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/flow/dispatch/user/logic/UserRecordInitializerTest.java diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/flow/dispatch/child/logic/ChildDispatchFactory.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/flow/dispatch/child/logic/ChildDispatchFactory.java new file mode 100644 index 000000000000..7a8e4c5bedc6 --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/flow/dispatch/child/logic/ChildDispatchFactory.java @@ -0,0 +1,251 @@ +/* + * Copyright (C) 2024 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.node.app.workflows.handle.flow.dispatch.child.logic; + +import static com.hedera.hapi.node.base.ResponseCodeEnum.OK; +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 com.hedera.hapi.node.base.AccountID; +import com.hedera.hapi.node.base.Key; +import com.hedera.hapi.node.transaction.TransactionBody; +import com.hedera.node.app.signature.DelegateKeyVerifier; +import com.hedera.node.app.signature.KeyVerifier; +import com.hedera.node.app.signature.impl.SignatureVerificationImpl; +import com.hedera.node.app.spi.signatures.SignatureVerification; +import com.hedera.node.app.spi.signatures.VerificationAssistant; +import com.hedera.node.app.spi.workflows.ComputeDispatchFeesAsTopLevel; +import com.hedera.node.app.spi.workflows.HandleContext; +import com.hedera.node.app.spi.workflows.PreCheckException; +import com.hedera.node.app.spi.workflows.record.ExternalizedRecordCustomizer; +import com.hedera.node.app.workflows.dispatcher.TransactionDispatcher; +import com.hedera.node.app.workflows.handle.flow.dispatch.Dispatch; +import com.hedera.node.app.workflows.handle.flow.dispatch.child.ChildDispatchComponent; +import com.hedera.node.app.workflows.handle.record.SingleTransactionRecordBuilderImpl; +import com.hedera.node.app.workflows.handle.stack.SavepointStackImpl; +import com.hedera.node.app.workflows.prehandle.PreHandleContextImpl; +import com.hedera.node.app.workflows.prehandle.PreHandleResult; +import com.hedera.pbj.runtime.io.buffer.Bytes; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.util.Collections; +import java.util.function.Predicate; +import javax.inject.Inject; +import javax.inject.Provider; +import javax.inject.Singleton; + +/** + * A factory for constructing child dispatches.This also gets the pre-handle result for the child transaction, + * and signature verifications for the child transaction. + */ +@Singleton +public class ChildDispatchFactory { + private static final NoOpKeyVerifier NO_OP_KEY_VERIFIER = new NoOpKeyVerifier(); + + private final ChildTxnInfoFactory childTxnInfoFactory; + private final TransactionDispatcher dispatcher; + private final ChildRecordBuilderFactory recordBuilderFactory; + + @Inject + public ChildDispatchFactory( + final ChildTxnInfoFactory childTxnInfoFactory, + final TransactionDispatcher dispatcher, + final ChildRecordBuilderFactory recordBuilderFactory) { + this.childTxnInfoFactory = childTxnInfoFactory; + this.dispatcher = dispatcher; + this.recordBuilderFactory = recordBuilderFactory; + } + + /** + * Creates a child dispatch. This method computes the transaction info and initializes record builder for the child + * transaction. + * @param parentDispatch the parent dispatch + * @param txBody the transaction body + * @param callback the key verifier for child dispatch + * @param syntheticPayerId the synthetic payer id + * @param category the transaction category + * @param childDispatchFactory the child dispatch factory + * @param customizer the externalized record customizer + * @param reversingBehavior the reversing behavior + * @return the child dispatch + */ + public Dispatch createChildDispatch( + @NonNull final Dispatch parentDispatch, + @NonNull final TransactionBody txBody, + @Nullable final Predicate callback, + @NonNull final AccountID syntheticPayerId, + @NonNull final HandleContext.TransactionCategory category, + @NonNull final Provider childDispatchFactory, + @NonNull final ExternalizedRecordCustomizer customizer, + @NonNull final SingleTransactionRecordBuilderImpl.ReversingBehavior reversingBehavior) { + final var preHandleResult = dispatchPreHandleForChildTxn(parentDispatch, txBody, syntheticPayerId); + final var childTxnInfo = childTxnInfoFactory.getTxnInfoFrom(txBody); + final var recordBuilder = recordBuilderFactory.recordBuilderFor( + childTxnInfo, + parentDispatch.recordListBuilder(), + parentDispatch.handleContext().configuration(), + category, + reversingBehavior, + customizer); + + return childDispatchFactory + .get() + .create( + recordBuilder, + childTxnInfo, + isScheduled(category), + syntheticPayerId, + category, + new SavepointStackImpl(parentDispatch.stack().peek()), + preHandleResult, + getKeyVerifier(callback)); + } + + /** + * Dispatches the pre-handle checks for the child transaction. This runs pureChecks and then dispatches pre-handle + * for child transaction. + * @param parentDispatch the parent dispatch + * @param txBody the transaction body + * @param syntheticPayerId the synthetic payer id + * @return the pre-handle result + */ + private PreHandleResult dispatchPreHandleForChildTxn( + final @NonNull Dispatch parentDispatch, + final @NonNull TransactionBody txBody, + final @NonNull AccountID syntheticPayerId) { + try { + dispatcher.dispatchPureChecks(txBody); + final var preHandleContext = new PreHandleContextImpl( + parentDispatch.readableStoreFactory(), + txBody, + syntheticPayerId, + parentDispatch.handleContext().configuration(), + dispatcher); + dispatcher.dispatchPreHandle(preHandleContext); + return new PreHandleResult( + null, + null, + SO_FAR_SO_GOOD, + OK, + null, + preHandleContext.requiredNonPayerKeys(), + null, + preHandleContext.requiredHollowAccounts(), + null, + null, + 0); + } catch (final PreCheckException e) { + return new PreHandleResult( + null, + null, + PRE_HANDLE_FAILURE, + e.responseCode(), + null, + Collections.emptySet(), + null, + Collections.emptySet(), + null, + null, + 0); + } + } + + /** + * Returns whether the transaction is scheduled or not. + * @param category the transaction category + * @return the compute dispatch fees as top level + */ + @NonNull + private static ComputeDispatchFeesAsTopLevel isScheduled(final HandleContext.TransactionCategory category) { + return category == HandleContext.TransactionCategory.SCHEDULED + ? ComputeDispatchFeesAsTopLevel.YES + : ComputeDispatchFeesAsTopLevel.NO; + } + + /** + * A {@link KeyVerifier} that always returns {@link SignatureVerificationImpl} with a + * passed verification. + */ + static class NoOpKeyVerifier implements KeyVerifier { + private static final SignatureVerification PASSED_VERIFICATION = + new SignatureVerificationImpl(Key.DEFAULT, Bytes.EMPTY, true); + + @NonNull + @Override + public SignatureVerification verificationFor(@NonNull final Key key) { + return PASSED_VERIFICATION; + } + + @NonNull + @Override + public SignatureVerification verificationFor( + @NonNull final Key key, @NonNull final VerificationAssistant callback) { + return PASSED_VERIFICATION; + } + + @NonNull + @Override + public SignatureVerification verificationFor(@NonNull final Bytes evmAlias) { + return PASSED_VERIFICATION; + } + + @Override + public int numSignaturesVerified() { + return 0; + } + } + + /** + * Returns a {@link KeyVerifier} based on the callback. If the callback is null, then it returns a + * {@link NoOpKeyVerifier}. Otherwise, it returns a {@link DelegateKeyVerifier} with the callback. + * The callback is null if the signature verification is not required. This is the case for hollow account + * completion and auto account creation. + * @param callback the callback + * @return the key verifier + */ + static KeyVerifier getKeyVerifier(@Nullable Predicate callback) { + return callback == null + ? NO_OP_KEY_VERIFIER + : new KeyVerifier() { + private final KeyVerifier verifier = new DelegateKeyVerifier(callback); + + @NonNull + @Override + public SignatureVerification verificationFor(@NonNull final Key key) { + return callback.test(key) ? NoOpKeyVerifier.PASSED_VERIFICATION : verifier.verificationFor(key); + } + + @NonNull + @Override + public SignatureVerification verificationFor( + @NonNull final Key key, @NonNull final VerificationAssistant callback) { + throw new UnsupportedOperationException("Should never be called!"); + } + + @NonNull + @Override + public SignatureVerification verificationFor(@NonNull final Bytes evmAlias) { + throw new UnsupportedOperationException("Should never be called!"); + } + + @Override + public int numSignaturesVerified() { + return 0; + } + }; + } +} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/flow/dispatch/child/logic/ChildRecordBuilderFactory.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/flow/dispatch/child/logic/ChildRecordBuilderFactory.java new file mode 100644 index 000000000000..f0c88baa8bc8 --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/flow/dispatch/child/logic/ChildRecordBuilderFactory.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2024 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.node.app.workflows.handle.flow.dispatch.child.logic; + +import static com.hedera.node.app.spi.workflows.HandleContext.PrecedingTransactionCategory.LIMITED_CHILD_RECORDS; +import static com.hedera.node.app.spi.workflows.HandleContext.TransactionCategory.CHILD; +import static com.hedera.node.app.spi.workflows.HandleContext.TransactionCategory.PRECEDING; +import static com.hedera.node.app.spi.workflows.HandleContext.TransactionCategory.SCHEDULED; +import static java.util.Objects.requireNonNull; + +import com.hedera.node.app.spi.workflows.HandleContext; +import com.hedera.node.app.spi.workflows.record.ExternalizedRecordCustomizer; +import com.hedera.node.app.workflows.TransactionInfo; +import com.hedera.node.app.workflows.handle.record.RecordListBuilder; +import com.hedera.node.app.workflows.handle.record.SingleTransactionRecordBuilderImpl; +import com.swirlds.config.api.Configuration; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import javax.inject.Inject; +import javax.inject.Singleton; + +/** + * Provider of the child record builder based on the dispatched child transaction category + */ +@Singleton +public class ChildRecordBuilderFactory { + /** + * Constructs the {@link ChildRecordBuilderFactory} instance. + */ + @Inject + public ChildRecordBuilderFactory() {} + + /** + * Provides the record builder for the child transaction category and initializes it. + * The record builder is created based on the child category and the reversing behavior. + * @param txnInfo the transaction info + * @param recordListBuilder the record list builder + * @param configuration the configuration + * @param childCategory the child category + * @param reversingBehavior the reversing behavior + * @param customizer the externalized record customizer + * @return the record builder + */ + public SingleTransactionRecordBuilderImpl recordBuilderFor( + TransactionInfo txnInfo, + final RecordListBuilder recordListBuilder, + final Configuration configuration, + HandleContext.TransactionCategory childCategory, + SingleTransactionRecordBuilderImpl.ReversingBehavior reversingBehavior, + @Nullable final ExternalizedRecordCustomizer customizer) { + final SingleTransactionRecordBuilderImpl recordBuilder; + if (childCategory == PRECEDING) { + recordBuilder = switch (reversingBehavior) { + case REMOVABLE -> recordListBuilder.addRemovablePreceding(configuration); + case REVERSIBLE -> recordListBuilder.addReversiblePreceding(configuration); + case IRREVERSIBLE -> recordListBuilder.addPreceding(configuration, LIMITED_CHILD_RECORDS);}; + } else if (childCategory == CHILD) { + recordBuilder = switch (reversingBehavior) { + case REMOVABLE -> recordListBuilder.addRemovableChildWithExternalizationCustomizer( + configuration, requireNonNull(customizer)); + case REVERSIBLE -> recordListBuilder.addChild(configuration, childCategory); + case IRREVERSIBLE -> throw new IllegalArgumentException("Unsupported reversing behavior: " + + reversingBehavior + " for child category: " + childCategory);}; + } else if (childCategory == SCHEDULED) { + recordBuilder = recordListBuilder.addChild(configuration, childCategory); + } else { + throw new IllegalArgumentException("Unsupported child category: " + childCategory); + } + initializeRecord(recordBuilder, txnInfo); + return recordBuilder; + } + + /** + * Initializes the user record with the transaction information. + * @param recordBuilder the record builder + * @param txnInfo the transaction info + */ + private void initializeRecord( + @NonNull final SingleTransactionRecordBuilderImpl recordBuilder, @NonNull final TransactionInfo txnInfo) { + recordBuilder + .transaction(txnInfo.transaction()) + .transactionBytes(txnInfo.signedBytes()) + .memo(txnInfo.txBody().memo()); + final var transactionID = txnInfo.txBody().transactionID(); + if (transactionID != null) { + recordBuilder.transactionID(transactionID); + } + } +} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/flow/dispatch/child/logic/ChildTxnInfoFactory.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/flow/dispatch/child/logic/ChildTxnInfoFactory.java new file mode 100644 index 000000000000..9c032fbb5498 --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/flow/dispatch/child/logic/ChildTxnInfoFactory.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2024 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.node.app.workflows.handle.flow.dispatch.child.logic; + +import static com.hedera.hapi.util.HapiUtils.functionOf; + +import com.hedera.hapi.node.base.AccountID; +import com.hedera.hapi.node.base.HederaFunctionality; +import com.hedera.hapi.node.base.SignatureMap; +import com.hedera.hapi.node.base.Transaction; +import com.hedera.hapi.node.base.TransactionID; +import com.hedera.hapi.node.transaction.SignedTransaction; +import com.hedera.hapi.node.transaction.TransactionBody; +import com.hedera.hapi.util.UnknownHederaFunctionality; +import com.hedera.node.app.workflows.TransactionInfo; +import javax.inject.Inject; +import javax.inject.Singleton; + +/** + * Factory for providing all needed information for a child transaction. + */ +@Singleton +public class ChildTxnInfoFactory { + /** + * Constructs the {@link ChildTxnInfoFactory} instance. + */ + @Inject + public ChildTxnInfoFactory() {} + + /** + * Provides the transaction information for the given dispatched transaction body. + * @param txBody the transaction body + * @return the transaction information + */ + public TransactionInfo getTxnInfoFrom(TransactionBody txBody) { + final var bodyBytes = TransactionBody.PROTOBUF.toBytes(txBody); + final var signedTransaction = + SignedTransaction.newBuilder().bodyBytes(bodyBytes).build(); + final var signedTransactionBytes = SignedTransaction.PROTOBUF.toBytes(signedTransaction); + final var transaction = Transaction.newBuilder() + .signedTransactionBytes(signedTransactionBytes) + .build(); + // Since in the current systems the synthetic transactions need not have a transaction ID + // Payer will be injected as synthetic payer in dagger subcomponent, since the payer could be different + // for schedule dispatches. Also, there will not be signature verifications for synthetic transactions. + // So these fields are set to default values and will not be used. + return new TransactionInfo( + transaction, + txBody, + TransactionID.DEFAULT, + AccountID.DEFAULT, + SignatureMap.DEFAULT, + signedTransactionBytes, + functionOfTxn(txBody)); + } + + /** + * Provides the functionality of the transaction body. + * @param txBody the transaction body + * @return the functionality + */ + private static HederaFunctionality functionOfTxn(final TransactionBody txBody) { + try { + return functionOf(txBody); + } catch (final UnknownHederaFunctionality e) { + throw new IllegalArgumentException("Unknown Hedera Functionality", e); + } + } +} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/flow/dispatch/user/logic/PreHandleResultManager.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/flow/dispatch/user/logic/PreHandleResultManager.java new file mode 100644 index 000000000000..f7bd648a2e8e --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/flow/dispatch/user/logic/PreHandleResultManager.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2024 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.node.app.workflows.handle.flow.dispatch.user.logic; + +import com.hedera.node.app.service.token.ReadableAccountStore; +import com.hedera.node.app.workflows.SolvencyPreCheck; +import com.hedera.node.app.workflows.dispatcher.ReadableStoreFactory; +import com.hedera.node.app.workflows.prehandle.PreHandleResult; +import com.hedera.node.app.workflows.prehandle.PreHandleWorkflow; +import com.swirlds.platform.system.transaction.ConsensusTransaction; +import com.swirlds.state.spi.info.NodeInfo; +import edu.umd.cs.findbugs.annotations.NonNull; +import javax.inject.Inject; +import javax.inject.Singleton; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** + * A class that manages the pre-handle result for a transaction. + */ +@Singleton +public class PreHandleResultManager { + private static final Logger logger = LogManager.getLogger(PreHandleResultManager.class); + final PreHandleWorkflow preHandleWorkflow; + final SolvencyPreCheck solvencyPreCheck; + + @Inject + public PreHandleResultManager( + @NonNull final PreHandleWorkflow preHandleWorkflow, @NonNull final SolvencyPreCheck solvencyPreCheck) { + this.preHandleWorkflow = preHandleWorkflow; + this.solvencyPreCheck = solvencyPreCheck; + } + + /** + * This method gets all the verification data for the current transaction. If pre-handle was previously ran + * successfully, we only add the missing keys. If it did not run or an error occurred, we run it again. + * If there is a due diligence error, this method will return a CryptoTransfer to charge the node along with + * its verification data. + * @param creator the node that created the transaction + * @param platformTxn the transaction to be verified + * @param storeFactory the store factory + * @return the verification data for the transaction + */ + @NonNull + public PreHandleResult getCurrentPreHandleResult( + @NonNull final NodeInfo creator, + @NonNull final ConsensusTransaction platformTxn, + final ReadableStoreFactory storeFactory) { + final var metadata = platformTxn.getMetadata(); + final PreHandleResult previousResult; + if (metadata instanceof PreHandleResult result) { + previousResult = result; + } else { + // This should be impossible since the Platform contract guarantees that SwirldState.preHandle() + // is always called before SwirldState.handleTransaction(); and our preHandle() implementation + // always sets the metadata to a PreHandleResult + logger.error( + "Received transaction without PreHandleResult metadata from node {} (was {})", + creator.nodeId(), + metadata); + previousResult = null; + } + // We do not know how long transactions are kept in memory. Clearing metadata to avoid keeping it for too long. + platformTxn.setMetadata(null); + return preHandleWorkflow.preHandleTransaction( + creator.accountId(), + storeFactory, + storeFactory.getStore(ReadableAccountStore.class), + platformTxn, + previousResult); + } +} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/flow/dispatch/user/logic/UserRecordInitializer.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/flow/dispatch/user/logic/UserRecordInitializer.java new file mode 100644 index 000000000000..a373c88b932d --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/flow/dispatch/user/logic/UserRecordInitializer.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2024 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.node.app.workflows.handle.flow.dispatch.user.logic; + +import com.hedera.hapi.node.base.Transaction; +import com.hedera.node.app.fees.ExchangeRateManager; +import com.hedera.node.app.workflows.TransactionInfo; +import com.hedera.node.app.workflows.handle.record.SingleTransactionRecordBuilderImpl; +import com.hedera.pbj.runtime.io.buffer.Bytes; +import javax.inject.Inject; +import javax.inject.Singleton; + +/** + * Initializes the user record with all the necessary information. + */ +@Singleton +public class UserRecordInitializer { + private final ExchangeRateManager exchangeRateManager; + + /** + * Creates a user record initializer with the given exchange rate manager. + * @param exchangeRateManager the exchange rate manager + */ + @Inject + public UserRecordInitializer(final ExchangeRateManager exchangeRateManager) { + this.exchangeRateManager = exchangeRateManager; + } + + /** + * Initializes the user record with the transaction information. The record builder list is initialized with the + * transaction, transaction bytes, transaction ID, exchange rate, and memo. + * @param recordBuilder the record builder + * @param txnInfo the transaction info + */ + // TODO: Guarantee that this never throws an exception + public void initializeUserRecord(SingleTransactionRecordBuilderImpl recordBuilder, TransactionInfo txnInfo) { + final Bytes transactionBytes; + final var transaction = txnInfo.transaction(); + if (transaction.signedTransactionBytes().length() > 0) { + transactionBytes = transaction.signedTransactionBytes(); + } else { + // in this case, recorder hash the transaction itself, not its bodyBytes. + transactionBytes = Transaction.PROTOBUF.toBytes(transaction); + } + // Initialize record builder list + recordBuilder + .transaction(txnInfo.transaction()) + .transactionBytes(transactionBytes) + .transactionID(txnInfo.txBody().transactionIDOrThrow()) + .exchangeRate(exchangeRateManager.exchangeRates()) + .memo(txnInfo.txBody().memo()); + } +} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/flow/txn/logic/HollowAccountCompleter.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/flow/txn/logic/HollowAccountCompleter.java new file mode 100644 index 000000000000..d9f5731522dd --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/flow/txn/logic/HollowAccountCompleter.java @@ -0,0 +1,197 @@ +/* + * Copyright (C) 2024 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.node.app.workflows.handle.flow.txn.logic; + +import static com.hedera.hapi.node.base.HederaFunctionality.ETHEREUM_TRANSACTION; +import static com.hedera.hapi.util.HapiUtils.isHollow; +import static com.hedera.node.app.service.contract.impl.ContractServiceImpl.CONTRACT_SERVICE; +import static com.hedera.node.app.spi.key.KeyUtils.IMMUTABILITY_SENTINEL_KEY; +import static java.util.Objects.requireNonNull; + +import com.hedera.hapi.node.base.AccountID; +import com.hedera.hapi.node.base.Key; +import com.hedera.hapi.node.state.token.Account; +import com.hedera.hapi.node.token.CryptoUpdateTransactionBody; +import com.hedera.hapi.node.transaction.TransactionBody; +import com.hedera.node.app.service.file.ReadableFileStore; +import com.hedera.node.app.service.token.ReadableAccountStore; +import com.hedera.node.app.service.token.records.CryptoUpdateRecordBuilder; +import com.hedera.node.app.signature.KeyVerifier; +import com.hedera.node.app.signature.impl.SignatureVerificationImpl; +import com.hedera.node.app.spi.signatures.SignatureVerification; +import com.hedera.node.app.spi.workflows.HandleContext; +import com.hedera.node.app.workflows.handle.flow.dispatch.Dispatch; +import com.hedera.node.app.workflows.handle.flow.txn.UserTransactionComponent; +import com.hedera.node.app.workflows.handle.record.RecordListBuilder; +import com.hedera.node.config.data.ConsensusConfig; +import com.hedera.pbj.runtime.io.buffer.Bytes; +import com.swirlds.config.api.Configuration; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.util.LinkedHashSet; +import java.util.Set; +import javax.inject.Inject; +import javax.inject.Singleton; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** + * Completes the hollow accounts by finalizing them. + */ +@Singleton +public class HollowAccountCompleter { + private static final Logger logger = LogManager.getLogger(HollowAccountCompleter.class); + + @Inject + public HollowAccountCompleter() { + // do nothing + } + + /** + * Finalizes the hollow accounts by updating the key on the hollow accounts that need to be finalized. + * This is done by dispatching a preceding synthetic update transaction. The key is derived from the signature + * expansion, by looking up the ECDSA key for the alias. + * The hollow accounts that need to be finalized are determined by the set of hollow accounts that are returned + * by the pre-handle result. + * @param userTxn the user transaction component + * @param dispatch the dispatch + */ + public void finalizeHollowAccounts(@NonNull UserTransactionComponent userTxn, @NonNull Dispatch dispatch) { + // Any hollow accounts that must sign to have all needed signatures, need to be finalized + // as a result of transaction being handled. + Set hollowAccounts = userTxn.preHandleResult().getHollowAccounts(); + SignatureVerification maybeEthTxVerification = null; + if (userTxn.functionality() == ETHEREUM_TRANSACTION) { + final var ethFinalization = findEthHollowAccount(userTxn); + if (ethFinalization != null) { + hollowAccounts = new LinkedHashSet<>(userTxn.preHandleResult().getHollowAccounts()); + hollowAccounts.add(ethFinalization.hollowAccount); + maybeEthTxVerification = ethFinalization.ethVerification(); + } + } + finalizeHollowAccounts( + dispatch.handleContext(), + userTxn.configuration(), + hollowAccounts, + dispatch.keyVerifier(), + maybeEthTxVerification, + userTxn.recordListBuilder()); + } + + /** + * Finds the hollow account that needs to be finalized for the Ethereum transaction. + * @param userTxn the user transaction component + * @return the hollow account that needs to be finalized for the Ethereum transaction + */ + @Nullable + private EthFinalization findEthHollowAccount(UserTransactionComponent userTxn) { + final var maybeEthTxSigs = CONTRACT_SERVICE + .handlers() + .ethereumTransactionHandler() + .maybeEthTxSigsFor( + userTxn.txnInfo().txBody().ethereumTransactionOrThrow(), + userTxn.readableStoreFactory().getStore(ReadableFileStore.class), + userTxn.configuration()); + if (maybeEthTxSigs != null) { + final var alias = Bytes.wrap(maybeEthTxSigs.address()); + final var accountStore = userTxn.readableStoreFactory().getStore(ReadableAccountStore.class); + final var maybeHollowAccountId = accountStore.getAccountIDByAlias(alias); + if (maybeHollowAccountId != null) { + final var maybeHollowAccount = requireNonNull(accountStore.getAccountById(maybeHollowAccountId)); + if (isHollow(maybeHollowAccount)) { + return new EthFinalization( + maybeHollowAccount, + new SignatureVerificationImpl( + Key.newBuilder() + .ecdsaSecp256k1(Bytes.wrap(maybeEthTxSigs.publicKey())) + .build(), + alias, + true)); + } + } + } + return null; + } + + /** + * Updates key on the hollow accounts that need to be finalized. This is done by dispatching a preceding + * synthetic update transaction. The ksy is derived from the signature expansion, by looking up the ECDSA key + * for the alias. + * + * @param context the handle context + * @param configuration the configuration + * @param accounts the set of hollow accounts that need to be finalized + * @param verifier the key verifier + * @param ethTxVerification the Ethereum transaction verification + */ + private void finalizeHollowAccounts( + @NonNull final HandleContext context, + @NonNull final Configuration configuration, + @NonNull final Set accounts, + @NonNull final KeyVerifier verifier, + @Nullable SignatureVerification ethTxVerification, + @NonNull final RecordListBuilder recordListBuilder) { + final var consensusConfig = configuration.getConfigData(ConsensusConfig.class); + final var maxRecords = consensusConfig.handleMaxPrecedingRecords(); + for (final var hollowAccount : accounts) { + if (recordListBuilder.precedingRecordBuilders().size() == maxRecords) { + break; + } + if (hollowAccount.accountIdOrElse(AccountID.DEFAULT).equals(AccountID.DEFAULT)) { + // The CryptoCreateHandler uses a "hack" to validate that a CryptoCreate with + // an EVM address has signed with that alias's ECDSA key; that is, it adds a + // dummy "hollow account" with the EVM address as an alias. But we don't want + // to try to finalize such a dummy account, so skip it here. + continue; + } + // get the verified key for this hollow account + final var verification = + ethTxVerification != null && hollowAccount.alias().equals(ethTxVerification.evmAlias()) + ? ethTxVerification + : requireNonNull( + verifier.verificationFor(hollowAccount.alias()), + "Required hollow account verified signature did not exist"); + if (verification.key() != null) { + if (!IMMUTABILITY_SENTINEL_KEY.equals(hollowAccount.keyOrThrow())) { + logger.error("Hollow account {} has a key other than the sentinel key", hollowAccount); + return; + } + // dispatch synthetic update transaction for updating key on this hollow account + final var syntheticUpdateTxn = TransactionBody.newBuilder() + .cryptoUpdateAccount(CryptoUpdateTransactionBody.newBuilder() + .accountIDToUpdate(hollowAccount.accountId()) + .key(verification.key()) + .build()) + .build(); + // Note the null key verification callback below; we bypass signature + // verifications when doing hollow account finalization + final var recordBuilder = context.dispatchPrecedingTransaction( + syntheticUpdateTxn, CryptoUpdateRecordBuilder.class, null, context.payer()); + // For some reason update accountId is set only for the hollow account finalizations and not + // for top level crypto update transactions. So we set it here. + recordBuilder.accountID(hollowAccount.accountId()); + } + } + } + + /** + * A record that contains the hollow account and the Ethereum verification. + * @param hollowAccount the hollow account + * @param ethVerification the Ethereum verification + */ + private record EthFinalization(Account hollowAccount, SignatureVerification ethVerification) {} +} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/flow/txn/logic/SchedulePurger.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/flow/txn/logic/SchedulePurger.java new file mode 100644 index 000000000000..2eaaa98adb25 --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/flow/txn/logic/SchedulePurger.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2024 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.node.app.workflows.handle.flow.txn.logic; + +import com.hedera.node.app.service.schedule.ScheduleService; +import com.hedera.node.app.service.schedule.WritableScheduleStore; +import com.hedera.node.app.spi.metrics.StoreMetricsService; +import com.hedera.node.app.workflows.dispatcher.WritableStoreFactory; +import com.hedera.node.app.workflows.handle.ScheduleExpirationHook; +import com.hedera.node.app.workflows.handle.flow.txn.UserTransactionComponent; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.time.Instant; +import javax.inject.Inject; +import javax.inject.Singleton; + +/** + * The logic for the schedule service cron that purges expired schedules which runs as a part of default + * handle process for the user transaction + */ +@Singleton +public class SchedulePurger { + private final ScheduleExpirationHook scheduleExpirationHook; + private final StoreMetricsService storeMetricsService; + + /** + * Constructs the SchedulePurger with the given dependencies. + * @param scheduleExpirationHook the schedule expiration hook + * @param storeMetricsService the store metrics service + */ + @Inject + public SchedulePurger( + @NonNull final ScheduleExpirationHook scheduleExpirationHook, + @NonNull final StoreMetricsService storeMetricsService) { + this.scheduleExpirationHook = scheduleExpirationHook; + this.storeMetricsService = storeMetricsService; + } + + /** + * Expire schedules that are due to be executed between the last handled transaction time and the current consensus + * time. + * @param userTxnContext the user transaction component + */ + public void expireSchedules(@NonNull UserTransactionComponent userTxnContext) { + final var lastHandledTxnTime = userTxnContext.lastHandledConsensusTime(); + if (lastHandledTxnTime == Instant.EPOCH) { + return; + } + if (userTxnContext.consensusNow().getEpochSecond() > lastHandledTxnTime.getEpochSecond()) { + final var firstSecondToExpire = lastHandledTxnTime.getEpochSecond(); + final var lastSecondToExpire = userTxnContext.consensusNow().getEpochSecond() - 1; + final var scheduleStore = new WritableStoreFactory( + userTxnContext.stack(), + ScheduleService.NAME, + userTxnContext.configuration(), + storeMetricsService) + .getStore(WritableScheduleStore.class); + // purge all expired schedules between the first consensus time of last block and the current consensus time + scheduleExpirationHook.processExpiredSchedules(scheduleStore, firstSecondToExpire, lastSecondToExpire); + // commit the stack + userTxnContext.stack().commitFullStack(); + } + } +} diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/flow/dispatch/child/logic/ChildDispatchFactoryTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/flow/dispatch/child/logic/ChildDispatchFactoryTest.java new file mode 100644 index 000000000000..ca60c3c15fa4 --- /dev/null +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/flow/dispatch/child/logic/ChildDispatchFactoryTest.java @@ -0,0 +1,213 @@ +/* + * Copyright (C) 2024 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.node.app.workflows.handle.flow.dispatch.child.logic; + +import static com.hedera.hapi.node.base.ResponseCodeEnum.OK; +import static com.hedera.hapi.node.base.ResponseCodeEnum.PAYER_ACCOUNT_DELETED; +import static com.hedera.node.app.workflows.handle.flow.dispatch.child.logic.ChildRecordBuilderFactoryTest.asTxn; +import static com.hedera.node.app.workflows.handle.flow.dispatch.child.logic.ChildTxnInfoFactoryTest.consensusTime; +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 org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willThrow; +import static org.mockito.Mockito.verify; + +import com.hedera.hapi.node.base.AccountID; +import com.hedera.hapi.node.base.Key; +import com.hedera.hapi.node.base.NftTransfer; +import com.hedera.hapi.node.base.TokenID; +import com.hedera.hapi.node.base.TokenTransferList; +import com.hedera.hapi.node.state.token.Account; +import com.hedera.hapi.node.token.CryptoTransferTransactionBody; +import com.hedera.hapi.node.transaction.TransactionBody; +import com.hedera.node.app.service.token.ReadableAccountStore; +import com.hedera.node.app.spi.workflows.ComputeDispatchFeesAsTopLevel; +import com.hedera.node.app.spi.workflows.HandleContext; +import com.hedera.node.app.spi.workflows.PreCheckException; +import com.hedera.node.app.spi.workflows.record.ExternalizedRecordCustomizer; +import com.hedera.node.app.state.WrappedHederaState; +import com.hedera.node.app.workflows.dispatcher.ReadableStoreFactory; +import com.hedera.node.app.workflows.dispatcher.TransactionDispatcher; +import com.hedera.node.app.workflows.handle.flow.dispatch.Dispatch; +import com.hedera.node.app.workflows.handle.flow.dispatch.child.ChildDispatchComponent; +import com.hedera.node.app.workflows.handle.record.RecordListBuilder; +import com.hedera.node.app.workflows.handle.record.SingleTransactionRecordBuilderImpl; +import com.hedera.node.app.workflows.handle.stack.SavepointStackImpl; +import com.hedera.node.app.workflows.prehandle.PreHandleResult; +import com.hedera.node.config.testfixtures.HederaTestConfigBuilder; +import com.swirlds.config.api.Configuration; +import java.util.Collections; +import java.util.function.Predicate; +import javax.inject.Provider; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class ChildDispatchFactoryTest { + @Mock + private TransactionDispatcher dispatcher; + + @Mock + private HandleContext handleContext; + + @Mock + private Dispatch parentDispatch; + + @Mock + private Provider childDispatchFactoryProvider; + + @Mock + private ChildDispatchComponent.Factory childDispatchFactory; + + @Mock + private ReadableStoreFactory readableStoreFactory; + + @Mock + private ReadableAccountStore accountStore; + + @Mock + private SavepointStackImpl savepointStack; + + private ChildDispatchFactory subject; + + private static final AccountID payerId = + AccountID.newBuilder().accountNum(1_234L).build(); + private static final CryptoTransferTransactionBody transferBody = CryptoTransferTransactionBody.newBuilder() + .tokenTransfers(TokenTransferList.newBuilder() + .token(TokenID.DEFAULT) + .nftTransfers(NftTransfer.newBuilder() + .receiverAccountID(AccountID.DEFAULT) + .senderAccountID(AccountID.DEFAULT) + .serialNumber(1) + .build()) + .build()) + .build(); + private static final TransactionBody txBody = asTxn(transferBody, payerId, consensusTime); + private final Configuration configuration = HederaTestConfigBuilder.createConfig(); + private final RecordListBuilder recordListBuilder = new RecordListBuilder(consensusTime); + + private final ChildTxnInfoFactory childTxnInfoFactory = new ChildTxnInfoFactory(); + private final ChildRecordBuilderFactory childRecordBuilderFactory = new ChildRecordBuilderFactory(); + + private final Predicate callback = key -> true; + private final HandleContext.TransactionCategory category = HandleContext.TransactionCategory.CHILD; + private final ExternalizedRecordCustomizer customizer = recordBuilder -> recordBuilder; + private final SingleTransactionRecordBuilderImpl.ReversingBehavior reversingBehavior = + SingleTransactionRecordBuilderImpl.ReversingBehavior.REMOVABLE; + + @BeforeEach + public void setUp() { + subject = new ChildDispatchFactory(childTxnInfoFactory, dispatcher, childRecordBuilderFactory); + + given(parentDispatch.handleContext()).willReturn(handleContext); + given(handleContext.configuration()).willReturn(configuration); + given(parentDispatch.readableStoreFactory()).willReturn(readableStoreFactory); + given(readableStoreFactory.getStore(ReadableAccountStore.class)).willReturn(accountStore); + given(accountStore.getAccountById(payerId)) + .willReturn(Account.newBuilder().key(Key.DEFAULT).build()); + given(parentDispatch.recordListBuilder()).willReturn(recordListBuilder); + given(parentDispatch.stack()).willReturn(savepointStack); + given(savepointStack.peek()).willReturn(new WrappedHederaState(savepointStack)); + given(childDispatchFactoryProvider.get()).willReturn(childDispatchFactory); + } + + @Test + void testCreateChildDispatch() throws PreCheckException { + // Create the child dispatch + subject.createChildDispatch( + parentDispatch, + txBody, + callback, + payerId, + category, + childDispatchFactoryProvider, + customizer, + reversingBehavior); + final var expectedPreHandleResult = new PreHandleResult( + null, + null, + SO_FAR_SO_GOOD, + OK, + null, + Collections.emptySet(), + null, + Collections.emptySet(), + null, + null, + 0); + + verify(dispatcher).dispatchPureChecks(txBody); + verify(dispatcher).dispatchPreHandle(any()); + + verify(childDispatchFactory) + .create( + any(), + any(), + eq(ComputeDispatchFeesAsTopLevel.NO), + eq(payerId), + eq(category), + any(), + eq(expectedPreHandleResult), + any()); + } + + @Test + void failsToCreateDispatchIfPreHandleException() throws PreCheckException { + willThrow(new PreCheckException(PAYER_ACCOUNT_DELETED)) + .given(dispatcher) + .dispatchPreHandle(any()); + subject.createChildDispatch( + parentDispatch, + txBody, + callback, + payerId, + category, + childDispatchFactoryProvider, + customizer, + reversingBehavior); + final var expectedPreHandleResult = new PreHandleResult( + null, + null, + PRE_HANDLE_FAILURE, + PAYER_ACCOUNT_DELETED, + null, + Collections.emptySet(), + null, + Collections.emptySet(), + null, + null, + 0); + verify(dispatcher).dispatchPureChecks(txBody); + verify(dispatcher).dispatchPreHandle(any()); + + verify(childDispatchFactory) + .create( + any(), + any(), + eq(ComputeDispatchFeesAsTopLevel.NO), + eq(payerId), + eq(category), + any(), + eq(expectedPreHandleResult), + any()); + } +} diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/flow/dispatch/child/logic/ChildRecordBuilderFactoryTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/flow/dispatch/child/logic/ChildRecordBuilderFactoryTest.java new file mode 100644 index 000000000000..94f529f957f9 --- /dev/null +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/flow/dispatch/child/logic/ChildRecordBuilderFactoryTest.java @@ -0,0 +1,215 @@ +/* + * Copyright (C) 2024 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.node.app.workflows.handle.flow.dispatch.child.logic; + +import static com.hedera.node.app.spi.workflows.HandleContext.TransactionCategory.CHILD; +import static com.hedera.node.app.spi.workflows.HandleContext.TransactionCategory.PRECEDING; +import static com.hedera.node.app.spi.workflows.HandleContext.TransactionCategory.SCHEDULED; +import static com.hedera.node.app.spi.workflows.HandleContext.TransactionCategory.USER; +import static com.hedera.node.app.spi.workflows.record.ExternalizedRecordCustomizer.NOOP_EXTERNALIZED_RECORD_CUSTOMIZER; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.hedera.hapi.node.base.AccountID; +import com.hedera.hapi.node.base.HederaFunctionality; +import com.hedera.hapi.node.base.NftTransfer; +import com.hedera.hapi.node.base.SignatureMap; +import com.hedera.hapi.node.base.Timestamp; +import com.hedera.hapi.node.base.TokenID; +import com.hedera.hapi.node.base.TokenTransferList; +import com.hedera.hapi.node.base.Transaction; +import com.hedera.hapi.node.base.TransactionID; +import com.hedera.hapi.node.token.CryptoTransferTransactionBody; +import com.hedera.hapi.node.transaction.TransactionBody; +import com.hedera.node.app.spi.workflows.record.ExternalizedRecordCustomizer; +import com.hedera.node.app.workflows.TransactionInfo; +import com.hedera.node.app.workflows.handle.record.RecordListBuilder; +import com.hedera.node.app.workflows.handle.record.SingleTransactionRecordBuilderImpl; +import com.hedera.node.config.testfixtures.HederaTestConfigBuilder; +import com.hedera.pbj.runtime.io.buffer.Bytes; +import com.swirlds.config.api.Configuration; +import java.time.Instant; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class ChildRecordBuilderFactoryTest { + private static final Instant consensusTime = Instant.ofEpochSecond(1_234_567L); + + private ChildRecordBuilderFactory factory; + private RecordListBuilder recordListBuilder; + private Configuration configuration; + private ExternalizedRecordCustomizer customizer; + + private static final AccountID payerId = + AccountID.newBuilder().accountNum(1_234L).build(); + private static final CryptoTransferTransactionBody transferBody = CryptoTransferTransactionBody.newBuilder() + .tokenTransfers(TokenTransferList.newBuilder() + .token(TokenID.DEFAULT) + .nftTransfers(NftTransfer.newBuilder() + .receiverAccountID(AccountID.DEFAULT) + .senderAccountID(AccountID.DEFAULT) + .serialNumber(1) + .build()) + .build()) + .build(); + private static final TransactionBody txBody = asTxn(transferBody, payerId, consensusTime); + private static final TransactionInfo txnInfo = new TransactionInfo( + Transaction.newBuilder().body(txBody).build(), + txBody, + SignatureMap.DEFAULT, + Bytes.EMPTY, + HederaFunctionality.CRYPTO_TRANSFER); + + @BeforeEach + void setUp() { + recordListBuilder = new RecordListBuilder(consensusTime); + configuration = HederaTestConfigBuilder.createConfig(); + customizer = NOOP_EXTERNALIZED_RECORD_CUSTOMIZER; + factory = new ChildRecordBuilderFactory(); + } + + @Test + void testRecordBuilderForPrecedingRemovable() { + var recordBuilder = factory.recordBuilderFor( + txnInfo, + recordListBuilder, + configuration, + PRECEDING, + SingleTransactionRecordBuilderImpl.ReversingBehavior.REMOVABLE, + customizer); + + assertNotNull(recordBuilder); + assertTrue(recordListBuilder.precedingRecordBuilders().contains(recordBuilder)); + } + + @Test + void testRecordBuilderForPrecedingReversible() { + var recordBuilder = factory.recordBuilderFor( + txnInfo, + recordListBuilder, + configuration, + PRECEDING, + SingleTransactionRecordBuilderImpl.ReversingBehavior.REVERSIBLE, + customizer); + + assertNotNull(recordBuilder); + assertTrue(recordListBuilder.precedingRecordBuilders().contains(recordBuilder)); + } + + @Test + void testRecordBuilderForChildRemovable() { + var recordBuilder = factory.recordBuilderFor( + txnInfo, + recordListBuilder, + configuration, + CHILD, + SingleTransactionRecordBuilderImpl.ReversingBehavior.REMOVABLE, + customizer); + + assertNotNull(recordBuilder); + assertTrue(recordListBuilder.childRecordBuilders().contains(recordBuilder)); + } + + @Test + void testRecordBuilderForChildReversible() { + var recordBuilder = factory.recordBuilderFor( + txnInfo, + recordListBuilder, + configuration, + CHILD, + SingleTransactionRecordBuilderImpl.ReversingBehavior.REVERSIBLE, + customizer); + + assertNotNull(recordBuilder); + assertTrue(recordListBuilder.childRecordBuilders().contains(recordBuilder)); + } + + @Test + void testRecordBuilderForUnsupportedReversingBehavior() { + assertThrows( + IllegalArgumentException.class, + () -> factory.recordBuilderFor( + txnInfo, + recordListBuilder, + configuration, + CHILD, + SingleTransactionRecordBuilderImpl.ReversingBehavior.IRREVERSIBLE, + customizer)); + } + + @Test + void testRecordBuilderForScheduled() { + var recordBuilder = factory.recordBuilderFor( + txnInfo, + recordListBuilder, + configuration, + SCHEDULED, + SingleTransactionRecordBuilderImpl.ReversingBehavior.REVERSIBLE, + customizer); + + assertNotNull(recordBuilder); + assertTrue(recordListBuilder.childRecordBuilders().contains(recordBuilder)); + } + + @Test + void testRecordBuilderForUser() { + assertThrows( + IllegalArgumentException.class, + () -> factory.recordBuilderFor( + txnInfo, + recordListBuilder, + configuration, + USER, + SingleTransactionRecordBuilderImpl.ReversingBehavior.IRREVERSIBLE, + customizer)); + } + + @Test + void testInitializeUserRecord() { + var recordBuilder = factory.recordBuilderFor( + txnInfo, + recordListBuilder, + configuration, + CHILD, + SingleTransactionRecordBuilderImpl.ReversingBehavior.REMOVABLE, + customizer); + + assertNotNull(recordBuilder); + assertEquals(txnInfo.transaction(), recordBuilder.transaction()); + assertEquals(txnInfo.txBody().transactionID(), recordBuilder.transactionID()); + assertEquals( + txnInfo.txBody().memo(), + recordBuilder.build().transaction().body().memo()); + assertEquals(txnInfo.signedBytes(), recordBuilder.build().transaction().signedTransactionBytes()); + } + + public static TransactionBody asTxn( + final CryptoTransferTransactionBody body, final AccountID payerId, Instant consensusTimestamp) { + return TransactionBody.newBuilder() + .transactionID(TransactionID.newBuilder() + .accountID(payerId) + .transactionValidStart(Timestamp.newBuilder() + .seconds(consensusTimestamp.getEpochSecond()) + .build()) + .build()) + .memo("test memo") + .cryptoTransfer(body) + .build(); + } +} diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/flow/dispatch/child/logic/ChildTxnInfoFactoryTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/flow/dispatch/child/logic/ChildTxnInfoFactoryTest.java new file mode 100644 index 000000000000..a0ca4377e713 --- /dev/null +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/flow/dispatch/child/logic/ChildTxnInfoFactoryTest.java @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2024 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.node.app.workflows.handle.flow.dispatch.child.logic; + +import static com.hedera.node.app.workflows.handle.flow.dispatch.child.logic.ChildRecordBuilderFactoryTest.asTxn; +import static org.junit.jupiter.api.Assertions.*; + +import com.hedera.hapi.node.base.*; +import com.hedera.hapi.node.token.CryptoTransferTransactionBody; +import com.hedera.hapi.node.transaction.SignedTransaction; +import com.hedera.hapi.node.transaction.TransactionBody; +import com.hedera.hapi.util.UnknownHederaFunctionality; +import com.hedera.node.app.workflows.TransactionInfo; +import com.hedera.pbj.runtime.io.buffer.Bytes; +import java.time.Instant; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class ChildTxnInfoFactoryTest { + static final Instant consensusTime = Instant.ofEpochSecond(1_234_567L); + private static final AccountID payerId = + AccountID.newBuilder().accountNum(1_234L).build(); + private static final CryptoTransferTransactionBody transferBody = CryptoTransferTransactionBody.newBuilder() + .tokenTransfers(TokenTransferList.newBuilder() + .token(TokenID.DEFAULT) + .nftTransfers(NftTransfer.newBuilder() + .receiverAccountID(AccountID.DEFAULT) + .senderAccountID(AccountID.DEFAULT) + .serialNumber(1) + .build()) + .build()) + .build(); + private static final TransactionBody txBody = asTxn(transferBody, payerId, consensusTime); + private static final TransactionInfo txnInfo = new TransactionInfo( + Transaction.newBuilder().body(txBody).build(), + txBody, + SignatureMap.DEFAULT, + Bytes.EMPTY, + HederaFunctionality.CRYPTO_TRANSFER); + + private ChildTxnInfoFactory subject; + + @BeforeEach + void setUp() { + subject = new ChildTxnInfoFactory(); + } + + @Test + void testGetTxnInfoFrom() { + var txnInfo = assertDoesNotThrow(() -> subject.getTxnInfoFrom(txBody)); + + assertNotNull(txnInfo); + assertEquals(txBody, txnInfo.txBody()); + assertEquals(TransactionID.DEFAULT, txnInfo.transactionID()); + assertEquals(AccountID.DEFAULT, txnInfo.payerID()); + assertEquals(SignatureMap.DEFAULT, txnInfo.signatureMap()); + + var bodyBytes = TransactionBody.PROTOBUF.toBytes(txBody); + var signedTransaction = + SignedTransaction.newBuilder().bodyBytes(bodyBytes).build(); + var signedTransactionBytes = SignedTransaction.PROTOBUF.toBytes(signedTransaction); + assertArrayEquals( + signedTransactionBytes.toByteArray(), txnInfo.signedBytes().toByteArray()); + + var transaction = Transaction.newBuilder() + .signedTransactionBytes(signedTransactionBytes) + .build(); + assertEquals(transaction, txnInfo.transaction()); + } + + @Test + void testFunctionOfTxnThrowsException() { + var txBody = TransactionBody.newBuilder() + .transactionID(TransactionID.newBuilder().build()) + .memo("Test Memo") + .build(); + Exception exception = assertThrows(IllegalArgumentException.class, () -> subject.getTxnInfoFrom(txBody)); + assertTrue(exception.getCause() instanceof UnknownHederaFunctionality); + assertEquals("Unknown Hedera Functionality", exception.getMessage()); + } +} diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/flow/dispatch/txn/logic/HollowAccountCompleterTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/flow/dispatch/txn/logic/HollowAccountCompleterTest.java new file mode 100644 index 000000000000..ebebabfc6aba --- /dev/null +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/flow/dispatch/txn/logic/HollowAccountCompleterTest.java @@ -0,0 +1,235 @@ +/* + * Copyright (C) 2024 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.node.app.workflows.handle.flow.dispatch.txn.logic; + +import static com.hedera.hapi.node.base.HederaFunctionality.ETHEREUM_TRANSACTION; +import static com.hedera.node.app.spi.key.KeyUtils.IMMUTABILITY_SENTINEL_KEY; +import static com.hedera.node.app.workflows.handle.flow.dispatch.child.logic.ChildRecordBuilderFactoryTest.asTxn; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mock.Strictness.LENIENT; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +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.NftTransfer; +import com.hedera.hapi.node.base.SignatureMap; +import com.hedera.hapi.node.base.TokenID; +import com.hedera.hapi.node.base.TokenTransferList; +import com.hedera.hapi.node.base.Transaction; +import com.hedera.hapi.node.base.TransactionID; +import com.hedera.hapi.node.contract.EthereumTransactionBody; +import com.hedera.hapi.node.state.token.Account; +import com.hedera.hapi.node.token.CryptoTransferTransactionBody; +import com.hedera.hapi.node.transaction.SignedTransaction; +import com.hedera.hapi.node.transaction.TransactionBody; +import com.hedera.node.app.service.token.ReadableAccountStore; +import com.hedera.node.app.service.token.records.CryptoUpdateRecordBuilder; +import com.hedera.node.app.signature.KeyVerifier; +import com.hedera.node.app.signature.impl.SignatureVerificationImpl; +import com.hedera.node.app.spi.signatures.SignatureVerification; +import com.hedera.node.app.spi.workflows.HandleContext; +import com.hedera.node.app.workflows.TransactionInfo; +import com.hedera.node.app.workflows.dispatcher.ReadableStoreFactory; +import com.hedera.node.app.workflows.handle.flow.dispatch.Dispatch; +import com.hedera.node.app.workflows.handle.flow.txn.UserTransactionComponent; +import com.hedera.node.app.workflows.handle.flow.txn.logic.HollowAccountCompleter; +import com.hedera.node.app.workflows.handle.record.RecordListBuilder; +import com.hedera.node.app.workflows.handle.record.SingleTransactionRecordBuilderImpl; +import com.hedera.node.app.workflows.prehandle.PreHandleResult; +import com.hedera.node.config.testfixtures.HederaTestConfigBuilder; +import com.hedera.pbj.runtime.io.buffer.Bytes; +import com.swirlds.config.api.Configuration; +import java.time.Instant; +import java.util.Collections; +import java.util.Set; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class HollowAccountCompleterTest { + @Mock(strictness = LENIENT) + private Dispatch dispatch; + + @Mock(strictness = LENIENT) + private HandleContext handleContext; + + @Mock(strictness = LENIENT) + private UserTransactionComponent userTxn; + + @Mock(strictness = LENIENT) + private ReadableAccountStore accountStore; + + @Mock(strictness = LENIENT) + private KeyVerifier keyVerifier; + + @Mock(strictness = LENIENT) + private ReadableStoreFactory readableStoreFactory; + + @Mock(strictness = LENIENT) + private PreHandleResult preHandleResult; + + @Mock(strictness = LENIENT) + private SingleTransactionRecordBuilderImpl recordBuilder; + + private Configuration configuration = HederaTestConfigBuilder.createConfig(); + private static final Instant consensusTime = Instant.ofEpochSecond(1_234_567L); + private static final RecordListBuilder recordListBuilder = new RecordListBuilder(consensusTime); + private static final AccountID payerId = + AccountID.newBuilder().accountNum(1_234L).build(); + private static final CryptoTransferTransactionBody transferBody = CryptoTransferTransactionBody.newBuilder() + .tokenTransfers(TokenTransferList.newBuilder() + .token(TokenID.DEFAULT) + .nftTransfers(NftTransfer.newBuilder() + .receiverAccountID(AccountID.DEFAULT) + .senderAccountID(AccountID.DEFAULT) + .serialNumber(1) + .build()) + .build()) + .build(); + private static final TransactionBody txBody = asTxn(transferBody, payerId, consensusTime); + private static final SignedTransaction transaction = SignedTransaction.newBuilder() + .bodyBytes(TransactionBody.PROTOBUF.toBytes(txBody)) + .build(); + private static final Bytes transactionBytes = SignedTransaction.PROTOBUF.toBytes(transaction); + final TransactionInfo txnInfo = new TransactionInfo( + Transaction.newBuilder().body(txBody).build(), + txBody, + SignatureMap.DEFAULT, + transactionBytes, + HederaFunctionality.CRYPTO_TRANSFER); + + @InjectMocks + private HollowAccountCompleter hollowAccountCompleter; + + @BeforeEach + void setUp() { + when(dispatch.handleContext()).thenReturn(handleContext); + when(dispatch.keyVerifier()).thenReturn(keyVerifier); + when(handleContext.payer()).thenReturn(payerId); + // when(handleContext.configuration()).thenReturn(configuration); + when(userTxn.recordListBuilder()).thenReturn(recordListBuilder); + when(userTxn.configuration()).thenReturn(configuration); + // when(userTxn.txnInfo()).thenReturn(txnInfo); + when(userTxn.readableStoreFactory()).thenReturn(readableStoreFactory); + when(userTxn.readableStoreFactory().getStore(ReadableAccountStore.class)) + .thenReturn(accountStore); + when(userTxn.preHandleResult()).thenReturn(preHandleResult); + when(handleContext.dispatchPrecedingTransaction(any(), any(), any(), any())) + .thenReturn(recordBuilder); + } + + @Test + void finalizeHollowAccountsNoHollowAccounts() { + when(userTxn.preHandleResult().getHollowAccounts()).thenReturn(Collections.emptySet()); + + hollowAccountCompleter.finalizeHollowAccounts(userTxn, dispatch); + + verifyNoInteractions(keyVerifier); + verifyNoInteractions(handleContext); + } + + @Test + void doesntFinalizeHollowAccountsWithNoImmutabilitySentinelKey() { + final var hollowAccount = Account.newBuilder() + .accountId(AccountID.newBuilder().accountNum(1).build()) + .key(Key.DEFAULT) + .alias(Bytes.wrap(new byte[] {1, 2, 3})) + .build(); + when(userTxn.preHandleResult().getHollowAccounts()).thenReturn(Collections.singleton(hollowAccount)); + SignatureVerification verification = + new SignatureVerificationImpl(Key.DEFAULT, Bytes.wrap(new byte[] {1, 2, 3}), true); + when(keyVerifier.verificationFor(Bytes.wrap(new byte[] {1, 2, 3}))).thenReturn(verification); + when(userTxn.preHandleResult().getHollowAccounts()).thenReturn(Set.of(hollowAccount)); + + hollowAccountCompleter.finalizeHollowAccounts(userTxn, dispatch); + + verify(keyVerifier).verificationFor(Bytes.wrap(new byte[] {1, 2, 3})); + verify(handleContext, never()) + .dispatchPrecedingTransaction( + eq(txBody), eq(CryptoUpdateRecordBuilder.class), isNull(), eq(AccountID.DEFAULT)); + } + + @Test + void finalizeHollowAccountsWithHollowAccounts() { + final var hollowAccount = Account.newBuilder() + .accountId(AccountID.newBuilder().accountNum(1).build()) + .key(IMMUTABILITY_SENTINEL_KEY) + .alias(Bytes.wrap(new byte[] {1, 2, 3})) + .build(); + when(userTxn.preHandleResult().getHollowAccounts()).thenReturn(Collections.singleton(hollowAccount)); + SignatureVerification verification = + new SignatureVerificationImpl(Key.DEFAULT, Bytes.wrap(new byte[] {1, 2, 3}), true); + when(keyVerifier.verificationFor(Bytes.wrap(new byte[] {1, 2, 3}))).thenReturn(verification); + when(userTxn.preHandleResult().getHollowAccounts()).thenReturn(Set.of(hollowAccount)); + + hollowAccountCompleter.finalizeHollowAccounts(userTxn, dispatch); + + verify(keyVerifier).verificationFor(Bytes.wrap(new byte[] {1, 2, 3})); + verify(handleContext).dispatchPrecedingTransaction(any(), any(), any(), any()); + verify(recordBuilder).accountID(AccountID.newBuilder().accountNum(1).build()); + } + + @Test + void finalizeHollowAccountsWithEthereumTransaction() { + when(userTxn.functionality()).thenReturn(ETHEREUM_TRANSACTION); + + final var hollowAccount = Account.newBuilder() + .accountId(AccountID.newBuilder().accountNum(1).build()) + .key(IMMUTABILITY_SENTINEL_KEY) + .alias(Bytes.wrap(new byte[] {1, 2, 3})) + .build(); + final var txnBody = TransactionBody.newBuilder() + .transactionID(TransactionID.newBuilder() + .accountID(AccountID.newBuilder().accountNum(1).build()) + .build()) + .ethereumTransaction(EthereumTransactionBody.DEFAULT) + .build(); + final TransactionInfo txnInfo = new TransactionInfo( + Transaction.newBuilder().body(txnBody).build(), + txnBody, + SignatureMap.DEFAULT, + transactionBytes, + ETHEREUM_TRANSACTION); + + SignatureVerification ethVerification = + new SignatureVerificationImpl(Key.DEFAULT, Bytes.wrap(new byte[] {1, 2, 3}), true); + when(userTxn.preHandleResult().getHollowAccounts()).thenReturn(Set.of(hollowAccount)); + when(userTxn.readableStoreFactory().getStore(ReadableAccountStore.class)) + .thenReturn(accountStore); + when(accountStore.getAccountIDByAlias(Bytes.wrap(new byte[] {1, 2, 3}))).thenReturn(AccountID.DEFAULT); + when(accountStore.getAccountById(AccountID.DEFAULT)).thenReturn(hollowAccount); + when(userTxn.configuration()).thenReturn(configuration); + when(userTxn.recordListBuilder()).thenReturn(recordListBuilder); + when(userTxn.txnInfo()).thenReturn(txnInfo); + when(keyVerifier.verificationFor(Bytes.wrap(new byte[] {1, 2, 3}))).thenReturn(ethVerification); + + hollowAccountCompleter.finalizeHollowAccounts(userTxn, dispatch); + + verify(handleContext).dispatchPrecedingTransaction(any(), any(), any(), any()); + verify(recordBuilder).accountID(AccountID.newBuilder().accountNum(1).build()); + } +} diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/flow/dispatch/txn/logic/SchedulePurgerTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/flow/dispatch/txn/logic/SchedulePurgerTest.java new file mode 100644 index 000000000000..1f375c665b00 --- /dev/null +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/flow/dispatch/txn/logic/SchedulePurgerTest.java @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2024 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.node.app.workflows.handle.flow.dispatch.txn.logic; + +import static org.mockito.Mock.Strictness.LENIENT; +import static org.mockito.Mockito.*; + +import com.hedera.node.app.service.schedule.WritableScheduleStore; +import com.hedera.node.app.service.token.records.TokenContext; +import com.hedera.node.app.spi.metrics.StoreMetricsService; +import com.hedera.node.app.workflows.dispatcher.WritableStoreFactory; +import com.hedera.node.app.workflows.handle.ScheduleExpirationHook; +import com.hedera.node.app.workflows.handle.flow.txn.UserTransactionComponent; +import com.hedera.node.app.workflows.handle.flow.txn.logic.SchedulePurger; +import com.hedera.node.app.workflows.handle.stack.SavepointStackImpl; +import com.hedera.node.config.testfixtures.HederaTestConfigBuilder; +import com.swirlds.config.api.Configuration; +import java.time.Instant; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedConstruction; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class SchedulePurgerTest { + @Mock + private StoreMetricsService storeMetricsService; + + @Mock(strictness = LENIENT) + private UserTransactionComponent userTxnContext; + + @Mock + private WritableScheduleStore writableScheduleStore; + + @Mock + private SavepointStackImpl savepointStack; + + @Mock(strictness = LENIENT) + private TokenContext tokenContext; + + private Configuration configuration = HederaTestConfigBuilder.createConfig(); + private ScheduleExpirationHook scheduleExpirationHook = new ScheduleExpirationHook(); + + @InjectMocks + private SchedulePurger subject; + + @BeforeEach + void setUp() { + subject = new SchedulePurger(scheduleExpirationHook, storeMetricsService); + when(userTxnContext.stack()).thenReturn(savepointStack); + when(userTxnContext.tokenContext()).thenReturn(tokenContext); + when(tokenContext.configuration()).thenReturn(configuration); + } + + @Test + void expireSchedulesWhenLastHandledTxnTimeIsEpoch() { + when(userTxnContext.lastHandledConsensusTime()).thenReturn(Instant.EPOCH); + + try (MockedConstruction mocked = + Mockito.mockConstruction(WritableStoreFactory.class, (mock, context) -> { + when(mock.getStore(WritableScheduleStore.class)).thenReturn(writableScheduleStore); + })) { + + subject.expireSchedules(userTxnContext); + + verifyNoInteractions(writableScheduleStore); + verifyNoInteractions(storeMetricsService); + } + } + + @Test + void expireSchedulesWhenCurrentConsensusTimeGreaterThanLastHandledTime() { + Instant lastHandledTime = Instant.ofEpochSecond(1000); + Instant consensusNow = Instant.ofEpochSecond(2000); + + when(userTxnContext.lastHandledConsensusTime()).thenReturn(lastHandledTime); + when(userTxnContext.consensusNow()).thenReturn(consensusNow); + try (MockedConstruction mocked = + Mockito.mockConstruction(WritableStoreFactory.class, (mock, context) -> { + when(mock.getStore(WritableScheduleStore.class)).thenReturn(writableScheduleStore); + })) { + + subject.expireSchedules(userTxnContext); + + verify(writableScheduleStore).purgeExpiredSchedulesBetween(1000, 1999); + verify(userTxnContext.stack()).commitFullStack(); + } + } + + @Test + void expireSchedulesWhenCurrentConsensusTimeNotGreaterThanLastHandledTime() { + Instant lastHandledTime = Instant.ofEpochSecond(2000); + Instant consensusNow = Instant.ofEpochSecond(1000); + + when(userTxnContext.lastHandledConsensusTime()).thenReturn(lastHandledTime); + when(userTxnContext.consensusNow()).thenReturn(consensusNow); + + try (MockedConstruction mocked = + Mockito.mockConstruction(WritableStoreFactory.class, (mock, context) -> { + when(mock.getStore(WritableScheduleStore.class)).thenReturn(writableScheduleStore); + })) { + + subject.expireSchedules(userTxnContext); + + verifyNoInteractions(writableScheduleStore); + verifyNoInteractions(storeMetricsService); + } + } +} diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/flow/dispatch/user/logic/PreHandleResultManagerTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/flow/dispatch/user/logic/PreHandleResultManagerTest.java new file mode 100644 index 000000000000..05fd762e066e --- /dev/null +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/flow/dispatch/user/logic/PreHandleResultManagerTest.java @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2024 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.node.app.workflows.handle.flow.dispatch.user.logic; + +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.hedera.hapi.node.base.AccountID; +import com.hedera.node.app.info.NodeInfoImpl; +import com.hedera.node.app.service.token.ReadableAccountStore; +import com.hedera.node.app.workflows.SolvencyPreCheck; +import com.hedera.node.app.workflows.dispatcher.ReadableStoreFactory; +import com.hedera.node.app.workflows.prehandle.PreHandleResult; +import com.hedera.node.app.workflows.prehandle.PreHandleWorkflow; +import com.swirlds.platform.system.transaction.ConsensusTransactionImpl; +import com.swirlds.state.spi.info.NodeInfo; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class PreHandleResultManagerTest { + @Mock + private PreHandleWorkflow preHandleWorkflow; + + @Mock + private SolvencyPreCheck solvencyPreCheck; + + @Mock + private ConsensusTransactionImpl platformTxn; + + @Mock + private ReadableStoreFactory storeFactory; + + @Mock + private ReadableAccountStore accountStore; + + @Mock + private PreHandleResult previousResult; + + private NodeInfo creator = + new NodeInfoImpl(0L, AccountID.newBuilder().accountNum(3).build(), 500, "", 50006, "", ""); + + @InjectMocks + private PreHandleResultManager subject; + + @BeforeEach + void setUp() { + subject = new PreHandleResultManager(preHandleWorkflow, solvencyPreCheck); + when(storeFactory.getStore(ReadableAccountStore.class)).thenReturn(accountStore); + } + + @Test + void getCurrentPreHandleResultWithPreviousResult() { + given(platformTxn.getMetadata()).willReturn(previousResult); + final var result = subject.getCurrentPreHandleResult(creator, platformTxn, storeFactory); + verify(preHandleWorkflow) + .preHandleTransaction( + eq(creator.accountId()), + eq(storeFactory), + eq(accountStore), + eq(platformTxn), + eq(previousResult)); + } + + @Test + void getCurrentPreHandleResultWithoutPreviousResult() { + given(platformTxn.getMetadata()).willReturn(null); + PreHandleResult result = subject.getCurrentPreHandleResult(creator, platformTxn, storeFactory); + verify(preHandleWorkflow, times(1)) + .preHandleTransaction( + eq(creator.accountId()), eq(storeFactory), eq(accountStore), eq(platformTxn), eq(null)); + } + + @Test + void getCurrentPreHandleResultMetadataNotPreHandleResult() { + Object wrongMetadata = new Object(); + given(platformTxn.getMetadata()).willReturn(wrongMetadata); + + PreHandleResult result = subject.getCurrentPreHandleResult(creator, platformTxn, storeFactory); + verify(preHandleWorkflow, times(1)) + .preHandleTransaction( + eq(creator.accountId()), eq(storeFactory), eq(accountStore), eq(platformTxn), eq(null)); + } +} diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/flow/dispatch/user/logic/UserRecordInitializerTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/flow/dispatch/user/logic/UserRecordInitializerTest.java new file mode 100644 index 000000000000..7b5a34c9ddd8 --- /dev/null +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/flow/dispatch/user/logic/UserRecordInitializerTest.java @@ -0,0 +1,156 @@ +/* + * Copyright (C) 2024 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.node.app.workflows.handle.flow.dispatch.user.logic; + +import static com.hedera.node.app.workflows.handle.flow.dispatch.child.logic.ChildRecordBuilderFactoryTest.asTxn; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.BDDMockito.given; + +import com.hedera.hapi.node.base.AccountID; +import com.hedera.hapi.node.base.HederaFunctionality; +import com.hedera.hapi.node.base.NftTransfer; +import com.hedera.hapi.node.base.SignatureMap; +import com.hedera.hapi.node.base.Timestamp; +import com.hedera.hapi.node.base.TokenID; +import com.hedera.hapi.node.base.TokenTransferList; +import com.hedera.hapi.node.base.Transaction; +import com.hedera.hapi.node.base.TransactionID; +import com.hedera.hapi.node.token.CryptoTransferTransactionBody; +import com.hedera.hapi.node.transaction.ExchangeRateSet; +import com.hedera.hapi.node.transaction.SignedTransaction; +import com.hedera.hapi.node.transaction.TransactionBody; +import com.hedera.node.app.fees.ExchangeRateManager; +import com.hedera.node.app.spi.workflows.record.ExternalizedRecordCustomizer; +import com.hedera.node.app.workflows.TransactionInfo; +import com.hedera.node.app.workflows.handle.record.SingleTransactionRecordBuilderImpl; +import com.hedera.pbj.runtime.io.buffer.Bytes; +import java.time.Instant; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class UserRecordInitializerTest { + private static final Instant consensusTime = Instant.ofEpochSecond(1_234_567L); + + private SingleTransactionRecordBuilderImpl recordBuilder = new SingleTransactionRecordBuilderImpl( + consensusTime, + SingleTransactionRecordBuilderImpl.ReversingBehavior.IRREVERSIBLE, + ExternalizedRecordCustomizer.NOOP_EXTERNALIZED_RECORD_CUSTOMIZER); + + private static final AccountID payerId = + AccountID.newBuilder().accountNum(1_234L).build(); + private static final CryptoTransferTransactionBody transferBody = CryptoTransferTransactionBody.newBuilder() + .tokenTransfers(TokenTransferList.newBuilder() + .token(TokenID.DEFAULT) + .nftTransfers(NftTransfer.newBuilder() + .receiverAccountID(AccountID.DEFAULT) + .senderAccountID(AccountID.DEFAULT) + .serialNumber(1) + .build()) + .build()) + .build(); + private static final TransactionBody txBody = asTxn(transferBody, payerId, consensusTime); + private static final SignedTransaction transaction = SignedTransaction.newBuilder() + .bodyBytes(TransactionBody.PROTOBUF.toBytes(txBody)) + .build(); + private static final Bytes transactionBytes = SignedTransaction.PROTOBUF.toBytes(transaction); + + @Mock + private ExchangeRateManager exchangeRateManager; + + @Mock + private ExchangeRateSet exchangeRateSet; + + @InjectMocks + private UserRecordInitializer subject; + + @BeforeEach + void setUp() { + subject = new UserRecordInitializer(exchangeRateManager); + given(exchangeRateManager.exchangeRates()).willReturn(exchangeRateSet); + } + + @Test + void initializeUserRecordWithSignedTransactionBytes() { + final TransactionInfo txnInfo = new TransactionInfo( + Transaction.newBuilder().body(txBody).build(), + txBody, + SignatureMap.DEFAULT, + transactionBytes, + HederaFunctionality.CRYPTO_TRANSFER); + + assertDoesNotThrow(() -> subject.initializeUserRecord(recordBuilder, txnInfo)); + + assertEquals(Transaction.newBuilder().body(txBody).build(), recordBuilder.transaction()); + assertEquals( + TransactionID.newBuilder() + .accountID(payerId) + .transactionValidStart(Timestamp.newBuilder() + .seconds(consensusTime.getEpochSecond()) + .build()) + .build(), + recordBuilder.transactionID()); + assertEquals(exchangeRateManager.exchangeRates(), recordBuilder.exchangeRate()); + } + + @Test + void initializeUserRecordWithoutSignedTransactionBytes() { + final TransactionInfo txnInfo = new TransactionInfo( + Transaction.newBuilder().body(txBody).build(), + txBody, + SignatureMap.DEFAULT, + Bytes.EMPTY, + HederaFunctionality.CRYPTO_TRANSFER); + + assertDoesNotThrow(() -> subject.initializeUserRecord(recordBuilder, txnInfo)); + + assertEquals(Transaction.newBuilder().body(txBody).build(), recordBuilder.transaction()); + assertEquals( + TransactionID.newBuilder() + .accountID(payerId) + .transactionValidStart(Timestamp.newBuilder() + .seconds(consensusTime.getEpochSecond()) + .build()) + .build(), + recordBuilder.transactionID()); + assertEquals(exchangeRateManager.exchangeRates(), recordBuilder.exchangeRate()); + } + + @Test + void initializeUserRecordWithoutTransaction() { + final TransactionInfo txnInfo = new TransactionInfo( + Transaction.DEFAULT, txBody, SignatureMap.DEFAULT, Bytes.EMPTY, HederaFunctionality.CRYPTO_TRANSFER); + + assertDoesNotThrow(() -> subject.initializeUserRecord(recordBuilder, txnInfo)); + + assertEquals(Transaction.DEFAULT, recordBuilder.transaction()); + assertEquals( + TransactionID.newBuilder() + .accountID(payerId) + .transactionValidStart(Timestamp.newBuilder() + .seconds(consensusTime.getEpochSecond()) + .build()) + .build(), + recordBuilder.transactionID()); + assertEquals(exchangeRateManager.exchangeRates(), recordBuilder.exchangeRate()); + } +}