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

feat: Introduce generic custom fee limits #17315

Open
wants to merge 4 commits into
base: hip-991-topic-fees
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -180,5 +180,5 @@ message ConsensusCreateTopicTransactionBody {
* custom_fees list SHALL NOT contain more than
* `MAX_CUSTOM_FEE_ENTRIES_FOR_TOPICS` entries.
*/
repeated ConsensusCustomFee custom_fees = 10;
repeated FixedCustomFee custom_fees = 10;
}
Original file line number Diff line number Diff line change
Expand Up @@ -189,5 +189,5 @@ message ConsensusTopicInfo {
* Custom fees defined here SHALL be assessed in addition to the base
* network and node fees.
*/
repeated ConsensusCustomFee custom_fees = 12;
repeated FixedCustomFee custom_fees = 12;
}
Original file line number Diff line number Diff line change
Expand Up @@ -181,5 +181,5 @@ message ConsensusUpdateTopicTransactionBody {
* custom_fees list SHALL NOT contain more than
* `MAX_CUSTOM_FEE_ENTRIES_FOR_TOPICS` entries.
*/
ConsensusCustomFeeList custom_fees = 12;
FixedCustomFeeList custom_fees = 12;
}
24 changes: 21 additions & 3 deletions hapi/hedera-protobufs/services/custom_fees.proto
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,7 @@ message AssessedCustomFee {
* Only "fixed" fee definitions are supported because there is no basis for
* a fractional fee on a consensus submit transaction.
*/
message ConsensusCustomFee {
message FixedCustomFee {
/**
* A fixed custom fee.
* <p>
Expand Down Expand Up @@ -374,12 +374,12 @@ message ConsensusCustomFee {
* A _set_ field of this type with an empty `fees` list SHALL remove any
* existing values.
*/
message ConsensusCustomFeeList {
message FixedCustomFeeList {
/**
* A set of custom fee definitions.<br/>
* These are fees to be assessed for each submit to a topic.
*/
repeated ConsensusCustomFee fees = 1;
repeated FixedCustomFee fees = 1;
}

/**
Expand All @@ -402,3 +402,21 @@ message FeeExemptKeyList {
*/
repeated Key keys = 1;
}

/**
* A maximum custom fee that the user is willing to pay.
* <p>
* This message is used to specify the maximum custom fee that given user is
* willing to pay.
*/
message CustomFeeLimit {
/**
* A payer account identifier.
*/
AccountID account_id = 1;

/**
* A custom fee amount limit.
*/
FixedFee amount_limit = 2;
}
26 changes: 26 additions & 0 deletions hapi/hedera-protobufs/services/response_code.proto
Original file line number Diff line number Diff line change
Expand Up @@ -1673,4 +1673,30 @@ enum ResponseCodeEnum {
*/
FEE_SCHEDULE_KEY_NOT_SET = 380;

/**
* The fee amount is exceeding the amount that the payer
* is willing to pay.
*/
MAX_CUSTOM_FEE_LIMIT_EXCEEDED = 381;

/**
* There are no corresponding custom fees.
*/
NO_VALID_MAX_CUSTOM_FEE = 382;

/**
* The provided list contains invalid max custom fee.
*/
INVALID_MAX_CUSTOM_FEES = 383;

/**
* The provided max custom fee list contains fees with
* duplicate denominations.
*/
DUPLICATE_DENOMINATION_IN_MAX_CUSTOM_FEE_LIST = 384;

/**
* Max custom fees list is not supported for this operation.
*/
MAX_CUSTOM_FEES_IS_NOT_SUPPORTED = 385;
}
Original file line number Diff line number Diff line change
Expand Up @@ -190,5 +190,5 @@ message Topic {
* If this list is not empty, custom fees defined here SHALL be
* charged _in addition to_ the base network and node fees.
*/
repeated ConsensusCustomFee custom_fees = 13;
repeated FixedCustomFee custom_fees = 13;
}
10 changes: 10 additions & 0 deletions hapi/hedera-protobufs/services/transaction_body.proto
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ import "node_update.proto";
import "node_delete.proto";

import "event/state_signature_transaction.proto";
import "custom_fees.proto";

/**
* A transaction body.
Expand Down Expand Up @@ -561,4 +562,13 @@ message TransactionBody {
*/
com.hedera.hapi.platform.event.StateSignatureTransaction state_signature_transaction = 65;
}

/**
* A list of maximum custom fees that the users are willing to pay.
* <p>
* This field is OPTIONAL.<br/>
* If left empty, the users are accepting to pay any custom fee.<br/>
* If used with a transaction type that does not support custom fee limits, the transaction will fail.
*/
repeated CustomFeeLimit maxCustomFees = 1001;
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2022-2024 Hedera Hashgraph, LLC
* Copyright (C) 2022-2025 Hedera Hashgraph, LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -16,6 +16,7 @@

package com.hedera.node.app.workflows;

import static com.hedera.hapi.node.base.HederaFunctionality.CONSENSUS_SUBMIT_MESSAGE;
import static com.hedera.hapi.node.base.ResponseCodeEnum.INSUFFICIENT_TX_FEE;
import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_TRANSACTION;
import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_TRANSACTION_BODY;
Expand All @@ -40,6 +41,7 @@
import com.hedera.hapi.node.base.Timestamp;
import com.hedera.hapi.node.base.Transaction;
import com.hedera.hapi.node.base.TransactionID;
import com.hedera.hapi.node.transaction.CustomFeeLimit;
import com.hedera.hapi.node.transaction.SignedTransaction;
import com.hedera.hapi.node.transaction.TransactionBody;
import com.hedera.hapi.util.HapiUtils;
Expand Down Expand Up @@ -83,6 +85,8 @@
private static final Logger logger = LogManager.getLogger(TransactionChecker.class);

private static final int USER_TRANSACTION_NONCE = 0;
private static final List<HederaFunctionality> FUNCTIONALITIES_WITH_MAX_CUSTOM_FEES =
List.of(CONSENSUS_SUBMIT_MESSAGE);

// Metric config for keeping track of the number of deprecated transactions received
private static final String COUNTER_DEPRECATED_TXNS_NAME = "DeprTxnsRcv";
Expand Down Expand Up @@ -250,7 +254,7 @@
public TransactionInfo checkParsed(@NonNull final TransactionInfo txInfo) throws PreCheckException {
try {
checkPrefixMismatch(txInfo.signatureMap().sigPair());
checkTransactionBody(txInfo.txBody());
checkTransactionBody(txInfo.txBody(), txInfo.functionality());
return txInfo;
} catch (PreCheckException e) {
throw new DueDiligenceException(e.responseCode(), txInfo);
Expand Down Expand Up @@ -310,10 +314,12 @@
* @throws PreCheckException if validation fails
* @throws NullPointerException if any of the parameters is {@code null}
*/
private void checkTransactionBody(@NonNull final TransactionBody txBody) throws PreCheckException {
private void checkTransactionBody(@NonNull final TransactionBody txBody, HederaFunctionality functionality)
throws PreCheckException {
final var config = props.getConfiguration().getConfigData(HederaConfig.class);
checkTransactionID(txBody.transactionIDOrThrow());
checkMemo(txBody.memo(), config.transactionMaxMemoUtf8Bytes());
checkMaxCustomFee(txBody.maxCustomFees(), functionality);

// You cannot have a negative transaction fee!! We're not paying you, buddy.
if (txBody.transactionFee() < 0) {
Expand Down Expand Up @@ -430,6 +436,22 @@
}
}

private void checkMaxCustomFee(List<CustomFeeLimit> maxCustomFeeList, HederaFunctionality functionality)
throws PreCheckException {
if (!FUNCTIONALITIES_WITH_MAX_CUSTOM_FEES.contains(functionality) && !maxCustomFeeList.isEmpty()) {
throw new PreCheckException(ResponseCodeEnum.MAX_CUSTOM_FEES_IS_NOT_SUPPORTED);

Check warning on line 442 in hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/TransactionChecker.java

View check run for this annotation

Codecov / codecov/patch

hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/TransactionChecker.java#L442

Added line #L442 was not covered by tests
}

// check required fields
for (var maxCustomFee : maxCustomFeeList) {
if (maxCustomFee.accountId() == null
|| maxCustomFee.amountLimit() == null
|| maxCustomFee.amountLimit().amount() < 0) {
throw new PreCheckException(ResponseCodeEnum.INVALID_MAX_CUSTOM_FEES);

Check warning on line 450 in hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/TransactionChecker.java

View check run for this annotation

Codecov / codecov/patch

hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/TransactionChecker.java#L450

Added line #L450 was not covered by tests
}
}

Check warning on line 452 in hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/TransactionChecker.java

View check run for this annotation

Codecov / codecov/patch

hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/TransactionChecker.java#L452

Added line #L452 was not covered by tests
}

/**
* This method converts a {@link Timestamp} to an {@link Instant} limited between {@link Instant#MIN} and
* {@link Instant#MAX}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
import com.hedera.hapi.node.base.TokenID;
import com.hedera.hapi.node.base.TokenSupplyType;
import com.hedera.hapi.node.base.TokenType;
import com.hedera.hapi.node.transaction.ConsensusCustomFee;
import com.hedera.hapi.node.transaction.FixedCustomFee;
import com.hedera.node.app.service.token.ReadableAccountStore;
import com.hedera.node.app.service.token.ReadableTokenRelationStore;
import com.hedera.node.app.service.token.ReadableTokenStore;
Expand Down Expand Up @@ -66,7 +66,7 @@ public void validate(
@NonNull final ReadableAccountStore accountStore,
@NonNull final ReadableTokenRelationStore tokenRelationStore,
@NonNull final ReadableTokenStore tokenStore,
@NonNull final List<ConsensusCustomFee> customFees,
@NonNull final List<FixedCustomFee> customFees,
@NonNull final ExpiryValidator expiryValidator) {
requireNonNull(accountStore);
requireNonNull(tokenRelationStore);
Expand All @@ -89,7 +89,7 @@ public void validate(
}

private void validateFixedFee(
@NonNull final ConsensusCustomFee fee,
@NonNull final FixedCustomFee fee,
@NonNull final ReadableTokenRelationStore tokenRelationStore,
@NonNull final ReadableTokenStore tokenStore) {
final var fixedFee = fee.fixedFeeOrThrow();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
import com.hedera.hapi.node.consensus.ConsensusCreateTopicTransactionBody;
import com.hedera.hapi.node.state.consensus.Topic;
import com.hedera.hapi.node.state.token.Account;
import com.hedera.hapi.node.transaction.ConsensusCustomFee;
import com.hedera.hapi.node.transaction.FixedCustomFee;
import com.hedera.hapi.node.transaction.FixedFee;
import com.hedera.hapi.node.transaction.TransactionBody;
import com.hedera.node.app.service.consensus.ReadableTopicStore;
Expand Down Expand Up @@ -121,7 +121,7 @@ private TransactionBody newCreateTxn(
Key adminKey,
Key submitKey,
boolean hasAutoRenewAccount,
List<ConsensusCustomFee> customFees,
List<FixedCustomFee> customFees,
List<Key> feeExemptKeyList) {
final var txnId = TransactionID.newBuilder().accountID(payerId).build();
final var createTopicBuilder = ConsensusCreateTopicTransactionBody.newBuilder();
Expand Down Expand Up @@ -481,7 +481,7 @@ void validatedAutoRenewAccount() {
@Test
@DisplayName("Handle works as expected wit custom fees and FEKL")
void validatedCustomFees() {
final var customFees = List.of(ConsensusCustomFee.newBuilder()
final var customFees = List.of(FixedCustomFee.newBuilder()
.fixedFee(FixedFee.newBuilder().amount(1).build())
.feeCollectorAccountId(AccountID.DEFAULT)
.build());
Expand Down Expand Up @@ -542,7 +542,7 @@ void failWithTooManyFeeExemptKeys() {
@Test
@DisplayName("Handle fail with invalid custom fee amount")
void failWithInvalidFeeAmount() {
final var customFees = List.of(ConsensusCustomFee.newBuilder()
final var customFees = List.of(FixedCustomFee.newBuilder()
.fixedFee(FixedFee.newBuilder().amount(-1).build())
.feeCollectorAccountId(AccountID.DEFAULT)
.build());
Expand All @@ -565,7 +565,7 @@ void failWithInvalidFeeAmount() {
@Test
@DisplayName("Handle fail with invalid collector")
void failWithInvalidCollector() {
final var customFees = List.of(ConsensusCustomFee.newBuilder()
final var customFees = List.of(FixedCustomFee.newBuilder()
.fixedFee(FixedFee.newBuilder().amount(1).build())
.feeCollectorAccountId(AccountID.DEFAULT)
.build());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
import com.hedera.hapi.node.base.ThresholdKey;
import com.hedera.hapi.node.base.TopicID;
import com.hedera.hapi.node.state.consensus.Topic;
import com.hedera.hapi.node.transaction.ConsensusCustomFee;
import com.hedera.hapi.node.transaction.FixedCustomFee;
import com.hedera.hapi.node.transaction.FixedFee;
import com.hedera.node.app.service.consensus.ReadableTopicStore;
import com.hedera.node.app.service.consensus.impl.ReadableTopicStoreImpl;
Expand Down Expand Up @@ -114,7 +114,7 @@ public class ConsensusTestBase {
protected final long sequenceNumber = 1L;
protected final long autoRenewSecs = 100L;
protected final Instant consensusTimestamp = Instant.ofEpochSecond(1_234_567L);
protected final List<ConsensusCustomFee> customFees = List.of(ConsensusCustomFee.newBuilder()
protected final List<FixedCustomFee> customFees = List.of(FixedCustomFee.newBuilder()
.fixedFee(FixedFee.newBuilder().amount(1).build())
.feeCollectorAccountId(anotherPayer)
.build());
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2020-2024 Hedera Hashgraph, LLC
* Copyright (C) 2020-2025 Hedera Hashgraph, LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -45,6 +45,7 @@
import com.hedera.services.bdd.spec.utilops.mod.BodyMutation;
import com.hedera.services.bdd.suites.HapiSuite;
import com.hederahashgraph.api.proto.java.AccountID;
import com.hederahashgraph.api.proto.java.CustomFeeLimit;
import com.hederahashgraph.api.proto.java.Duration;
import com.hederahashgraph.api.proto.java.HederaFunctionality;
import com.hederahashgraph.api.proto.java.Key;
Expand All @@ -56,6 +57,7 @@
import com.hederahashgraph.api.proto.java.TransactionRecord;
import edu.umd.cs.findbugs.annotations.Nullable;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
Expand Down Expand Up @@ -128,6 +130,7 @@ public abstract class HapiSpecOperation implements SpecOperation {
protected Map<Key, SigControl> overrides = Collections.EMPTY_MAP;

protected Optional<Long> fee = Optional.empty();
protected List<Function<HapiSpec, CustomFeeLimit>> maxCustomFeeList = new ArrayList<>();
protected Optional<Long> validDurationSecs = Optional.empty();
protected Optional<String> customTxnId = Optional.empty();
protected Optional<String> memo = Optional.empty();
Expand Down Expand Up @@ -318,6 +321,10 @@ protected Transaction finalizedTxn(
.orElse(minDef)
.andThen(opDef);

for (final var supplier : maxCustomFeeList) {
netDef = netDef.andThen(b -> b.addMaxCustomFees(supplier.apply(spec)));
}

setKeyControlOverrides(spec);
List<Key> keys = signersToUseFor(spec);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@
import com.hedera.services.bdd.spec.HapiSpec;
import com.hedera.services.bdd.spec.queries.HapiQueryOp;
import com.hedera.services.bdd.spec.transactions.TxnUtils;
import com.hederahashgraph.api.proto.java.ConsensusCustomFee;
import com.hederahashgraph.api.proto.java.ConsensusGetTopicInfoQuery;
import com.hederahashgraph.api.proto.java.ConsensusTopicInfo;
import com.hederahashgraph.api.proto.java.FixedCustomFee;
import com.hederahashgraph.api.proto.java.HederaFunctionality;
import com.hederahashgraph.api.proto.java.Query;
import com.hederahashgraph.api.proto.java.ResponseType;
Expand Down Expand Up @@ -68,7 +68,7 @@ public class HapiGetTopicInfo extends HapiQueryOp<HapiGetTopicInfo> {
private Optional<String> feeScheduleKey = Optional.empty();
private final List<String> expectedFeeExemptKeyList = new ArrayList<>();
private boolean expectFeeExemptKeyListEmpty = false;
private final List<BiConsumer<HapiSpec, List<ConsensusCustomFee>>> expectedFees = new ArrayList<>();
private final List<BiConsumer<HapiSpec, List<FixedCustomFee>>> expectedFees = new ArrayList<>();
private boolean expectNoFees = false;
private Optional<Integer> expectCustomFeeSize = Optional.empty();
private boolean saveRunningHash = false;
Expand Down Expand Up @@ -171,7 +171,7 @@ public HapiGetTopicInfo hasFeeExemptKeys(List<String> feeExemptKeyAssertion) {
return this;
}

public HapiGetTopicInfo hasCustomFee(BiConsumer<HapiSpec, List<ConsensusCustomFee>> feeAssertion) {
public HapiGetTopicInfo hasCustomFee(BiConsumer<HapiSpec, List<FixedCustomFee>> feeAssertion) {
expectedFees.add(feeAssertion);
return this;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
import com.hedera.services.bdd.spec.utilops.mod.BodyMutation;
import com.hedera.services.bdd.spec.verification.Condition;
import com.hederahashgraph.api.proto.java.AccountID;
import com.hederahashgraph.api.proto.java.CustomFeeLimit;
import com.hederahashgraph.api.proto.java.HederaFunctionality;
import com.hederahashgraph.api.proto.java.Key;
import com.hederahashgraph.api.proto.java.Query;
Expand Down Expand Up @@ -635,6 +636,11 @@ public T feeUsd(double price) {
return self();
}

public T maxCustomFee(Function<HapiSpec, CustomFeeLimit> f) {
maxCustomFeeList.add(f);
return self();
}

public T signedByPayerAnd(String... keys) {
final String[] copy = new String[keys.length + 1];
copy[0] = DEFAULT_PAYER;
Expand Down
Loading
Loading