From 89969e996b7b1667ce4021ee767149b3265eb000 Mon Sep 17 00:00:00 2001 From: Michael Tinker Date: Tue, 21 Oct 2025 17:49:38 -0500 Subject: [PATCH 1/3] intermezzo Signed-off-by: Michael Tinker --- .../hedera/node/app/fees/AppFeeCharging.java | 2 +- .../handle/DispatchHandleContext.java | 4 +- .../handle/dispatch/ChildDispatchFactory.java | 2 +- .../contract/hapi/ContractGetInfoSuite.java | 52 +++++++++++++++++++ 4 files changed, 57 insertions(+), 3 deletions(-) diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/fees/AppFeeCharging.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/fees/AppFeeCharging.java index 2c0e339f94db..ab30fb338963 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/fees/AppFeeCharging.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/fees/AppFeeCharging.java @@ -49,7 +49,7 @@ public ValidationResult validate( @NonNull final AccountID creatorId, @NonNull final Fees fees, @NonNull final TransactionBody body, - boolean isDuplicate, + final boolean isDuplicate, @NonNull final HederaFunctionality function, @NonNull final HandleContext.TransactionCategory category) { requireNonNull(payer); diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/DispatchHandleContext.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/DispatchHandleContext.java index 826820a526a4..3f322dec02ce 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/DispatchHandleContext.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/DispatchHandleContext.java @@ -99,11 +99,13 @@ public class DispatchHandleContext implements HandleContext, FeeContext { private final DispatchProcessor dispatchProcessor; private final ThrottleAdviser throttleAdviser; private final FeeAccumulator feeAccumulator; - private Map dispatchPaidRewards; private final DispatchMetadata dispatchMetaData; private final TransactionChecker transactionChecker; private final TransactionCategory transactionCategory; + @Nullable + private Map dispatchPaidRewards; + // This is used to store the pre-handle results for the inner transactions // in an atomic batch, null otherwise @Nullable diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/dispatch/ChildDispatchFactory.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/dispatch/ChildDispatchFactory.java index e9bb3d9ea1fd..2a414d50de57 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/dispatch/ChildDispatchFactory.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/dispatch/ChildDispatchFactory.java @@ -239,7 +239,7 @@ private RecordDispatch newChildDispatch( @NonNull final Instant consensusNow, @NonNull final DispatchMetadata dispatchMetadata, @NonNull final ConsensusThrottling consensusThrottling, - @Nullable FeeCharging customFeeCharging, + @Nullable final FeeCharging customFeeCharging, // @UserTxnScope @NonNull final NodeInfo creatorInfo, @NonNull final Configuration config, diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/hapi/ContractGetInfoSuite.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/hapi/ContractGetInfoSuite.java index 6e613b0f9698..c25e0cf8860d 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/hapi/ContractGetInfoSuite.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/hapi/ContractGetInfoSuite.java @@ -8,6 +8,8 @@ import static com.hedera.services.bdd.spec.assertions.ContractInfoAsserts.contractWith; import static com.hedera.services.bdd.spec.queries.QueryVerbs.getAccountBalance; import static com.hedera.services.bdd.spec.queries.QueryVerbs.getContractInfo; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.atomicBatch; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.contractCall; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.contractCreate; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoCreate; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.uploadInitCode; @@ -23,12 +25,18 @@ import static com.hedera.services.bdd.suites.HapiSuite.ONE_HUNDRED_HBARS; import static com.hedera.services.bdd.suites.HapiSuite.TINY_PARTS_PER_WHOLE; import static com.hedera.services.bdd.suites.contract.precompile.CreatePrecompileSuite.MEMO; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INNER_TRANSACTION_FAILED; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INSUFFICIENT_GAS; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.REVERTED_SUCCESS; +import static org.junit.jupiter.api.Assertions.assertEquals; import com.hedera.services.bdd.junit.HapiTest; import com.hedera.services.bdd.spec.HapiSpecSetup; import com.hederahashgraph.api.proto.java.ResponseCodeEnum; +import java.util.Optional; import java.util.concurrent.atomic.AtomicLong; import java.util.stream.Stream; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.DynamicTest; import org.junit.jupiter.api.Tag; @@ -95,4 +103,48 @@ final Stream invalidContractFromAnswerOnly() { .nodePayment(27_159_182L) .hasAnswerOnlyPrecheck(ResponseCodeEnum.INVALID_CONTRACT_ID)); } + + @HapiTest + @DisplayName("Inner Txs payer/signer gets charged for all gas used, should pass") + public Stream userPaysTheGasUsed() { + final var batchOperator = "batchOperator"; + final var payer = "payer"; + + return hapiTest( + uploadInitCode("Multipurpose"), + contractCreate("Multipurpose").gas(2_000_000L), + cryptoCreate(payer).balance(ONE_HUNDRED_HBARS), + cryptoCreate(batchOperator).balance(ONE_HUNDRED_HBARS), + atomicBatch( + contractCall("Multipurpose", "believeIn", 1L) + .gas(32_000L) + .payingWith(payer) + .signingWith(payer) + .batchKey(batchOperator) + .hasKnownStatus(REVERTED_SUCCESS), + contractCall("Multipurpose", "believeIn", 2L) + .gas(32_000L) + .payingWith(payer) + .signingWith(payer) + .batchKey(batchOperator) + .hasKnownStatus(REVERTED_SUCCESS), + contractCall("Multipurpose", "believeIn", 3L) + .gas(24_000L) + .payingWith(payer) + .signingWith(payer) + .batchKey(batchOperator) + .hasKnownStatus(INSUFFICIENT_GAS)) + .payingWith(batchOperator) + .hasKnownStatus(INNER_TRANSACTION_FAILED), + getAccountBalance(payer).hasTinyBars(spec -> actual -> { + final long expected = ONE_HUNDRED_HBARS + - ((2 * 32_000L + 24_000L) * spec.ratesProvider().currentTinybarGasPrice()); + try { + assertEquals(expected, actual, "Balance did not reflect gas used"); + } catch (Throwable t) { + return Optional.of(t.getMessage()); + } + return Optional.empty(); + })); + } } From 6092109a6d16fef3ba84d7c15eaaac1facd0cdd1 Mon Sep 17 00:00:00 2001 From: Michael Tinker Date: Tue, 21 Oct 2025 18:49:13 -0500 Subject: [PATCH 2/3] Make DispatchHandleContext a FeeCharging.Context Signed-off-by: Michael Tinker --- .../handle/DispatchHandleContext.java | 62 +++++++++++++++++-- .../handle/dispatch/ChildDispatchFactory.java | 6 ++ .../handle/dispatch/ValidationResult.java | 20 +++--- .../handle/steps/ParentTxnFactory.java | 6 +- .../impl/StandaloneDispatchFactory.java | 5 ++ .../handle/DispatchHandleContextTest.java | 17 +++-- .../dispatch/ChildDispatchFactoryTest.java | 5 +- .../handle/dispatch/ValidationResultTest.java | 8 --- .../workflows/handle/steps/ParentTxnTest.java | 5 ++ .../contract/hapi/ContractGetInfoSuite.java | 2 +- 10 files changed, 106 insertions(+), 30 deletions(-) diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/DispatchHandleContext.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/DispatchHandleContext.java index 3f322dec02ce..3e1bdd78c334 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/DispatchHandleContext.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/DispatchHandleContext.java @@ -5,6 +5,7 @@ import static com.hedera.hapi.util.HapiUtils.functionOf; import static com.hedera.node.app.spi.workflows.HandleContext.DispatchMetadata.Type.INNER_TRANSACTION_BYTES; import static com.hedera.node.app.spi.workflows.HandleContext.TransactionCategory.BATCH_INNER; +import static com.hedera.node.app.spi.workflows.HandleContext.TransactionCategory.CHILD; import static com.hedera.node.app.workflows.handle.stack.SavepointStackImpl.castBuilder; import static java.util.Collections.emptyMap; import static java.util.Objects.requireNonNull; @@ -31,6 +32,7 @@ import com.hedera.node.app.spi.fees.ExchangeRateInfo; import com.hedera.node.app.spi.fees.FeeCalculator; import com.hedera.node.app.spi.fees.FeeCalculatorFactory; +import com.hedera.node.app.spi.fees.FeeCharging; import com.hedera.node.app.spi.fees.FeeContext; import com.hedera.node.app.spi.fees.Fees; import com.hedera.node.app.spi.fees.ResourcePriceCalculator; @@ -55,6 +57,7 @@ import com.hedera.node.app.workflows.TransactionInfo; import com.hedera.node.app.workflows.dispatcher.TransactionDispatcher; import com.hedera.node.app.workflows.handle.dispatch.ChildDispatchFactory; +import com.hedera.node.app.workflows.handle.dispatch.ValidationResult; import com.hedera.node.app.workflows.handle.stack.SavepointStackImpl; import com.hedera.node.app.workflows.handle.validation.AttributeValidatorImpl; import com.hedera.node.app.workflows.handle.validation.ExpiryValidatorImpl; @@ -70,11 +73,12 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.function.ObjLongConsumer; /** * The {@link HandleContext} implementation. */ -public class DispatchHandleContext implements HandleContext, FeeContext { +public class DispatchHandleContext implements HandleContext, FeeContext, FeeCharging.Context { private final Instant consensusNow; private final NodeInfo creatorInfo; private final TransactionInfo txnInfo; @@ -83,6 +87,7 @@ public class DispatchHandleContext implements HandleContext, FeeContext { private final BlockRecordInfo blockRecordInfo; private final ResourcePriceCalculator resourcePriceCalculator; private final FeeManager feeManager; + private final FeeCharging feeCharging; private final StoreFactoryImpl storeFactory; private final AccountID payerId; private final AppKeyVerifier verifier; @@ -123,6 +128,7 @@ public DispatchHandleContext( @NonNull final BlockRecordInfo blockRecordInfo, @NonNull final ResourcePriceCalculator resourcePriceCalculator, @NonNull final FeeManager feeManager, + @NonNull final FeeCharging feeCharging, @NonNull final StoreFactoryImpl storeFactory, @NonNull final AccountID payerId, @NonNull final AppKeyVerifier verifier, @@ -150,6 +156,7 @@ public DispatchHandleContext( this.blockRecordInfo = requireNonNull(blockRecordInfo); this.resourcePriceCalculator = requireNonNull(resourcePriceCalculator); this.feeManager = requireNonNull(feeManager); + this.feeCharging = requireNonNull(feeCharging); this.storeFactory = requireNonNull(storeFactory); this.payerId = requireNonNull(payerId); this.verifier = requireNonNull(verifier); @@ -197,16 +204,19 @@ public boolean tryToCharge(@NonNull final AccountID accountId, final long amount if (amount < 0) { throw new IllegalArgumentException("Cannot charge negative amount " + amount); } - return feeAccumulator.chargeFee(accountId, amount, null).networkFee() == amount; + return feeCharging + .charge(this, ValidationResult.newSuccess(creatorInfo.accountId()), new Fees(0, amount, 0)) + .totalFee() + == amount; } @Override public void refundBestEffort(@NonNull final AccountID accountId, final long amount) { requireNonNull(accountId); if (amount < 0) { - throw new IllegalArgumentException("Cannot charge negative amount " + amount); + throw new IllegalArgumentException("Cannot refund negative amount " + amount); } - feeAccumulator.refundFee(accountId, amount); + feeCharging.refund(this, new Fees(0, amount, 0)); } @NonNull @@ -473,4 +483,48 @@ public NodeInfo creatorInfo() { public DispatchMetadata dispatchMetadata() { return dispatchMetaData; } + + @Override + public AccountID payerId() { + return payerId; + } + + @Override + public AccountID nodeAccountId() { + return creatorInfo.accountId(); + } + + @Override + public Fees charge( + @NonNull final AccountID payerId, @NonNull final Fees fees, @Nullable final ObjLongConsumer cb) { + return feeAccumulator.chargeFee(payerId, fees.totalFee(), cb); + } + + @Override + public void refund(@NonNull final AccountID receiverId, @NonNull final Fees fees) { + feeAccumulator.refundFee(receiverId, fees.totalFee()); + } + + @Override + public Fees charge( + @NonNull final AccountID payerId, + @NonNull final Fees fees, + @NonNull final AccountID nodeAccountId, + @Nullable final ObjLongConsumer cb) { + return feeAccumulator.chargeFees(payerId, nodeAccountId, fees, cb); + } + + @Override + public void refund( + @NonNull final AccountID payerId, @NonNull final Fees fees, @NonNull final AccountID nodeAccountId) { + feeAccumulator.refundFees(payerId, fees, nodeAccountId); + } + + @Override + public TransactionCategory category() { + // When the DispatchHandleContext is used as a fee charging context, always report + // CHILD category to stay backward compatible with the calls made to FeeAccumulator + // when it was invoked directly + return CHILD; + } } diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/dispatch/ChildDispatchFactory.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/dispatch/ChildDispatchFactory.java index 2a414d50de57..c3315f02be6a 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/dispatch/ChildDispatchFactory.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/dispatch/ChildDispatchFactory.java @@ -26,6 +26,7 @@ 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.fees.AppFeeCharging; import com.hedera.node.app.fees.ExchangeRateManager; import com.hedera.node.app.fees.FeeAccumulator; import com.hedera.node.app.fees.FeeManager; @@ -104,6 +105,7 @@ public class ChildDispatchFactory { private final Authorizer authorizer; private final NetworkInfo networkInfo; private final FeeManager feeManager; + private final AppFeeCharging appFeeCharging; private final DispatchProcessor dispatchProcessor; private final ServiceScopeLookup serviceScopeLookup; private final ExchangeRateManager exchangeRateManager; @@ -116,6 +118,7 @@ public ChildDispatchFactory( @NonNull final Authorizer authorizer, @NonNull final NetworkInfo networkInfo, @NonNull final FeeManager feeManager, + @NonNull final AppFeeCharging appFeeCharging, @NonNull final DispatchProcessor dispatchProcessor, @NonNull final ServiceScopeLookup serviceScopeLookup, @NonNull final ExchangeRateManager exchangeRateManager, @@ -125,6 +128,7 @@ public ChildDispatchFactory( this.authorizer = requireNonNull(authorizer); this.networkInfo = requireNonNull(networkInfo); this.feeManager = requireNonNull(feeManager); + this.appFeeCharging = requireNonNull(appFeeCharging); this.dispatchProcessor = requireNonNull(dispatchProcessor); this.serviceScopeLookup = requireNonNull(serviceScopeLookup); this.exchangeRateManager = requireNonNull(exchangeRateManager); @@ -266,6 +270,7 @@ private RecordDispatch newChildDispatch( final var storeFactory = new StoreFactoryImpl(readableStoreFactory, writableStoreFactory, serviceApiFactory); final var childFeeAccumulator = new FeeAccumulator( serviceApiFactory.getApi(TokenServiceApi.class), (FeeStreamBuilder) builder, childStack); + final var feeCharging = customFeeCharging != null ? customFeeCharging : appFeeCharging; final var dispatchHandleContext = new DispatchHandleContext( consensusNow, creatorInfo, @@ -275,6 +280,7 @@ private RecordDispatch newChildDispatch( blockRecordInfo, priceCalculator, feeManager, + feeCharging, storeFactory, payerId, keyVerifier, diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/dispatch/ValidationResult.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/dispatch/ValidationResult.java index d16ce91cbd30..8756739521f0 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/dispatch/ValidationResult.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/dispatch/ValidationResult.java @@ -135,6 +135,17 @@ public static ValidationResult newSuccess(@NonNull final AccountID creatorId, @N return new ValidationResult(creatorId, null, payer, null, CAN_PAY_SERVICE_FEE, NO_DUPLICATE); } + /** + * Creates an error report with no error. + * @param creatorId the creator account ID + * @return the error report + */ + @NonNull + public static ValidationResult newSuccess(@NonNull final AccountID creatorId) { + requireNonNull(creatorId); + return new ValidationResult(creatorId, null, null, null, CAN_PAY_SERVICE_FEE, NO_DUPLICATE); + } + /** * Creates an error report with no error. * @param creatorId the creator account ID @@ -187,15 +198,6 @@ public ResponseCodeEnum creatorErrorOrThrow() { return requireNonNull(creatorError); } - /** - * Checks if there is a payer. - * @return payer account if there is a payer. Otherwise, throws an exception. - */ - @NonNull - public Account payerOrThrow() { - return requireNonNull(payer); - } - /** * Returns the error report with all fees except service fee. * @return the error report diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/steps/ParentTxnFactory.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/steps/ParentTxnFactory.java index 78139a07f144..e9ad467deac8 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/steps/ParentTxnFactory.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/steps/ParentTxnFactory.java @@ -24,6 +24,7 @@ import com.hedera.node.app.blocks.BlockStreamManager; import com.hedera.node.app.blocks.impl.BoundaryStateChangeListener; import com.hedera.node.app.blocks.impl.ImmediateStateChangeListener; +import com.hedera.node.app.fees.AppFeeCharging; import com.hedera.node.app.fees.ExchangeRateManager; import com.hedera.node.app.fees.FeeAccumulator; import com.hedera.node.app.fees.FeeManager; @@ -96,6 +97,7 @@ public class ParentTxnFactory { private final Authorizer authorizer; private final NetworkInfo networkInfo; private final FeeManager feeManager; + private final AppFeeCharging appFeeCharging; private final DispatchProcessor dispatchProcessor; private final ServiceScopeLookup serviceScopeLookup; private final ExchangeRateManager exchangeRateManager; @@ -117,6 +119,7 @@ public ParentTxnFactory( @NonNull final Authorizer authorizer, @NonNull final NetworkInfo networkInfo, @NonNull final FeeManager feeManager, + @NonNull final AppFeeCharging appFeeCharging, @NonNull final DispatchProcessor dispatchProcessor, @NonNull final ServiceScopeLookup serviceScopeLookup, @NonNull final ExchangeRateManager exchangeRateManager, @@ -134,6 +137,7 @@ public ParentTxnFactory( this.authorizer = requireNonNull(authorizer); this.networkInfo = requireNonNull(networkInfo); this.feeManager = requireNonNull(feeManager); + this.appFeeCharging = requireNonNull(appFeeCharging); this.dispatcher = requireNonNull(dispatcher); this.networkUtilizationManager = requireNonNull(networkUtilizationManager); this.dispatchProcessor = requireNonNull(dispatchProcessor); @@ -326,7 +330,6 @@ private Dispatch createDispatch( final var throttleAdvisor = new AppThrottleAdviser(networkUtilizationManager, consensusNow); final var feeAccumulator = new FeeAccumulator( serviceApiFactory.getApi(TokenServiceApi.class), (FeeStreamBuilder) baseBuilder, stack); - final var dispatchHandleContext = new DispatchHandleContext( consensusNow, creatorInfo, @@ -336,6 +339,7 @@ private Dispatch createDispatch( streamMode != RECORDS ? blockStreamManager : blockRecordManager, priceCalculator, feeManager, + appFeeCharging, storeFactory, requireNonNull(txnInfo.payerID()), keyVerifier, diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/standalone/impl/StandaloneDispatchFactory.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/standalone/impl/StandaloneDispatchFactory.java index 0a95785e3293..a604ecfbf0e2 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/standalone/impl/StandaloneDispatchFactory.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/standalone/impl/StandaloneDispatchFactory.java @@ -15,6 +15,7 @@ import com.hedera.hapi.node.transaction.TransactionBody; import com.hedera.node.app.blocks.impl.BoundaryStateChangeListener; import com.hedera.node.app.blocks.impl.ImmediateStateChangeListener; +import com.hedera.node.app.fees.AppFeeCharging; import com.hedera.node.app.fees.ExchangeRateManager; import com.hedera.node.app.fees.FeeAccumulator; import com.hedera.node.app.fees.FeeManager; @@ -75,6 +76,7 @@ @Singleton public class StandaloneDispatchFactory { private final FeeManager feeManager; + private final AppFeeCharging appFeeCharging; private final Authorizer authorizer; private final NetworkInfo networkInfo; private final ConfigProvider configProvider; @@ -92,6 +94,7 @@ public class StandaloneDispatchFactory { @Inject public StandaloneDispatchFactory( @NonNull final FeeManager feeManager, + @NonNull final AppFeeCharging appFeeCharging, @NonNull final Authorizer authorizer, @NonNull final NetworkInfo networkInfo, @NonNull final ConfigProvider configProvider, @@ -106,6 +109,7 @@ public StandaloneDispatchFactory( @NonNull final TransactionChecker transactionChecker, @NonNull final ScheduleServiceImpl scheduleService) { this.feeManager = requireNonNull(feeManager); + this.appFeeCharging = requireNonNull(appFeeCharging); this.authorizer = requireNonNull(authorizer); this.networkInfo = requireNonNull(networkInfo); this.configProvider = requireNonNull(configProvider); @@ -181,6 +185,7 @@ public Dispatch newDispatch( blockRecordInfo, priceCalculator, feeManager, + appFeeCharging, storeFactory, requireNonNull(txnInfo.payerID()), NO_OP_KEY_VERIFIER, diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/DispatchHandleContextTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/DispatchHandleContextTest.java index b6696ff8d155..db3ba9e878df 100644 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/DispatchHandleContextTest.java +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/DispatchHandleContextTest.java @@ -80,6 +80,7 @@ import com.hedera.node.app.spi.authorization.Authorizer; import com.hedera.node.app.spi.fees.ExchangeRateInfo; import com.hedera.node.app.spi.fees.FeeCalculator; +import com.hedera.node.app.spi.fees.FeeCharging; import com.hedera.node.app.spi.fees.FeeContext; import com.hedera.node.app.spi.fees.Fees; import com.hedera.node.app.spi.fees.ResourcePriceCalculator; @@ -185,6 +186,9 @@ public class DispatchHandleContextTest extends StateTestBase implements Scenario @Mock private FeeAccumulator feeAccumulator; + @Mock + private FeeCharging feeCharging; + @Mock private TransactionChecker transactionChecker; @@ -315,7 +319,8 @@ void setup() { @Test void delegatesFeeChargingForPayer() { - given(feeAccumulator.chargeFee(payerId, 123L, null)).willReturn(new Fees(0, 123L, 0)); + given(creatorInfo.accountId()).willReturn(AccountID.DEFAULT); + given(feeCharging.charge(eq(subject), any(), eq(new Fees(0, 123L, 0)))).willReturn(new Fees(0, 123L, 0)); assertTrue(subject.tryToChargePayer(123L)); } @@ -328,8 +333,8 @@ void failsFastOnNegativeAmounts() { @Test void delegatesFeeChargingForOtherAccount() { - given(feeAccumulator.chargeFee(AccountID.DEFAULT, 123L, null)).willReturn(new Fees(0, 122L, 0)); - + given(creatorInfo.accountId()).willReturn(AccountID.DEFAULT); + given(feeCharging.charge(eq(subject), any(), eq(new Fees(0, 123L, 0)))).willReturn(new Fees(0, 122L, 0)); assertFalse(subject.tryToCharge(AccountID.DEFAULT, 123L)); } @@ -337,7 +342,7 @@ void delegatesFeeChargingForOtherAccount() { void delegatesRefunding() { subject.refundBestEffort(payerId, 123L); - verify(feeAccumulator).refundFee(payerId, 123L); + verify(feeCharging).refund(subject, new Fees(0, 123L, 0)); } @Test @@ -419,6 +424,7 @@ void testConstructorWithInvalidArguments() { blockRecordManager, resourcePriceCalculator, feeManager, + feeCharging, storeFactory, payerId, verifier, @@ -444,7 +450,7 @@ void testConstructorWithInvalidArguments() { for (int i = 0; i < allArgs.length; i++) { final var index = i; // Skip signatureMapSize, payerKey, preHandleResults and batchInnerTxnPreHandler - if (index == 2 || index == 4 || index == 24 || index == 25) { + if (index == 2 || index == 4 || index == 25 || index == 26) { continue; } assertThatThrownBy(() -> { @@ -827,6 +833,7 @@ private DispatchHandleContext createContext( blockRecordManager, resourcePriceCalculator, feeManager, + feeCharging, storeFactory, payerId, verifier, diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/dispatch/ChildDispatchFactoryTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/dispatch/ChildDispatchFactoryTest.java index 534fe965497e..8fa7840dca3c 100644 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/dispatch/ChildDispatchFactoryTest.java +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/dispatch/ChildDispatchFactoryTest.java @@ -24,6 +24,7 @@ import com.hedera.hapi.node.state.token.Account; import com.hedera.hapi.node.transaction.TransactionBody; import com.hedera.hapi.util.UnknownHederaFunctionality; +import com.hedera.node.app.fees.AppFeeCharging; import com.hedera.node.app.fees.ExchangeRateManager; import com.hedera.node.app.fees.FeeManager; import com.hedera.node.app.service.token.ReadableAccountStore; @@ -39,7 +40,6 @@ import com.hedera.node.app.spi.workflows.DispatchOptions.PropagateFeeChargingStrategy; import com.hedera.node.app.spi.workflows.HandleContext; import com.hedera.node.app.spi.workflows.record.StreamBuilder; -import com.hedera.node.app.state.DeduplicationCache; import com.hedera.node.app.store.ReadableStoreFactory; import com.hedera.node.app.workflows.TransactionChecker; import com.hedera.node.app.workflows.dispatcher.TransactionDispatcher; @@ -127,7 +127,7 @@ class ChildDispatchFactoryTest { private TransactionChecker transactionChecker; @Mock - private DeduplicationCache deduplicationCache; + private AppFeeCharging appFeeCharging; private ChildDispatchFactory subject; @@ -144,6 +144,7 @@ public void setUp() { authorizer, networkInfo, feeManager, + appFeeCharging, dispatchProcessor, serviceScopeLookup, exchangeRateManager, diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/dispatch/ValidationResultTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/dispatch/ValidationResultTest.java index df4f149b8cb9..6412da957834 100644 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/dispatch/ValidationResultTest.java +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/dispatch/ValidationResultTest.java @@ -133,14 +133,6 @@ public void testCreatorErrorOrThrow() { assertEquals(INVALID_TRANSACTION_DURATION, report.creatorErrorOrThrow()); } - @Test - public void testPayerOrThrow() { - ValidationResult report = - new ValidationResult(CREATOR_ACCOUNT_ID, null, PAYER_ACCOUNT, null, CAN_PAY_SERVICE_FEE, NO_DUPLICATE); - - assertEquals(PAYER_ACCOUNT, report.payerOrThrow()); - } - @Test public void testWithoutServiceFee() { ValidationResult report = diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/steps/ParentTxnTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/steps/ParentTxnTest.java index 1dc3afef84a0..ca0bdd525a52 100644 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/steps/ParentTxnTest.java +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/steps/ParentTxnTest.java @@ -30,6 +30,7 @@ import com.hedera.node.app.blocks.impl.BoundaryStateChangeListener; import com.hedera.node.app.blocks.impl.ImmediateStateChangeListener; import com.hedera.node.app.blocks.impl.PairedStreamBuilder; +import com.hedera.node.app.fees.AppFeeCharging; import com.hedera.node.app.fees.ExchangeRateManager; import com.hedera.node.app.fees.FeeManager; import com.hedera.node.app.records.BlockRecordManager; @@ -89,6 +90,9 @@ class ParentTxnTest { @Mock private PreHandleResult preHandleResult; + @Mock + private AppFeeCharging appFeeCharging; + @Mock private PreHandleWorkflow preHandleWorkflow; @@ -264,6 +268,7 @@ private ParentTxnFactory createUserTxnFactory() { authorizer, networkInfo, feeManager, + appFeeCharging, dispatchProcessor, serviceScopeLookup, exchangeRateManager, diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/hapi/ContractGetInfoSuite.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/hapi/ContractGetInfoSuite.java index c25e0cf8860d..c9372b34cac1 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/hapi/ContractGetInfoSuite.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/hapi/ContractGetInfoSuite.java @@ -105,7 +105,7 @@ final Stream invalidContractFromAnswerOnly() { } @HapiTest - @DisplayName("Inner Txs payer/signer gets charged for all gas used, should pass") + @DisplayName("Inner txs payer/signer gets charged for all gas used after INNER_TRANSACTION_FAILED") public Stream userPaysTheGasUsed() { final var batchOperator = "batchOperator"; final var payer = "payer"; From c3e84edadd918814d8ceb4a4ceb9066c697c2f6a Mon Sep 17 00:00:00 2001 From: Michael Tinker Date: Thu, 23 Oct 2025 07:51:34 -0500 Subject: [PATCH 3/3] Add FeeCharging.bypassForExtraHandlerCharges() Signed-off-by: Michael Tinker --- .../hedera/node/app/spi/fees/FeeCharging.java | 7 + .../node/app/spi/fees/NoopFeeCharging.java | 16 ++- .../app/spi/workflows/DispatchOptions.java | 8 +- .../app/spi/fees/NoopFeeChargingTest.java | 6 +- .../spi/workflows/DispatchOptionsTest.java | 8 +- .../handle/DispatchHandleContext.java | 18 ++- .../app/components/IngestComponentTest.java | 4 +- .../node/app/fees/AppFeeChargingTest.java | 6 + .../handle/DispatchHandleContextTest.java | 127 ++++++++++++++++-- .../dispatch/ChildDispatchFactoryTest.java | 4 +- .../standalone/TransactionExecutorsTest.java | 4 +- .../scope/HandleHederaNativeOperations.java | 7 +- .../scope/HandleSystemContractOperations.java | 4 +- .../statevalidation/util/StateUtils.java | 4 +- 14 files changed, 185 insertions(+), 38 deletions(-) diff --git a/hedera-node/hedera-app-spi/src/main/java/com/hedera/node/app/spi/fees/FeeCharging.java b/hedera-node/hedera-app-spi/src/main/java/com/hedera/node/app/spi/fees/FeeCharging.java index 7e915c1f0315..c1ca73d6d5ea 100644 --- a/hedera-node/hedera-app-spi/src/main/java/com/hedera/node/app/spi/fees/FeeCharging.java +++ b/hedera-node/hedera-app-spi/src/main/java/com/hedera/node/app/spi/fees/FeeCharging.java @@ -146,4 +146,11 @@ Fees charge( * @param fees the fees to be refunded */ void refund(@NonNull Context ctx, @NonNull Fees fees); + + /** + * Whether the dispatch context should bypass this strategy when charging handler-assessed fees. + */ + default boolean bypassForExtraHandlerCharges() { + return false; + } } diff --git a/hedera-node/hedera-app-spi/src/main/java/com/hedera/node/app/spi/fees/NoopFeeCharging.java b/hedera-node/hedera-app-spi/src/main/java/com/hedera/node/app/spi/fees/NoopFeeCharging.java index 57a7e40dad6a..18d792b331b3 100644 --- a/hedera-node/hedera-app-spi/src/main/java/com/hedera/node/app/spi/fees/NoopFeeCharging.java +++ b/hedera-node/hedera-app-spi/src/main/java/com/hedera/node/app/spi/fees/NoopFeeCharging.java @@ -15,8 +15,15 @@ /** * A fee charging strategy that validates all scenarios and charges no fees. */ -public enum NoopFeeCharging implements FeeCharging { - NOOP_FEE_CHARGING; +public class NoopFeeCharging implements FeeCharging { + public static final NoopFeeCharging UNIVERSAL_NOOP_FEE_CHARGING = new NoopFeeCharging(false); + public static final NoopFeeCharging DISPATCH_ONLY_NOOP_FEE_CHARGING = new NoopFeeCharging(true); + + private final boolean bypassForExtraHandlerCharges; + + public NoopFeeCharging(final boolean bypassForExtraHandlerCharges) { + this.bypassForExtraHandlerCharges = bypassForExtraHandlerCharges; + } @Override public Validation validate( @@ -51,6 +58,11 @@ public void refund(@NonNull final Context ctx, @NonNull final Fees fees) { // No-op } + @Override + public boolean bypassForExtraHandlerCharges() { + return bypassForExtraHandlerCharges; + } + private record PassedValidation(boolean creatorDidDueDiligence, @Nullable ResponseCodeEnum maybeErrorStatus) implements Validation { private static final PassedValidation INSTANCE = new PassedValidation(true, null); diff --git a/hedera-node/hedera-app-spi/src/main/java/com/hedera/node/app/spi/workflows/DispatchOptions.java b/hedera-node/hedera-app-spi/src/main/java/com/hedera/node/app/spi/workflows/DispatchOptions.java index 3e04d3a67cc5..5d287b76a3ec 100644 --- a/hedera-node/hedera-app-spi/src/main/java/com/hedera/node/app/spi/workflows/DispatchOptions.java +++ b/hedera-node/hedera-app-spi/src/main/java/com/hedera/node/app/spi/workflows/DispatchOptions.java @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 package com.hedera.node.app.spi.workflows; -import static com.hedera.node.app.spi.fees.NoopFeeCharging.NOOP_FEE_CHARGING; +import static com.hedera.node.app.spi.fees.NoopFeeCharging.UNIVERSAL_NOOP_FEE_CHARGING; import static com.hedera.node.app.spi.workflows.HandleContext.DispatchMetadata.EMPTY_METADATA; import static com.hedera.node.app.spi.workflows.HandleContext.DispatchMetadata.Type.CUSTOM_FEE_CHARGING; import static com.hedera.node.app.spi.workflows.record.StreamBuilder.SignedTxCustomizer.NOOP_SIGNED_TX_CUSTOMIZER; @@ -176,7 +176,7 @@ public static DispatchOptions independentDispatch( ReversingBehavior.IRREVERSIBLE, NOOP_SIGNED_TX_CUSTOMIZER, EMPTY_METADATA, - NOOP_FEE_CHARGING); + UNIVERSAL_NOOP_FEE_CHARGING); } /** @@ -331,7 +331,7 @@ public static DispatchOptions stepDispatch( reversingBehavior, signedTxCustomizer, EMPTY_METADATA, - NOOP_FEE_CHARGING); + UNIVERSAL_NOOP_FEE_CHARGING); } /** @@ -367,7 +367,7 @@ public static DispatchOptions stepDispatch( ReversingBehavior.REMOVABLE, signedTxCustomizer, metaData, - NOOP_FEE_CHARGING); + UNIVERSAL_NOOP_FEE_CHARGING); } /** diff --git a/hedera-node/hedera-app-spi/src/test/java/com/hedera/node/app/spi/fees/NoopFeeChargingTest.java b/hedera-node/hedera-app-spi/src/test/java/com/hedera/node/app/spi/fees/NoopFeeChargingTest.java index 46dcb16d202d..aa4e57ebaba1 100644 --- a/hedera-node/hedera-app-spi/src/test/java/com/hedera/node/app/spi/fees/NoopFeeChargingTest.java +++ b/hedera-node/hedera-app-spi/src/test/java/com/hedera/node/app/spi/fees/NoopFeeChargingTest.java @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 package com.hedera.node.app.spi.fees; -import static com.hedera.node.app.spi.fees.NoopFeeCharging.NOOP_FEE_CHARGING; +import static com.hedera.node.app.spi.fees.NoopFeeCharging.UNIVERSAL_NOOP_FEE_CHARGING; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.verifyNoInteractions; @@ -25,7 +25,7 @@ class NoopFeeChargingTest { @Test void validationAlwaysPasses() { - final var validation = NOOP_FEE_CHARGING.validate( + final var validation = UNIVERSAL_NOOP_FEE_CHARGING.validate( Account.DEFAULT, AccountID.DEFAULT, Fees.FREE, @@ -39,7 +39,7 @@ void validationAlwaysPasses() { @Test void chargingIsNoop() { - NOOP_FEE_CHARGING.charge(ctx, validation, Fees.FREE); + UNIVERSAL_NOOP_FEE_CHARGING.charge(ctx, validation, Fees.FREE); verifyNoInteractions(ctx, validation); } } diff --git a/hedera-node/hedera-app-spi/src/test/java/com/hedera/node/app/spi/workflows/DispatchOptionsTest.java b/hedera-node/hedera-app-spi/src/test/java/com/hedera/node/app/spi/workflows/DispatchOptionsTest.java index 89626566fe63..2a0ff8206efc 100644 --- a/hedera-node/hedera-app-spi/src/test/java/com/hedera/node/app/spi/workflows/DispatchOptionsTest.java +++ b/hedera-node/hedera-app-spi/src/test/java/com/hedera/node/app/spi/workflows/DispatchOptionsTest.java @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 package com.hedera.node.app.spi.workflows; -import static com.hedera.node.app.spi.fees.NoopFeeCharging.NOOP_FEE_CHARGING; +import static com.hedera.node.app.spi.fees.NoopFeeCharging.UNIVERSAL_NOOP_FEE_CHARGING; import static com.hedera.node.app.spi.workflows.HandleContext.DispatchMetadata.Type.CUSTOM_FEE_CHARGING; import static org.junit.jupiter.api.Assertions.*; @@ -23,12 +23,12 @@ void propagatesSubDispatchCustomFeeChargingViaExpectedKeyIfRequested() { StreamBuilder.class, DispatchOptions.StakingRewards.OFF, DispatchOptions.UsePresetTxnId.YES, - NOOP_FEE_CHARGING, + UNIVERSAL_NOOP_FEE_CHARGING, DispatchOptions.PropagateFeeChargingStrategy.YES); final var maybeFeeCharging = options.dispatchMetadata().getMetadata(CUSTOM_FEE_CHARGING, FeeCharging.class); assertTrue(maybeFeeCharging.isPresent()); - assertSame(NOOP_FEE_CHARGING, maybeFeeCharging.get()); + assertSame(UNIVERSAL_NOOP_FEE_CHARGING, maybeFeeCharging.get()); } @Test @@ -41,7 +41,7 @@ void doesNotPropagateSubDispatchCustomFeeChargingViaExpectedKeyIfNotRequested() StreamBuilder.class, DispatchOptions.StakingRewards.OFF, DispatchOptions.UsePresetTxnId.YES, - NOOP_FEE_CHARGING, + UNIVERSAL_NOOP_FEE_CHARGING, DispatchOptions.PropagateFeeChargingStrategy.NO); final var maybeFeeCharging = options.dispatchMetadata().getMetadata(CUSTOM_FEE_CHARGING, FeeCharging.class); diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/DispatchHandleContext.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/DispatchHandleContext.java index 3e1bdd78c334..c6fe9a7dbc4b 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/DispatchHandleContext.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/DispatchHandleContext.java @@ -204,10 +204,14 @@ public boolean tryToCharge(@NonNull final AccountID accountId, final long amount if (amount < 0) { throw new IllegalArgumentException("Cannot charge negative amount " + amount); } - return feeCharging - .charge(this, ValidationResult.newSuccess(creatorInfo.accountId()), new Fees(0, amount, 0)) - .totalFee() - == amount; + if (feeCharging.bypassForExtraHandlerCharges()) { + return feeAccumulator.chargeFee(accountId, amount, null).networkFee() == amount; + } else { + return feeCharging + .charge(this, ValidationResult.newSuccess(creatorInfo.accountId()), new Fees(0, amount, 0)) + .totalFee() + == amount; + } } @Override @@ -216,7 +220,11 @@ public void refundBestEffort(@NonNull final AccountID accountId, final long amou if (amount < 0) { throw new IllegalArgumentException("Cannot refund negative amount " + amount); } - feeCharging.refund(this, new Fees(0, amount, 0)); + if (feeCharging.bypassForExtraHandlerCharges()) { + feeAccumulator.refundFee(accountId, amount); + } else { + feeCharging.refund(this, new Fees(0, amount, 0)); + } } @NonNull diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/components/IngestComponentTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/components/IngestComponentTest.java index 23b8c85f2b71..1e5c3f726342 100644 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/components/IngestComponentTest.java +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/components/IngestComponentTest.java @@ -4,7 +4,7 @@ import static com.hedera.node.app.fixtures.AppTestBase.DEFAULT_CONFIG; import static com.hedera.node.app.service.token.impl.handlers.BaseCryptoHandler.asAccount; import static com.hedera.node.app.spi.AppContext.Gossip.UNAVAILABLE_GOSSIP; -import static com.hedera.node.app.spi.fees.NoopFeeCharging.NOOP_FEE_CHARGING; +import static com.hedera.node.app.spi.fees.NoopFeeCharging.UNIVERSAL_NOOP_FEE_CHARGING; import static com.hedera.node.app.state.recordcache.schemas.V0490RecordCacheSchema.TRANSACTION_RECEIPTS_STATE_ID; import static com.swirlds.platform.system.address.AddressBookUtils.endpointFor; import static java.util.concurrent.CompletableFuture.completedFuture; @@ -125,7 +125,7 @@ void setUp() { () -> DEFAULT_NODE_INFO, () -> NO_OP_METRICS, throttleFactory, - () -> NOOP_FEE_CHARGING, + () -> UNIVERSAL_NOOP_FEE_CHARGING, new AppEntityIdFactory(configuration)); final var hintsService = new HintsServiceImpl( NO_OP_METRICS, diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/fees/AppFeeChargingTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/fees/AppFeeChargingTest.java index 1e0c8e2b4614..9ca751926e0a 100644 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/fees/AppFeeChargingTest.java +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/fees/AppFeeChargingTest.java @@ -1,6 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 package com.hedera.node.app.fees; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; @@ -116,4 +117,9 @@ void refundsWithNodeAccountOnUser() { verify(ctx).refund(PAYER_ID, FEES, CREATOR_ID); } + + @Test + void doesNotBypassForHandlerCharges() { + assertFalse(subject.bypassForExtraHandlerCharges()); + } } diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/DispatchHandleContextTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/DispatchHandleContextTest.java index db3ba9e878df..506125da5da4 100644 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/DispatchHandleContextTest.java +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/DispatchHandleContextTest.java @@ -14,12 +14,13 @@ import static com.hedera.node.app.service.token.impl.schemas.V0490TokenSchema.ALIASES_STATE_ID; import static com.hedera.node.app.service.token.impl.schemas.V0490TokenSchema.ALIASES_STATE_LABEL; import static com.hedera.node.app.spi.authorization.SystemPrivilege.IMPERMISSIBLE; -import static com.hedera.node.app.spi.fees.NoopFeeCharging.NOOP_FEE_CHARGING; +import static com.hedera.node.app.spi.fees.NoopFeeCharging.UNIVERSAL_NOOP_FEE_CHARGING; import static com.hedera.node.app.spi.fixtures.workflows.ExceptionConditions.responseCode; import static com.hedera.node.app.spi.workflows.DispatchOptions.independentDispatch; import static com.hedera.node.app.spi.workflows.DispatchOptions.setupDispatch; import static com.hedera.node.app.spi.workflows.DispatchOptions.subDispatch; import static com.hedera.node.app.spi.workflows.HandleContext.DispatchMetadata.EMPTY_METADATA; +import static com.hedera.node.app.spi.workflows.HandleContext.TransactionCategory.CHILD; import static com.hedera.node.app.spi.workflows.HandleContext.TransactionCategory.USER; import static com.hedera.node.app.spi.workflows.record.StreamBuilder.ReversingBehavior.REVERSIBLE; import static com.hedera.node.app.spi.workflows.record.StreamBuilder.SignedTxCustomizer.NOOP_SIGNED_TX_CUSTOMIZER; @@ -135,6 +136,7 @@ import java.util.List; import java.util.Map; import java.util.function.Consumer; +import java.util.function.ObjLongConsumer; import java.util.function.Predicate; import java.util.stream.Stream; import org.assertj.core.api.Assertions; @@ -641,7 +643,7 @@ void testDispatchWithInvalidArguments() { StreamBuilder.class, StakingRewards.ON, UsePresetTxnId.NO, - NOOP_FEE_CHARGING, + UNIVERSAL_NOOP_FEE_CHARGING, PropagateFeeChargingStrategy.YES))) .isInstanceOf(NullPointerException.class); assertThatThrownBy(() -> subject.dispatch(subDispatch( @@ -652,7 +654,7 @@ void testDispatchWithInvalidArguments() { null, StakingRewards.ON, UsePresetTxnId.NO, - NOOP_FEE_CHARGING, + UNIVERSAL_NOOP_FEE_CHARGING, PropagateFeeChargingStrategy.YES))) .isInstanceOf(NullPointerException.class); } @@ -669,10 +671,10 @@ private static Stream createContextDispatchers() { StreamBuilder.class, StakingRewards.OFF, UsePresetTxnId.NO, - NOOP_FEE_CHARGING, + UNIVERSAL_NOOP_FEE_CHARGING, PropagateFeeChargingStrategy.YES))), - Arguments.of((Consumer) context -> context.dispatch( - setupDispatch(ALICE.accountID(), txBody, StreamBuilder.class, NOOP_FEE_CHARGING))))); + Arguments.of((Consumer) context -> context.dispatch(setupDispatch( + ALICE.accountID(), txBody, StreamBuilder.class, UNIVERSAL_NOOP_FEE_CHARGING))))); } @ParameterizedTest @@ -737,7 +739,8 @@ void testRemovableDispatchPrecedingIsNotCommitted() { Mockito.lenient().when(verifier.verificationFor((Key) any())).thenReturn(verification); - context.dispatch(setupDispatch(ALICE.accountID(), txBody, StreamBuilder.class, NOOP_FEE_CHARGING)); + context.dispatch( + setupDispatch(ALICE.accountID(), txBody, StreamBuilder.class, UNIVERSAL_NOOP_FEE_CHARGING)); verify(dispatchProcessor).processDispatch(childDispatch); verify(stack, never()).commitFullStack(); @@ -769,7 +772,7 @@ void testChildWithPaidRewardsUpdatedPaidRewards() { StreamBuilder.class, StakingRewards.ON, UsePresetTxnId.NO, - NOOP_FEE_CHARGING, + UNIVERSAL_NOOP_FEE_CHARGING, PropagateFeeChargingStrategy.YES)); verify(dispatchProcessor).processDispatch(childDispatch); @@ -855,6 +858,114 @@ private DispatchHandleContext createContext( category); } + @Test + void tryToCharge_bypassesFeeChargingStrategy_whenEnabled_returnsTrue() { + given(feeCharging.bypassForExtraHandlerCharges()).willReturn(true); + final var amount = 123L; + given(feeAccumulator.chargeFee(eq(payerId), eq(amount), any())).willReturn(new Fees(0, amount, 0)); + + final var result = subject.tryToCharge(payerId, amount); + + assertTrue(result); + verify(feeAccumulator).chargeFee(eq(payerId), eq(amount), any()); + verify(feeCharging, never()).charge(any(), any(), any()); + } + + @Test + void tryToCharge_bypassesFeeChargingStrategy_whenEnabled_returnsFalseIfNotFullyCharged() { + given(feeCharging.bypassForExtraHandlerCharges()).willReturn(true); + final var amount = 123L; + given(feeAccumulator.chargeFee(eq(payerId), eq(amount), any())).willReturn(new Fees(0, amount - 1, 0)); + + final var result = subject.tryToCharge(payerId, amount); + + assertFalse(result); + verify(feeAccumulator).chargeFee(eq(payerId), eq(amount), any()); + verify(feeCharging, never()).charge(any(), any(), any()); + } + + @Test + void refundBestEffort_bypassesFeeChargingStrategy_whenEnabled() { + given(feeCharging.bypassForExtraHandlerCharges()).willReturn(true); + final var amount = 321L; + + subject.refundBestEffort(payerId, amount); + + verify(feeAccumulator).refundFee(payerId, amount); + verify(feeCharging, never()).refund(any(), any()); + } + + @Test + void readableStore_delegatesToStoreFactory() throws Exception { + @SuppressWarnings("unchecked") + final Class storeInterface = + (Class) Class.forName("com.hedera.node.app.service.token.ReadableAccountStore"); + final var store = subject.readableStore(storeInterface); + assertThat(store).isNotNull(); + assertTrue(storeInterface.isInstance(store)); + } + + @Test + void creatorInfo_exposesInjectedNodeInfo() { + assertSame(creatorInfo, subject.creatorInfo()); + } + + @Test + void dispatchMetadata_exposesInjectedMetadata() { + assertSame(EMPTY_METADATA, subject.dispatchMetadata()); + } + + @Test + void payerId_exposesPrimaryPayer() { + assertSame(payerId, subject.payerId()); + } + + @Test + void nodeAccountId_comesFromCreatorInfo() { + given(creatorInfo.accountId()).willReturn(NODE_ACCOUNT_ID); + assertSame(NODE_ACCOUNT_ID, subject.nodeAccountId()); + } + + @Test + void charge_withoutNode_delegatesToFeeAccumulator() { + final ObjLongConsumer cb = (id, amt) -> {}; + given(feeAccumulator.chargeFee(eq(payerId), eq(FEES.totalFee()), any())).willReturn(FEES); + + final var charged = subject.charge(payerId, FEES, cb); + + assertSame(FEES, charged); + verify(feeAccumulator).chargeFee(eq(payerId), eq(FEES.totalFee()), any()); + } + + @Test + void charge_withNode_delegatesToFeeAccumulator() { + final ObjLongConsumer cb = (id, amt) -> {}; + given(feeAccumulator.chargeFees(eq(payerId), eq(NODE_ACCOUNT_ID), eq(FEES), any())) + .willReturn(FEES); + + final var charged = subject.charge(payerId, FEES, NODE_ACCOUNT_ID, cb); + + assertSame(FEES, charged); + verify(feeAccumulator).chargeFees(eq(payerId), eq(NODE_ACCOUNT_ID), eq(FEES), any()); + } + + @Test + void refund_withoutNode_delegatesToFeeAccumulator() { + subject.refund(payerId, FEES); + verify(feeAccumulator).refundFee(eq(payerId), eq(FEES.totalFee())); + } + + @Test + void refund_withNode_delegatesToFeeAccumulator() { + subject.refund(payerId, FEES, NODE_ACCOUNT_ID); + verify(feeAccumulator).refundFees(eq(payerId), eq(FEES), eq(NODE_ACCOUNT_ID)); + } + + @Test + void category_isChild_forFeeChargingContext() { + assertThat(subject.category()).isEqualTo(CHILD); + } + private void mockNeeded() { lenient() .when(childDispatchFactory.createChildDispatch( diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/dispatch/ChildDispatchFactoryTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/dispatch/ChildDispatchFactoryTest.java index 8fa7840dca3c..1936ca3ee066 100644 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/dispatch/ChildDispatchFactoryTest.java +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/dispatch/ChildDispatchFactoryTest.java @@ -4,7 +4,7 @@ import static com.hedera.hapi.node.base.HederaFunctionality.CONTRACT_CALL; import static com.hedera.node.app.fixtures.AppTestBase.DEFAULT_CONFIG; import static com.hedera.node.app.service.token.impl.api.TokenServiceApiProvider.TOKEN_SERVICE_API_PROVIDER; -import static com.hedera.node.app.spi.fees.NoopFeeCharging.NOOP_FEE_CHARGING; +import static com.hedera.node.app.spi.fees.NoopFeeCharging.UNIVERSAL_NOOP_FEE_CHARGING; import static java.util.Collections.emptySet; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -253,7 +253,7 @@ void testFunctionOfTxnThrowsException() { StreamBuilder.class, DispatchOptions.StakingRewards.ON, DispatchOptions.UsePresetTxnId.NO, - NOOP_FEE_CHARGING, + UNIVERSAL_NOOP_FEE_CHARGING, PropagateFeeChargingStrategy.YES), null)); assertInstanceOf(UnknownHederaFunctionality.class, exception.getCause()); diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/standalone/TransactionExecutorsTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/standalone/TransactionExecutorsTest.java index a85c64e0b0de..faaa3086a40d 100644 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/standalone/TransactionExecutorsTest.java +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/standalone/TransactionExecutorsTest.java @@ -6,7 +6,7 @@ import static com.hedera.node.app.records.schemas.V0490BlockRecordSchema.BLOCKS_STATE_ID; import static com.hedera.node.app.service.addressbook.impl.schemas.V053AddressBookSchema.NODES_STATE_ID; import static com.hedera.node.app.spi.AppContext.Gossip.UNAVAILABLE_GOSSIP; -import static com.hedera.node.app.spi.fees.NoopFeeCharging.NOOP_FEE_CHARGING; +import static com.hedera.node.app.spi.fees.NoopFeeCharging.UNIVERSAL_NOOP_FEE_CHARGING; import static com.hedera.node.app.util.FileUtilities.createFileID; import static com.hedera.node.app.workflows.standalone.TransactionExecutors.MAX_SIGNED_TXN_SIZE_PROPERTY; import static com.hedera.node.app.workflows.standalone.TransactionExecutors.TRANSACTION_EXECUTORS; @@ -398,7 +398,7 @@ private MerkleNodeState genesisState(@NonNull final Map override () -> NO_OP_METRICS, new AppThrottleFactory( () -> config, () -> state, () -> ThrottleDefinitions.DEFAULT, ThrottleAccumulator::new), - () -> NOOP_FEE_CHARGING, + () -> UNIVERSAL_NOOP_FEE_CHARGING, new AppEntityIdFactory(config)); registerServices(appContext, servicesRegistry); final var migrator = new FakeServiceMigrator(); diff --git a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/scope/HandleHederaNativeOperations.java b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/scope/HandleHederaNativeOperations.java index 8150d7cedf72..c03a60dd24f6 100644 --- a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/scope/HandleHederaNativeOperations.java +++ b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/scope/HandleHederaNativeOperations.java @@ -5,7 +5,7 @@ import static com.hedera.hapi.node.base.ResponseCodeEnum.OK; import static com.hedera.node.app.service.contract.impl.exec.utils.FrameUtils.selfDestructBeneficiariesFor; import static com.hedera.node.app.service.contract.impl.utils.SynthTxnUtils.synthHollowAccountCreation; -import static com.hedera.node.app.spi.fees.NoopFeeCharging.NOOP_FEE_CHARGING; +import static com.hedera.node.app.spi.fees.NoopFeeCharging.DISPATCH_ONLY_NOOP_FEE_CHARGING; import static com.hedera.node.app.spi.workflows.DispatchOptions.setupDispatch; import static java.util.Objects.requireNonNull; @@ -137,7 +137,10 @@ public void setNonce(final long contractNumber, final long nonce) { try { return context.dispatch(setupDispatch( - context.payer(), synthTxn, CryptoCreateStreamBuilder.class, NOOP_FEE_CHARGING)) + context.payer(), + synthTxn, + CryptoCreateStreamBuilder.class, + DISPATCH_ONLY_NOOP_FEE_CHARGING)) .status(); } catch (HandleException e) { // It is critically important we don't let HandleExceptions propagate to the workflow because diff --git a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/scope/HandleSystemContractOperations.java b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/scope/HandleSystemContractOperations.java index adf8f21c423d..32757f84f273 100644 --- a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/scope/HandleSystemContractOperations.java +++ b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/scope/HandleSystemContractOperations.java @@ -3,7 +3,7 @@ import static com.hedera.hapi.node.base.HederaFunctionality.CONTRACT_CALL; import static com.hedera.node.app.service.contract.impl.utils.ConversionUtils.tuweniToPbjBytes; -import static com.hedera.node.app.spi.fees.NoopFeeCharging.NOOP_FEE_CHARGING; +import static com.hedera.node.app.spi.fees.NoopFeeCharging.DISPATCH_ONLY_NOOP_FEE_CHARGING; import static com.hedera.node.app.spi.workflows.DispatchOptions.subDispatch; import static com.hedera.node.app.spi.workflows.record.StreamBuilder.signedTxWith; import static java.util.Objects.requireNonNull; @@ -93,7 +93,7 @@ public Predicate signatureTestWith(@NonNull final VerificationStrategy stra // transaction's remaining gas, so there is no more charging to do in the DispatchProcessor; // FUTURE - make the custom implementation here _directly_ deduct from remaining gas without // the manual precomputation upstream from here - NOOP_FEE_CHARGING, + DISPATCH_ONLY_NOOP_FEE_CHARGING, PropagateFeeChargingStrategy.YES)); } diff --git a/hedera-state-validator/src/main/java/com/hedera/statevalidation/util/StateUtils.java b/hedera-state-validator/src/main/java/com/hedera/statevalidation/util/StateUtils.java index 4a4be376c9aa..cdc8fde15482 100644 --- a/hedera-state-validator/src/main/java/com/hedera/statevalidation/util/StateUtils.java +++ b/hedera-state-validator/src/main/java/com/hedera/statevalidation/util/StateUtils.java @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 package com.hedera.statevalidation.util; -import static com.hedera.node.app.spi.fees.NoopFeeCharging.NOOP_FEE_CHARGING; +import static com.hedera.node.app.spi.fees.NoopFeeCharging.UNIVERSAL_NOOP_FEE_CHARGING; import static com.hedera.statevalidation.util.ConfigUtils.STATE_FILE_NAME; import static com.hedera.statevalidation.util.ConfigUtils.getConfiguration; import static com.hedera.statevalidation.util.PlatformContextHelper.getPlatformContext; @@ -160,7 +160,7 @@ private static ServicesRegistryImpl initServiceRegistry() { NoOpMetrics::new, new AppThrottleFactory( configSupplier, () -> null, () -> ThrottleDefinitions.DEFAULT, ThrottleAccumulator::new), - () -> NOOP_FEE_CHARGING, + () -> UNIVERSAL_NOOP_FEE_CHARGING, new AppEntityIdFactory(config)); final AtomicReference componentRef = new AtomicReference<>();