Skip to content

Commit dccae64

Browse files
feat: Simple Fees: use new extras for topic custom fees (#22252)
Signed-off-by: Josh Marinacci <[email protected]> Signed-off-by: Josh Marinacci <[email protected]>
1 parent 72add2d commit dccae64

File tree

7 files changed

+137
-81
lines changed

7 files changed

+137
-81
lines changed

hapi/hedera-protobuf-java-api/src/main/proto/fees/fee_schedule.proto

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,8 @@ enum Extra {
101101
TOKEN_CREATE_NFT=23;
102102
TOKEN_MINT_FUNGIBLE=24;
103103
TOKEN_MINT_NFT=25;
104+
CONSENSUS_CREATE_TOPIC_WITH_CUSTOM_FEE=26;
105+
CONSENSUS_SUBMIT_MESSAGE_WITH_CUSTOM_FEE=27;
104106
}
105107

106108
/**

hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/calculator/ConsensusCreateTopicFeeCalculator.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ public void accumulateServiceFee(
3838
lookupServiceFee(feeSchedule, HederaFunctionality.CONSENSUS_CREATE_TOPIC);
3939
feeResult.addServiceFee(1, serviceDef.baseFee());
4040
addExtraFee(feeResult, serviceDef, Extra.KEYS, feeSchedule, keys);
41+
final var hasCustomFees = !op.customFees().isEmpty();
42+
if (hasCustomFees) {
43+
addExtraFee(feeResult, serviceDef, Extra.CONSENSUS_CREATE_TOPIC_WITH_CUSTOM_FEE, feeSchedule, 1);
44+
}
4145
}
4246

4347
@Override

hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/calculator/ConsensusSubmitMessageFeeCalculator.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import com.hedera.hapi.node.base.HederaFunctionality;
77
import com.hedera.hapi.node.transaction.TransactionBody;
8+
import com.hedera.node.app.service.consensus.ReadableTopicStore;
89
import com.hedera.node.app.spi.fees.FeeContext;
910
import com.hedera.node.app.spi.fees.ServiceFeeCalculator;
1011
import edu.umd.cs.findbugs.annotations.NonNull;
@@ -30,6 +31,11 @@ public void accumulateServiceFee(
3031

3132
final var msgSize = op.message().length();
3233
addExtraFee(feeResult, serviceDef, Extra.BYTES, feeSchedule, msgSize);
34+
final var topic = feeContext.readableStore(ReadableTopicStore.class).getTopic(op.topicIDOrThrow());
35+
final var hasCustomFees = (topic != null && !topic.customFees().isEmpty());
36+
if (hasCustomFees) {
37+
addExtraFee(feeResult, serviceDef, Extra.CONSENSUS_SUBMIT_MESSAGE_WITH_CUSTOM_FEE, feeSchedule, 1);
38+
}
3339
}
3440

3541
@Override

hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/ConsensusSubmitMessageFeeCalculatorTest.java

Lines changed: 60 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,19 @@
66
import static org.hiero.hapi.fees.FeeScheduleUtils.makeExtraIncluded;
77
import static org.hiero.hapi.fees.FeeScheduleUtils.makeService;
88
import static org.hiero.hapi.fees.FeeScheduleUtils.makeServiceFee;
9+
import static org.mockito.BDDMockito.given;
10+
import static org.mockito.Mockito.mock;
911

1012
import com.hedera.hapi.node.base.HederaFunctionality;
1113
import com.hedera.hapi.node.consensus.ConsensusSubmitMessageTransactionBody;
14+
import com.hedera.hapi.node.state.consensus.Topic;
1215
import com.hedera.hapi.node.transaction.TransactionBody;
16+
import com.hedera.node.app.service.consensus.ReadableTopicStore;
1317
import com.hedera.node.app.service.consensus.impl.calculator.ConsensusSubmitMessageFeeCalculator;
18+
import com.hedera.node.app.service.consensus.impl.test.handlers.ConsensusTestBase;
1419
import com.hedera.node.app.spi.fees.FeeContext;
1520
import com.hedera.node.app.spi.fees.SimpleFeeCalculatorImpl;
21+
import com.hedera.pbj.runtime.io.buffer.Bytes;
1622
import java.util.List;
1723
import java.util.Set;
1824
import org.assertj.core.api.Assertions;
@@ -32,7 +38,7 @@
3238
* Unit tests for {@link ConsensusSubmitMessageFeeCalculator}.
3339
*/
3440
@ExtendWith(MockitoExtension.class)
35-
public class ConsensusSubmitMessageFeeCalculatorTest {
41+
public class ConsensusSubmitMessageFeeCalculatorTest extends ConsensusTestBase {
3642
@Mock
3743
private FeeContext feeContext;
3844

@@ -46,20 +52,63 @@ void setUp() {
4652
}
4753

4854
@Nested
49-
@DisplayName("update topic")
50-
class UpdateTopicTests {
55+
@DisplayName("submit message")
56+
class SubmitMessageTests {
5157
@Test
52-
@DisplayName("update topic")
53-
void updateTopic() {
54-
final var op = ConsensusSubmitMessageTransactionBody.newBuilder().build();
58+
@DisplayName("submit message to normal topic")
59+
void submitMessage() {
60+
givenValidTopic();
61+
62+
// submit message to topic
63+
final var op = ConsensusSubmitMessageTransactionBody.newBuilder()
64+
.topicID(topicId)
65+
.message(Bytes.wrap("foo"))
66+
.build();
5567
final var body =
5668
TransactionBody.newBuilder().consensusSubmitMessage(op).build();
57-
final var result = feeCalculator.calculateTxFee(body, feeContext);
69+
70+
final var feeCtx = mock(FeeContext.class);
71+
ReadableTopicStore readableStore = mock(ReadableTopicStore.class);
72+
given(storeFactory.readableStore(ReadableTopicStore.class)).willReturn(readableStore);
73+
given(feeCtx.readableStore(ReadableTopicStore.class)).willReturn(readableStore);
74+
given(readableStore.getTopic(topicId))
75+
.willReturn(Topic.newBuilder()
76+
.runningHash(Bytes.wrap(new byte[48]))
77+
.sequenceNumber(1L)
78+
.build());
79+
80+
final var result = feeCalculator.calculateTxFee(body, feeCtx);
5881
assertThat(result).isNotNull();
5982
Assertions.assertThat(result.node).isEqualTo(100000L);
6083
Assertions.assertThat(result.service).isEqualTo(498500000L);
6184
Assertions.assertThat(result.network).isEqualTo(200000L);
6285
}
86+
87+
@Test
88+
@DisplayName("submit message to custom fees topic")
89+
void submitMessageWithCustomFees() {
90+
givenValidTopic();
91+
92+
final var op = ConsensusSubmitMessageTransactionBody.newBuilder()
93+
.topicID(topic.topicId())
94+
.message(Bytes.wrap("foo"))
95+
.build();
96+
final var body =
97+
TransactionBody.newBuilder().consensusSubmitMessage(op).build();
98+
99+
final var feeCtx = mock(FeeContext.class);
100+
ReadableTopicStore readableStore = mock(ReadableTopicStore.class);
101+
given(storeFactory.readableStore(ReadableTopicStore.class)).willReturn(readableStore);
102+
given(feeCtx.readableStore(ReadableTopicStore.class)).willReturn(readableStore);
103+
// the 'topic' variable already has custom fees
104+
given(readableStore.getTopic(topic.topicId())).willReturn(topic);
105+
106+
final var result = feeCalculator.calculateTxFee(body, feeCtx);
107+
assertThat(result).isNotNull();
108+
Assertions.assertThat(result.node).isEqualTo(100000L);
109+
Assertions.assertThat(result.service).isEqualTo(498500000L + 500000000L);
110+
Assertions.assertThat(result.network).isEqualTo(200000L);
111+
}
63112
}
64113

65114
// Helper method to create test fee schedule using real production values from simpleFeesSchedules.json
@@ -74,13 +123,15 @@ private static FeeSchedule createTestFeeSchedule() {
74123
.extras(
75124
makeExtraDef(Extra.SIGNATURES, 1000000L),
76125
makeExtraDef(Extra.KEYS, 100000000L),
77-
makeExtraDef(Extra.BYTES, 110L))
126+
makeExtraDef(Extra.BYTES, 110L),
127+
makeExtraDef(Extra.CONSENSUS_SUBMIT_MESSAGE_WITH_CUSTOM_FEE, 500000000))
78128
.services(makeService(
79129
"Consensus",
80130
makeServiceFee(
81131
HederaFunctionality.CONSENSUS_SUBMIT_MESSAGE,
82132
498500000L,
83-
makeExtraIncluded(Extra.SIGNATURES, 1))))
133+
makeExtraIncluded(Extra.SIGNATURES, 1),
134+
makeExtraIncluded(Extra.CONSENSUS_SUBMIT_MESSAGE_WITH_CUSTOM_FEE, 0))))
84135
.build();
85136
}
86137
}

hedera-node/hedera-file-service-impl/src/main/resources/genesis/simpleFeesSchedules.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@
3838
{ "name": "CRYPTO_TRANSFER_BASE_NFT", "fee": 9000000 },
3939
{ "name": "CRYPTO_TRANSFER_BASE_FUNGIBLE_CUSTOM_FEES", "fee": 19000000 },
4040
{ "name": "CRYPTO_TRANSFER_BASE_NFT_CUSTOM_FEES", "fee": 19000000 },
41+
{ "name": "CONSENSUS_CREATE_TOPIC_WITH_CUSTOM_FEE", "fee": 19900000000 },
42+
{ "name": "CONSENSUS_SUBMIT_MESSAGE_WITH_CUSTOM_FEE", "fee": 499000000},
4143
{ "name": "HOOK_EXECUTION", "fee": 10000000000 }
4244
],
4345
"services": [
@@ -104,7 +106,8 @@
104106
"name": "ConsensusCreateTopic",
105107
"baseFee": 99000000,
106108
"extras": [
107-
{ "name": "KEYS", "includedCount": 0 }
109+
{ "name": "KEYS", "includedCount": 0 },
110+
{ "name": "CONSENSUS_CREATE_TOPIC_WITH_CUSTOM_FEE", "includedCount": 0 }
108111
]
109112
},
110113
{
@@ -118,7 +121,8 @@
118121
"name": "ConsensusSubmitMessage",
119122
"baseFee": 0,
120123
"extras": [
121-
{ "name": "BYTES", "includedCount": 100 }
124+
{ "name": "BYTES", "includedCount": 100 },
125+
{ "name": "CONSENSUS_SUBMIT_MESSAGE_WITH_CUSTOM_FEE", "includedCount": 0 }
122126
]
123127
},
124128
{

hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/fees/ConsensusServiceSimpleFeesSuite.java

Lines changed: 59 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,22 +8,22 @@
88
import static com.hedera.services.bdd.spec.transactions.TxnVerbs.deleteTopic;
99
import static com.hedera.services.bdd.spec.transactions.TxnVerbs.submitMessageTo;
1010
import static com.hedera.services.bdd.spec.transactions.TxnVerbs.updateTopic;
11+
import static com.hedera.services.bdd.spec.transactions.token.CustomFeeSpecs.fixedConsensusHbarFee;
1112
import static com.hedera.services.bdd.spec.utilops.UtilVerbs.compareSimpleToOld;
1213
import static com.hedera.services.bdd.spec.utilops.UtilVerbs.newKeyNamed;
1314
import static com.hedera.services.bdd.suites.HapiSuite.ONE_HBAR;
1415
import static com.hedera.services.bdd.suites.HapiSuite.ONE_HUNDRED_HBARS;
16+
import static com.hedera.services.bdd.suites.HapiSuite.ONE_MILLION_HBARS;
1517

1618
import com.hedera.services.bdd.junit.HapiTestLifecycle;
1719
import com.hedera.services.bdd.junit.LeakyHapiTest;
1820
import java.util.Arrays;
1921
import java.util.stream.Stream;
20-
import org.junit.jupiter.api.Disabled;
2122
import org.junit.jupiter.api.DisplayName;
2223
import org.junit.jupiter.api.DynamicTest;
2324
import org.junit.jupiter.api.Nested;
2425
import org.junit.jupiter.api.Tag;
2526

26-
@Disabled // Test is flaky and produces different results when run locally and in CI
2727
@Tag(SIMPLE_FEES)
2828
@Tag(MATS)
2929
@HapiTestLifecycle
@@ -92,6 +92,26 @@ final Stream<DynamicTest> createTopicWithPayerAdminComparison() {
9292
1);
9393
}
9494

95+
@LeakyHapiTest(overrides = {"fees.simpleFeesEnabled"})
96+
@DisplayName("compare create topic with custom fee")
97+
final Stream<DynamicTest> createTopicCustomFeeComparison() {
98+
return compareSimpleToOld(
99+
() -> Arrays.asList(
100+
cryptoCreate(PAYER).balance(ONE_HUNDRED_HBARS),
101+
cryptoCreate("collector"),
102+
createTopic("testTopic")
103+
.blankMemo()
104+
.withConsensusCustomFee(fixedConsensusHbarFee(88, "collector"))
105+
.payingWith(PAYER)
106+
.fee(ONE_HUNDRED_HBARS)
107+
.via("create-topic-txn")),
108+
"create-topic-txn",
109+
2.0001,
110+
1,
111+
2,
112+
5);
113+
}
114+
95115
@LeakyHapiTest(overrides = {"fees.simpleFeesEnabled"})
96116
@DisplayName("compare update topic with admin key")
97117
final Stream<DynamicTest> updateTopicComparisonWithPayerAdmin() {
@@ -201,12 +221,47 @@ final Stream<DynamicTest> submitBiggerMessageFeeComparison() {
201221
.fee(ONE_HBAR)
202222
.via("submit-message-txn")),
203223
"submit-message-txn",
204-
0.000110,
224+
// base == 0
225+
// network + node = 1000000
226+
// extra bytes = 1023-100= 923
227+
// byte cost = 110000
228+
// total = 102530000
229+
0.01025,
205230
1,
206-
0.000110,
231+
0.01025,
207232
1);
208233
}
209234

235+
@LeakyHapiTest(overrides = {"fees.simpleFeesEnabled"})
236+
@DisplayName("compare submit message with custom fee and included bytes")
237+
final Stream<DynamicTest> submitCustomFeeMessageWithIncludedBytesComparison() {
238+
// 100 is less than the free size, so there's no per byte charge
239+
final var byte_size = 100;
240+
final byte[] messageBytes = new byte[byte_size]; // up to 1k
241+
Arrays.fill(messageBytes, (byte) 0b1);
242+
return compareSimpleToOld(
243+
() -> Arrays.asList(
244+
cryptoCreate(PAYER).balance(ONE_MILLION_HBARS),
245+
cryptoCreate("collector"),
246+
createTopic("testTopic")
247+
.blankMemo()
248+
.withConsensusCustomFee(fixedConsensusHbarFee(88, "collector"))
249+
.payingWith(PAYER)
250+
.fee(ONE_HUNDRED_HBARS)
251+
.via("create-topic-txn"),
252+
// submit message, provide up to 1 hbar to pay for it
253+
submitMessageTo("testTopic")
254+
.blankMemo()
255+
.payingWith(PAYER)
256+
.message(new String(messageBytes))
257+
.fee(ONE_HUNDRED_HBARS)
258+
.via("submit-message-txn")),
259+
"submit-message-txn",
260+
0.05,
261+
1,
262+
0.05,
263+
1);
264+
}
210265
// TODO: support queries
211266
// @HapiTest()
212267
// @DisplayName("compare get topic info")

hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/fees/SimpleFeesSuite.java

Lines changed: 0 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -87,72 +87,6 @@ private static long ucents(int value) {
8787
return value * 100000;
8888
}
8989

90-
/*
91-
Disable custom fees for now.
92-
@Nested
93-
class TopicCustomFees {
94-
@HapiTest
95-
@DisplayName("compare create topic with custom fee")
96-
final Stream<DynamicTest> createTopicCustomFeeComparison() {
97-
return runBeforeAfter(
98-
cryptoCreate(PAYER).balance(ONE_HUNDRED_HBARS),
99-
cryptoCreate("collector"),
100-
createTopic("testTopic")
101-
.blankMemo()
102-
.withConsensusCustomFee(fixedConsensusHbarFee(88, "collector"))
103-
.payingWith(PAYER)
104-
.fee(ONE_HUNDRED_HBARS)
105-
.via("create-topic-txn"),
106-
validateChargedUsd(
107-
"create-topic-txn",
108-
ucents_to_USD(
109-
1000 // base fee for create topic
110-
+ 200_000 // custom fee
111-
+ 0 // node + network fee
112-
)));
113-
}
114-
115-
@LeakyHapiTest(overrides = {"fees.simpleFeesEnabled"})
116-
@DisplayName("compare submit message with custom fee and included bytes")
117-
final Stream<DynamicTest> submitCustomFeeMessageWithIncludedBytesComparison() {
118-
// 100 is less than the free size, so there's no per byte charge
119-
final var byte_size = 100;
120-
final byte[] messageBytes = new byte[byte_size]; // up to 1k
121-
Arrays.fill(messageBytes, (byte) 0b1);
122-
return runBeforeAfter(
123-
cryptoCreate(PAYER).balance(ONE_MILLION_HBARS),
124-
cryptoCreate("collector"),
125-
createTopic("testTopic")
126-
.blankMemo()
127-
.withConsensusCustomFee(fixedConsensusHbarFee(88, "collector"))
128-
.payingWith(PAYER)
129-
.fee(ONE_HUNDRED_HBARS)
130-
.via("create-topic-txn"),
131-
validateChargedUsd(
132-
"create-topic-txn",
133-
ucents_to_USD(
134-
1000 // base fee for create topic
135-
+ 200_000 // custom fee
136-
+ 1 * 3 // node + network fee
137-
)),
138-
// submit message, provide up to 1 hbar to pay for it
139-
submitMessageTo("testTopic")
140-
.blankMemo()
141-
.payingWith(PAYER)
142-
.message(new String(messageBytes))
143-
.fee(ONE_HUNDRED_HBARS)
144-
.via("submit-message-txn"),
145-
validateChargedUsd(
146-
"submit-message-txn",
147-
ucents_to_USD(
148-
7 // base fee
149-
+ 5000 // custom fee
150-
+ 1 * 3 // node + network fee
151-
)));
152-
}
153-
}
154-
*/
155-
15690
@Nested
15791
class TopicFees {
15892
/*

0 commit comments

Comments
 (0)