Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Validations for dispatchSyntheticTxn #9308

Merged
merged 34 commits into from
Nov 8, 2023
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
8482c26
feature: add validations to HandleContextImpl.dispatchSyntheticTxn
mustafauzunn Oct 16, 2023
437e7ba
style: formatting
mustafauzunn Oct 16, 2023
a22468b
feature: change validations
mustafauzunn Oct 16, 2023
abc59ac
feature: remove unused parameter
mustafauzunn Oct 16, 2023
9891413
style: formatting
mustafauzunn Oct 16, 2023
3565d1e
feature: improve response from catch
mustafauzunn Oct 19, 2023
f262b98
style: formatting
mustafauzunn Oct 19, 2023
11f96e6
refactor: apply PR comments
mustafauzunn Oct 19, 2023
581ee2c
style: formatting
mustafauzunn Oct 19, 2023
5f2a447
tests: fix failing unit tests
Oct 19, 2023
f7d3076
Merge branch 'develop' of github.com:hashgraph/hedera-services into 0…
mustafauzunn Oct 19, 2023
795583c
Merge branch 'develop' of github.com:hashgraph/hedera-services into 0…
mustafauzunn Oct 20, 2023
802b22c
Merge branch 'develop' into 09213-validations-dispatchSyntheticTxn
stoyanov-st Oct 23, 2023
04c3134
feat: skip payer key check with dispatched child transaction
stoyanov-st Oct 23, 2023
9f2e8ac
test: fix tests
stoyanov-st Oct 23, 2023
063a851
fix: correct if condition
stoyanov-st Oct 23, 2023
a69cf1f
Merge branch 'develop' into 09213-validations-dispatchSyntheticTxn
stoyanov-st Oct 24, 2023
de7603f
fix: post merge conflict
stoyanov-st Oct 24, 2023
7388d97
refactor: return only responseCode
stoyanov-st Oct 24, 2023
145eb26
refactor: keep this keyword
stoyanov-st Oct 24, 2023
97d9f9a
Merge branch 'develop' into 09213-validations-dispatchSyntheticTxn
stoyanov-st Oct 25, 2023
1400292
Merge branch 'develop' into 09213-validations-dispatchSyntheticTxn
stoyanov-st Oct 26, 2023
e87b568
feat: include duplication, solvency and timebox checks
stoyanov-st Oct 26, 2023
f74d6e0
Merge branch 'develop' into 09213-validations-dispatchSyntheticTxn
stoyanov-st Oct 27, 2023
cb66a35
Merge branch 'develop' into 09213-validations-dispatchSyntheticTxn
stoyanov-st Nov 1, 2023
46cd0e3
fix: post merge side effects
stoyanov-st Nov 1, 2023
25a3da7
Merge branch 'develop' into 09213-validations-dispatchSyntheticTxn
stoyanov-st Nov 3, 2023
0f8b13c
refactor: address PR comments
stoyanov-st Nov 3, 2023
5e5c6f5
test: fix test
stoyanov-st Nov 6, 2023
f99dc5e
Merge branch 'develop' into 09213-validations-dispatchSyntheticTxn
stoyanov-st Nov 7, 2023
0c92f47
test: fix another test
stoyanov-st Nov 7, 2023
78cd13e
Merge branch 'develop' into 09213-validations-dispatchSyntheticTxn
stoyanov-st Nov 8, 2023
6021c54
fix: pass correct function to validator
stoyanov-st Nov 8, 2023
d7f7847
fix failing HAPI tests
Neeharika-Sompalli Nov 8, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import com.hedera.hapi.node.base.ResponseCodeEnum;
import com.hedera.hapi.node.base.SubType;
import com.hedera.hapi.node.state.token.Account;
import com.hedera.hapi.node.transaction.TransactionBody;
import com.hedera.node.app.fees.ExchangeRateManager;
import com.hedera.node.app.fees.FeeManager;
import com.hedera.node.app.service.token.ReadableAccountStore;
Expand Down Expand Up @@ -123,14 +124,27 @@ public void checkSolvency(
// FUTURE ('#9550')
final boolean ingestCheck)
throws PreCheckException {
checkSolvency(txInfo.txBody(), txInfo.payerID(), txInfo.functionality(), account, fees, ingestCheck);
}

public void checkSolvency(
@NonNull final TransactionBody txBody,
@NonNull final AccountID payerID,
@NonNull final HederaFunctionality functionality,
@NonNull final Account account,
@NonNull final Fees fees,
// This is to match mono and pass HapiTest. Should reconsider later.
// FUTURE ('#9550')
final boolean ingestCheck)
throws PreCheckException {
// Skip solvency check for privileged transactions or superusers
if (authorizer.hasWaivedFees(txInfo.payerID(), txInfo.functionality(), txInfo.txBody())) {
if (authorizer.hasWaivedFees(payerID, functionality, txBody)) {
return;
}

final var totalFee = fees.totalFee();
final var availableBalance = account.tinybarBalance();
final var offeredFee = txInfo.txBody().transactionFee();
final var offeredFee = txBody.transactionFee();
final ResponseCodeEnum insufficientFeeResponseCode;
if (ingestCheck) { // throw different exception for ingest
insufficientFeeResponseCode = INSUFFICIENT_PAYER_BALANCE;
Expand All @@ -154,9 +168,9 @@ public void checkSolvency(

final long additionalCosts;
try {
final var now = txInfo.txBody().transactionIDOrThrow().transactionValidStartOrThrow();
additionalCosts = Math.max(0, estimateAdditionalCosts(txInfo, HapiUtils.asInstant(now)));
} catch (NullPointerException ex) {
final var now = txBody.transactionIDOrThrow().transactionValidStartOrThrow();
additionalCosts = Math.max(0, estimateAdditionalCosts(txBody, functionality, HapiUtils.asInstant(now)));
} catch (final NullPointerException ex) {
// One of the required fields was not present
throw new InsufficientBalanceException(INVALID_TRANSACTION_BODY, totalFee);
}
Expand All @@ -170,31 +184,34 @@ public void checkSolvency(

// FUTURE: This should be provided by the TransactionHandler:
// https://github.com/hashgraph/hedera-services/issues/8354
private long estimateAdditionalCosts(@NonNull final TransactionInfo txInfo, @NonNull final Instant consensusTime) {
return switch (txInfo.functionality()) {
case CRYPTO_CREATE -> txInfo.txBody().cryptoCreateAccountOrThrow().initialBalance();
private long estimateAdditionalCosts(
@NonNull final TransactionBody txBody,
@NonNull final HederaFunctionality functionality,
@NonNull final Instant consensusTime) {
return switch (functionality) {
case CRYPTO_CREATE -> txBody.cryptoCreateAccountOrThrow().initialBalance();
case CRYPTO_TRANSFER -> {
if (!txInfo.txBody().cryptoTransferOrThrow().hasTransfers()) {
if (!txBody.cryptoTransferOrThrow().hasTransfers()) {
yield 0L;
}
final var payerID = txInfo.txBody().transactionIDOrThrow().accountIDOrThrow();
yield -txInfo.txBody().cryptoTransferOrThrow().transfersOrThrow().accountAmountsOrThrow().stream()
final var payerID = txBody.transactionIDOrThrow().accountIDOrThrow();
yield -txBody.cryptoTransferOrThrow().transfersOrThrow().accountAmountsOrThrow().stream()
.filter(aa -> Objects.equals(aa.accountID(), payerID))
.mapToLong(AccountAmount::amount)
.sum();
}
case CONTRACT_CREATE -> {
final var contractCreate = txInfo.txBody().contractCreateInstanceOrThrow();
final var contractCreate = txBody.contractCreateInstanceOrThrow();
yield contractCreate.initialBalance()
+ contractCreate.gas() * estimatedGasPriceInTinybars(CONTRACT_CREATE, consensusTime);
}
case CONTRACT_CALL -> {
final var contractCall = txInfo.txBody().contractCallOrThrow();
final var contractCall = txBody.contractCallOrThrow();
yield contractCall.amount()
+ contractCall.gas() * estimatedGasPriceInTinybars(CONTRACT_CALL, consensusTime);
}
case ETHEREUM_TRANSACTION -> {
final var ethTxn = txInfo.txBody().ethereumTransactionOrThrow();
final var ethTxn = txBody.ethereumTransactionOrThrow();
yield ethTxn.maxGasAllowance();
}
default -> 0L;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,13 @@

package com.hedera.node.app.workflows.handle;

import static com.hedera.hapi.node.base.ResponseCodeEnum.DUPLICATE_TRANSACTION;
import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_SIGNATURE;
import static com.hedera.node.app.spi.HapiUtils.functionOf;
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.state.HederaRecordCache.DuplicateCheckResult.NO_DUPLICATE;
import static com.hedera.node.app.state.HederaRecordCache.DuplicateCheckResult.SAME_NODE;
import static java.util.Objects.requireNonNull;

import com.hedera.hapi.node.base.AccountID;
Expand Down Expand Up @@ -62,9 +66,13 @@
import com.hedera.node.app.spi.workflows.FunctionalityResourcePrices;
import com.hedera.node.app.spi.workflows.HandleContext;
import com.hedera.node.app.spi.workflows.HandleException;
import com.hedera.node.app.spi.workflows.InsufficientNonFeeDebitsException;
import com.hedera.node.app.spi.workflows.InsufficientServiceFeeException;
import com.hedera.node.app.spi.workflows.PreCheckException;
import com.hedera.node.app.spi.workflows.TransactionKeys;
import com.hedera.node.app.state.HederaRecordCache;
import com.hedera.node.app.state.WrappedHederaState;
import com.hedera.node.app.workflows.SolvencyPreCheck;
import com.hedera.node.app.workflows.TransactionChecker;
import com.hedera.node.app.workflows.dispatcher.ReadableStoreFactory;
import com.hedera.node.app.workflows.dispatcher.ServiceApiFactory;
Expand Down Expand Up @@ -110,13 +118,14 @@ public class HandleContextImpl implements HandleContext, FeeContext {
private final ServiceApiFactory serviceApiFactory;
private final WritableStoreFactory writableStoreFactory;
private final BlockRecordInfo blockRecordInfo;
private final RecordCache recordCache;
private final HederaRecordCache recordCache;
private final FeeAccumulator feeAccumulator;
private final Function<SubType, FeeCalculator> feeCalculatorCreator;
private final FeeManager feeManager;
private final Instant userTransactionConsensusTime;
private final ExchangeRateManager exchangeRateManager;
private final Authorizer authorizer;
private final SolvencyPreCheck solvencyPreCheck;

private ReadableStoreFactory readableStoreFactory;
private AttributeValidator attributeValidator;
Expand Down Expand Up @@ -145,6 +154,7 @@ public class HandleContextImpl implements HandleContext, FeeContext {
* @param exchangeRateManager The {@link ExchangeRateManager} used to obtain exchange rate information
* @param userTransactionConsensusTime The consensus time of the user transaction, not any child transactions
* @param authorizer The {@link Authorizer} used to authorize the transaction
* @param solvencyPreCheck The {@link SolvencyPreCheck} used to validate if the account is able to pay the fees
*/
public HandleContextImpl(
@NonNull final TransactionBody txBody,
Expand All @@ -163,11 +173,12 @@ public HandleContextImpl(
@NonNull final TransactionDispatcher dispatcher,
@NonNull final ServiceScopeLookup serviceScopeLookup,
@NonNull final BlockRecordInfo blockRecordInfo,
@NonNull final RecordCache recordCache,
@NonNull final HederaRecordCache recordCache,
@NonNull final FeeManager feeManager,
@NonNull final ExchangeRateManager exchangeRateManager,
@NonNull final Instant userTransactionConsensusTime,
@NonNull final Authorizer authorizer) {
@NonNull final Authorizer authorizer,
@NonNull final SolvencyPreCheck solvencyPreCheck) {
this.txBody = requireNonNull(txBody, "txBody must not be null");
this.functionality = requireNonNull(functionality, "functionality must not be null");
this.payer = requireNonNull(payer, "payer must not be null");
Expand Down Expand Up @@ -210,6 +221,7 @@ public HandleContextImpl(
}

this.exchangeRateManager = requireNonNull(exchangeRateManager, "exchangeRateManager must not be null");
this.solvencyPreCheck = requireNonNull(solvencyPreCheck, "solvencyPreCheck must not be null");
}

private WrappedHederaState current() {
Expand Down Expand Up @@ -242,7 +254,7 @@ public Key payerKey() {

@NonNull
@Override
public FeeCalculator feeCalculator(@NonNull SubType subType) {
public FeeCalculator feeCalculator(@NonNull final SubType subType) {
return feeCalculatorCreator.apply(subType);
}

Expand Down Expand Up @@ -334,7 +346,8 @@ public ExpiryValidator expiryValidator() {

@NonNull
@Override
public TransactionKeys allKeysForTransaction(@NonNull TransactionBody nestedTxn, @NonNull AccountID payerForNested)
public TransactionKeys allKeysForTransaction(
@NonNull final TransactionBody nestedTxn, @NonNull final AccountID payerForNested)
throws PreCheckException {
dispatcher.dispatchPureChecks(nestedTxn);
final var nestedContext = new PreHandleContextImpl(
Expand Down Expand Up @@ -368,7 +381,7 @@ public SignatureVerification verificationFor(@NonNull final Bytes evmAlias) {

@Override
public boolean isSuperUser() {
return authorizer.isSuperUser(payer);
return authorizer.isSuperUser(payer());
}

@Override
Expand Down Expand Up @@ -550,16 +563,16 @@ private void dispatchSyntheticTxn(
// Synthetic transaction bodies do not have transaction ids, node account
// ids, and so on; hence we don't need to validate them with the checker
dispatcher.dispatchPureChecks(txBody);
} catch (PreCheckException e) {
} catch (final PreCheckException e) {
childRecordBuilder.status(e.responseCode());
return;
}

final var childStack = new SavepointStackImpl(current());
HederaFunctionality function;
final HederaFunctionality function;
try {
function = functionOf(txBody);
} catch (UnknownHederaFunctionality e) {
} catch (final UnknownHederaFunctionality e) {
logger.error("Possible bug: unknown function in transaction body", e);
childRecordBuilder.status(ResponseCodeEnum.INVALID_TRANSACTION_BODY);
return;
Expand All @@ -573,11 +586,26 @@ private void dispatchSyntheticTxn(
try {
childPayerKey =
accountStore.getAccountById(transactionID.accountID()).key();
} catch (NullPointerException ex) {
} catch (final NullPointerException ex) {
childRecordBuilder.status(ResponseCodeEnum.INVALID_TRANSACTION_ID);
return;
}
}

try {
validate(
verifier,
function,
body(),
payer(),
payerKey,
childCategory,
networkInfo().selfNodeInfo().nodeId());
} catch (final PreCheckException e) {
childRecordBuilder.status(e.responseCode());
return;
}

final var childContext = new HandleContextImpl(
txBody,
function,
Expand All @@ -599,18 +627,99 @@ private void dispatchSyntheticTxn(
feeManager,
exchangeRateManager,
userTransactionConsensusTime,
authorizer);
authorizer,
solvencyPreCheck);

try {
dispatcher.dispatchHandle(childContext);
childRecordBuilder.status(ResponseCodeEnum.SUCCESS);
childStack.commitFullStack();
} catch (HandleException e) {
} catch (final HandleException e) {
childRecordBuilder.status(e.getStatus());
recordListBuilder.revertChildrenOf(childRecordBuilder);
}
}

private void validate(
@NonNull final KeyVerifier verifier,
final HederaFunctionality function,
stoyanov-st marked this conversation as resolved.
Show resolved Hide resolved
final TransactionBody txBody,
final AccountID payer,
final Key payerKey,
final TransactionCategory txCategory,
final long nodeID)
throws PreCheckException {

final PreHandleContextImpl preHandleContext;

preHandleContext = new PreHandleContextImpl(readableStoreFactory(), txBody, payer, configuration(), dispatcher);
dispatcher.dispatchPreHandle(preHandleContext);

// Check for duplicate transactions. It is perfectly normal for there to be duplicates -- it is valid for
// a user to intentionally submit duplicates to multiple nodes as a hedge against dishonest nodes, or for
// other reasons. If we find a duplicate, we *will not* execute the transaction, we will simply charge
// the payer (whether the payer from the transaction or the node in the event of a due diligence failure)
// and create an appropriate record to save in state and send to the record stream.
final var duplicateCheckResult = recordCache.hasDuplicate(txBody.transactionID(), nodeID);
if (duplicateCheckResult != NO_DUPLICATE && duplicateCheckResult != SAME_NODE) {
stoyanov-st marked this conversation as resolved.
Show resolved Hide resolved
throw new PreCheckException(DUPLICATE_TRANSACTION);
}

// Check the status and solvency of the payer
try {

final var fee = dispatchComputeFees(txBody, payer);
final var payerAccount = solvencyPreCheck.getPayerAccount(readableStoreFactory(), payer);
solvencyPreCheck.checkSolvency(txBody, payer, functionality, payerAccount, fee, true);
} catch (final InsufficientServiceFeeException | InsufficientNonFeeDebitsException e) {
stoyanov-st marked this conversation as resolved.
Show resolved Hide resolved
throw new PreCheckException(e.responseCode());
}

// Check the time box of the transaction
checker.checkTimeBox(txBody, userTransactionConsensusTime);

// Check if the payer has the required permissions
if (!authorizer.isAuthorized(payer, function)) {
if (function == HederaFunctionality.SYSTEM_DELETE) {
throw new PreCheckException(ResponseCodeEnum.NOT_SUPPORTED);
}
throw new PreCheckException(ResponseCodeEnum.UNAUTHORIZED);
}

// Check if the transaction is privileged and if the payer has the required privileges
final var privileges = authorizer.hasPrivilegedAuthorization(payer, function, txBody);
if (privileges == SystemPrivilege.UNAUTHORIZED) {
throw new PreCheckException(ResponseCodeEnum.AUTHORIZATION_FAILED);
}
if (privileges == SystemPrivilege.IMPERMISSIBLE) {
throw new PreCheckException(ResponseCodeEnum.ENTITY_NOT_ALLOWED_TO_DELETE);
}

// Skip payer verification when dispatching a child transaction
if (!txCategory.equals(CHILD)) {
jsync-swirlds marked this conversation as resolved.
Show resolved Hide resolved
// Check all signature verifications. This will also wait, if validation is still ongoing.
final var payerKeyVerification = verifier.verificationFor(payerKey);
if (payerKeyVerification.failed()) {
throw new PreCheckException(INVALID_SIGNATURE);
}
}

// verify all the keys
for (final var key : preHandleContext.requiredNonPayerKeys()) {
final var verification = verifier.verificationFor(key);
if (verification.failed()) {
throw new PreCheckException(INVALID_SIGNATURE);
}
}
// If there are any hollow accounts whose signatures need to be verified, verify them
for (final var hollowAccount : preHandleContext.requiredHollowAccounts()) {
final var verification = verifier.verificationFor(hollowAccount.alias());
if (verification.failed()) {
throw new PreCheckException(INVALID_SIGNATURE);
}
}
}

@Override
@NonNull
public <T> T addChildRecordBuilder(@NonNull final Class<T> recordBuilderClass) {
Expand Down
Loading
Loading